接口:从协议到抽象基类--Fluent Python第十一章读书心得

接口:从协议到抽象基类


动物们:什么是鸭子类型,白鹅类型和猴子补丁


鸭子类型和白鹅类型:

-我眼中的- 鸭子类型 白鹅类型
代表什么 协议 接口
特征 动态 规范
  • 所谓鸭子类型,就是说我们需要的鸭子不一定是真正的鸭子。我们只关注我们要求的方面像鸭子。因此我们甚至可以说我们实现的类,它既是鸭子,也是麻雀,甚至还有可能是犀牛。

  • 而白鹅类型的提出则更加的具有规范性:我们判断一个类是不是白鹅,不是根据这个类具体的行为,而是去看这个类是否继承自白鹅基类。即它的体内有没有白鹅的DNA。

因此我们所说的使用鸭子类型和白鹅类型时,其实就是在说: “符合相应的协议就可以||或者你要去继承相应的抽象基类,去实现接口所必须的方法”


猴子补丁:

作者在讲解猴子补丁的时候运用了一个例子,在一开始就定义的扑克牌类中,想要实现洗牌。

  • 不使用猴子补丁:

    • 创建一个方法shuffle,用于洗牌
  • 使用猴子补丁:

    • 由于标准库中有random.shuffle方法,所以我们只需要设法将类的实例变为参数,所以只需要实现序列协议中的__setitem__方法。

同样是为了实现洗牌功能,两种方式的行为却不太一样。而第二种的行为称之为补丁是比较贴切的,像是在自己没有完全实现的协议上打补丁一样。(实现有缺陷的协议–>实现更完善的协议)


抽象基类: 抽象方法和具体方法

什么是抽象基类:

在白鹅类型的定义中提到了抽象基类,但是抽象基类到底是什么呢?
抽象基类也是类。

import abc

class Tombola(abc.ABC):
	@abc.abstractmethod
	def load(self, iterable):

@abc.abstractmethod
	def pick(self): 

def loaded(self):
		return bool(self.inspect())

def inspect(self):
		items = []
		while True:
			try:
				items.append(self.pick())
			except LookupError:
				break
		self.load(items)
		return tuple(sorted(items))
  • @abstractmethod

    • 这个装饰器用于把方法标记为抽象方法,交由具体类去实现
    • 正因为需要去交给具体类去实现,所以无论如何都会被覆盖的抽象方法里什么都不用写
    • 什么都不用写啊!因为是抽象基类,也不需要去单独实例化的
    • 句法:
      • @abstractmethod上可以堆叠装饰器
      • @abstractmethoddef之间不可以有其他装饰器
  • 抽象方法->内部接口->具体方法

    • 如果被@abstractmethod的是抽象方法,那么剩下的方法是什么呢?
    • 是具体方法:
      • 具体方法是由抽象方法通过内部接口实现的。
      • 你的抽象方法和具体方法都是看得见摸得着的,但是内部接口这个名词其实并没有什么具体的意义。
      • 虽然没有显式规定说具体方法一定要调用抽象方法,但是最好还是要调用一下的。不然和写死了有什么区别。
  • 子类应该做什么:

    • 首先,为了实现抽象基类中的具体方法,你需要实现抽象方法
    • 如果你想要覆写其他具体方法,可以形成重载
    • 结论:至少要实现所有的抽象方法

警告:没事儿干不要自己去实现抽象基类


子类还是虚拟子类?

虚拟子类:

我们都知道,如果一个类继承自别的类,那么它就是这个类的子类。
大概还是要遵循一下语法的:

class BingoCage(Tombola):

这个时候就是继承自超类的子类了。如果使用类型检查函数isinstance去检查也完全没有问题。

但是白鹅类型提供了一个十分动态的方案: 注册

注册:

python提供了register函数来注册一个类为抽象基类的子类。
使用方式有两种:

–> 使用装饰器句法:(python3.3以后)

@Tombola.register
class TomboList(List):
#....

–> 使用函数:

Tombola.register(TomboList)
  • 注册这个东西的作用是隐式的声明,它只是在告诉python解释器:这个类是这个抽象基类的子类
  • 因此在调用isinstance进行类型检查的时候,python会直接返回True
  • 但是在调用的时候,如果你的子类并没有完全的实现抽象方法,那么会发生什么呢?
    • 当然是Error啦

“鹅的行为可能像鸭子”

在白鹅类型中我们强调的是规则:只有当你全部实现了抽象方法的时候,才可以去继承所谓的抽象基类,实现所谓的接口
但是你肯定会疑惑: 为什么要留一条名为注册的后路,这貌似不符合白鹅类型的标准。
事实上,不仅仅是注册。鸭子类型和白鹅类型都不是独立存在的。它们都是在我们面向对象的过程中产生的。

在自然界中,鹅的行为可以像鸭子 (当然反过来也可以)
举例说明: 如果手动实现了抽象方法,既没有继承也没有注册。会有什么后果。

>>> class Struggle:
    ...def __len__(self): return 23
    ...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True

事实上我们的Struggle类只是实现了len()方法而已,但是python解释器却认同了二者的类型。
即: Struggle是一个 Sized

而这一切的来源,是Sized类中的一个方法:__subclasshook__:

  • 简单的说,这个方法中定义的操作是用来检查一个类是否有资格成为它的子类。
    • 如果这个子类有资格成为这个抽象基类的子类,便认同它为自己的子类。(即“它”(可以)是一个“我”)
    • 这非常的6,因为我们并没有去显式的声明,而代码使用了一种检查的方式变相地实现了对于鸭子类型的识别。
    • 这或许可以叫作: 白鹅协议

白鹅协议:

一只动物,我们如果说它是某某白鹅,那么它必须有白鹅的DNA。
但是如果我们需要一个鸭子来展出。我们仍然可能可以把这只白鹅的近亲放上去而不被发现有问题。因为某种程度上,这个动物的行为很可能很像鸭子。

以下是人话:

一个实现的类,如果说他是一个某某类型,那么他应该继承该对应的抽象基类。
但是当我们需要一个其他实例的场景中,如果这个类实现了相关的方法,我们也可以把它的实例传进去而不会抛出Error,因为即便类是一个某某抽象基类的子类,它也仍然可以遵守很多其他的协议。

你可能感兴趣的:(python)