Python基础教程(第三版)(七)再谈抽象

时间:2023-02-25 10:00:25

菜鸡的学习笔记。

7.1 对象魔法

  • 多态:可对不同类型的对象执行相同的操作,但是操作将随对象所属的类型而异;
  • 封装:对外隐藏对象内部工作原理的细节;
  • 继承:可基于通用类创建出专用类。
    按作者的意思,多态最难懂,但也最有趣,就先讲多态。

7.1.1 多态

假如要实现查询物品价格的函数,但是有人用元组来存放(‘item’,price)数据,有人用字典来存放{‘item’: price}数据,还有人用别的来对象存放数据,那么在查询价格时,就要先判断对象的类型,再根据索引或者键值去获得价格。现在假设每种对象都有一种能查询价格的方法,那么就不用再去判断对象类型,只用调用方法即可,例如Object.get_price(),这就是多态(也有一定程度上封装)。


7.1.2 多态和方法

有很多函数都用了多态,例如+函数,repr()函数,等等。
关于鸭子类型。就是只要你会走得像鸭子,叫的像鸭子,就可以把你当成鸭子来处理,只要你有某种方法,就可以用这种方法去实现某种功能,而不管你是什么(自己的理解)。


7.1.3 封装

主要理解封装和多态的区别。封装和多态很像,都是抽象。但是二者的区别是多态让你无需知道对象所属的类就能调用其方法,而封装就让你无需知道对象的构造就能使用它。
书中的????:
比如有两个相同类的对象,都有get_name(),set_name()的方法,但是都关联到了一个全局变量name,这样更改其中任何一个,另一个就会变;这就需要封装,把name封装到对象内部,让每个对象都有一个自己的name,这样就会互不影响。


7.1.4 继承

让通用类衍生出专用类,并且不会丢失其中的必要方法。


7.2 类

7.2.1 类到底是什么

类就是类型,对象是类的实例化。人是个类,你就是个对象。
超类子类:如麻雀是鸟的子类,鸟是麻雀的超类。
子类可以增加新的方法,也可以重写超类的方法,比如鸟并不都吃肉,但是老鹰会吃肉,可以给子类老鹰新加方法eat_meat();又如鸟有方法fly(),但是子类企鹅不会飞,就应该重写fly(),让fly()起不到什么作用。


7.2.2 创建自定义类

和JAVA类似。别忘了self,创建对象似乎不需要new。


7.2.3 属性、函数和方法

主要就是理解self


7.2.4 再谈隐藏

>>>c.name
'Sam'
>>>c.name = 'Tom'
>>>c.get_name()
'Tom'

上面的代码不通过set_name()方法直接修改了变量。有的程序员声称这样违背了封装原则,他们认为应该对外完全隐藏对象的状态。为什么他们会如此极端?直接访问name不就好了,为什么非要用set_name,get_name呢?

关键是其他程序猿可能不知道(也不应该知道)对象内部发生的情况。例如,ClosedObject可能在对象修改其名称时向管理员发送电子邮件。这种功能可能包含在方法set_name中。但如果直接设置c.name,结果将如何呢?什么都不会发生——根本不会发送电子邮件。为避免这类问题,可将属性定义为私有。私有属性不能从对象外部访问,而只能通过存取器方法(如get_name和set_name)来访问。

可以用两个下划线让方法或者属性变为私有。
这种方法是变相的变为私有,只是对其名称做了转换,在开头加个下划线和类名即可访问,但是不推荐。

>>>s = Secretive()
>>>s.__inaccessible()  # 会报错
s._Secretive__inaccessible()  #不会报错

7.2.5 类的命名空间

额外的知识——下面两条语句等价:

def foo(x): return x * x
foo = lambda x: x * x

进入正题:

class MemberCounter:
    members = 0
    def init(self):
        MemberCounter.members += 1
m1 = MemberCounter()
m2 = MemberCounter()
m1.init()
print(MemberCounter.members)
m2.init()
print(MemberCounter.members)

结果:Python基础教程(第三版)(七)再谈抽象

上述代码在类作用域定义了一个变量,所有的成员(实例)都可访问它。

自己体会一下,不难理解,但要注意,定义init()时,写成members +=1 会报错,必须要加上类名。
有个特殊情况:

m1.members = 'Two'
print(m1.members)
print(m2.members)

结果:Python基础教程(第三版)(七)再谈抽象
相当于m1写入了一个新属性,覆盖了类级变量


7.2.6 指定超类

在class语句的类名后面通过用圆括号把超类名称括起来来制定超类。
一个栗子:

class Filter:
    def init(self):
        self.blocked = []

    def filter(self,sequence):
        return [x for x in sequence if x not in self.blocked]

class SPAMFilter(Filter):
    def init(self):
        self.blocked = ['SPAM']

f = Filter()
f.init()
print(f.filter([1, 2, 3]))

s = SPAMFilter()
s.init()
print(s.filter(['SPAM', 1, 2, 3]))

结果:
Python基础教程(第三版)(七)再谈抽象

  • 对于超类中的函数可以重写,如例子中的init;
  • 可以从超类中继承方法,如filter,不需要再次定义,提高了效率。

7.2.7 深入讨论继承

print(issubclass(SPAMFilter, Filter))
print(SPAMFilter.__bases__)
print(isinstance(s, SPAMFilter))
print(isinstance(s, Filter))

结果:Python基础教程(第三版)(七)再谈抽象

  • 判断一个类是否是另一个类的子类,可以用方法issubclass;
  • 想要知道某个类的基类,访问它的特殊属性__bases__;
  • 想要知道某个对象是否是某个类的实例,用方法isinstance;如果一个对象是某个特定的类的实例,那么也是该类的超类的实例。

    7.2.8 多个超类

    即多重继承,在圆括号中增加要继承的类即可。
    注意:如果继承的类中有方法重名,那么圆括号中前面的类的方法会覆盖后面的类的方法;
              慎用超类,容易带来Bug。


    7.2.9 接口和内省

    暂时没有理解什么是接口、内省。
    等以后结合java的接口理解一下。
    掌握两种方法
    hasattr(tc, 'talk')
    callable(getattr(tc, 'talk', None))
  • 第一种检查某个对象是否有某个属性,返回True或者False;
  • 第二种getattr,可以在属性不存在时设置返回的值。


7.2.10 抽象基类

使用模块abc可以创建抽象基类。抽象基类用于指定子类必须提供哪些功能,却不实现这些功能。
书中的这段话理解的还不透彻,先记录下来:

然而,有比手工检查各个方法更好的选择。在历史上的大部分时间内,Python几乎都只依赖于鸭子类型,即假设所有对象都能完成其工作,同时偶尔使用hasattr来检查所需的方法是否存在。很多其他语言(如JAVA和Go)都采用显式指定接口的理念,而有些第三方模块提供了这种理念的各种实现。最终,Python通过引入模块abc提供了官方解决方案。这个模块为所谓的抽象基类提供了支持。一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实现的一组抽象方法。

class Talker(ABC):
    @abstractmethod
    def talk(self):
        pass

上面的类是抽象基类ABC派生出的抽象子类,是不能够实例化的,它的作用只是为了规定其子类必须有talk方法。
实例化 Talker类会报错
Python基础教程(第三版)(七)再谈抽象
如果定义这么一个子类:

class Knigget(Talker):
    pass

由于没有重写方法talk,这个子类也是抽象类,也是无法实例化的。
如果重写了方法talk:

class Knigget(Talker):
    def talk(self):
        print("yeah!")

现在就可以实例化。从另一个角度说,如果一个对象确实是Talker对象,那么它一定有方法talk。

k = Knigget()
k.talk()
print(isinstance(k, Talker))

结果:Python基础教程(第三版)(七)再谈抽象
下图中Herring虽然有talk方法,但是isinstance判False。用Talker派生可以解决问题。如果是引用的类,无法自己定义,可以用register。但是regiser会把没有talk方法的类也弄成Talker的子类
书上说,只要实现了方法talk,即使不是Talker的子类,依然能够通过类型检查。(这句话不明白是啥意思,在register之前isinstance的结果明明是False)

class Herring:
    def talk(self):
        print("em...")

h = Herring()
h.talk()
print(isinstance(h, Talker))
Talker.register(Herring)
print(isinstance(h, Talker))

结果:Python基础教程(第三版)(七)再谈抽象


7.3 关于面向对象设计的一些思考

  • 将相关的东西放在一起。如果一个函数操作一个全局变量,最好将它们作为一个类的属性和方法;
  • 不要让对象之间过于亲密。方法应只关心其所属实例的属性,对于其他实例的状态,让它们自己去管理就好了;
  • 慎用继承,尤其是多态继承。继承有时很有用,但在有些情况下可能带来不必要的复杂性。要正确地使用多重继承很难,要排除其中的Bug更难。
  • 保持简单,让方法短小紧凑。一般而言,应确保大多数方法都能在30秒内读完并理解。对于其余的方法,尽可能将其篇幅控制在一页或一屏内。

确定哪些类以及这些类应该包含哪些方法时,尝试这样做:

  1. 将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词、形容词加上标记;
  2. 在名词中找出可能的类;
  3. 在动词中找出可能的方法;
  4. 在形容词中找出可能的属性;
  5. 将找出的方法和属性分配给个各类。

Python基础教程(第三版)(八)异常