《流畅的Python》笔记。本篇是“面向对象惯用方法”的第四篇,主要讨论接口。本篇内容将从鸭子类型的动态协议,逐渐过渡到使接口更明确、能验证明现是否符合规定的抽象基类(Abstract Base Class, ABC)。python
本篇讨论Python中接口的实现问题,主要内容以下:程序员
补充在正文以前:bash
猴子补丁并非Python特有,它指动态语言中,不用修改源代码,在运行时就能对代码的功能进行动态的追加或变动。下面的代码展现了猴子补丁的用法:微信
# 代码2.1 # 在文件中定义 class MyList: def __init__(self, iterable): self._data = list(iterable) def __len__(self): return len(self._data) def __getitem__(self, index): return self._data[index] # 下面的代码在控制台运行 >>> from random import shuffle >>> from my_list import MyList >>> mylist = MyList(range(10)) >>> def set_item(temp, i, item): ... temp._data[i] = item ... >>> MyList.__setitem__ = set_item >>> shuffle(mylist) >>> deck[:] [6, 3, 0, 1, 5, 4, 2, 7, 9, 8]
解释:app
random.shuffle
函数,对象必须实现__setitem__
方法,上述代码在运行时动态添加所需方法;set_item
函数的第一个参数并非self
,这是想说明,每一个Python方法说到底都是普通函数,把第一个参数命名为self
只是一种约定(但别随意打破这种约定)。这里之因此讲猴子补丁,主要是为了说明协议能够是动态的:即便对象最初没有实现某个协议,当须要时,咱们也能为它动态添加。框架
介绍完动态实现接口后,如今开始讨论抽象基类,它属于静态显示地实现接口。dom
有时候咱们须要明确区分“抽象类”(并非指“抽象基类”)与“接口”:以天然界为例,“抽象类”通常用于同一物种同一行为,而“接口”则用于不一样物种同一行为。固然,这两个概念有交叉的部分,某些行为既能够归到“接口“,也能够归到”抽象类“,而最后归到谁就见仁见智了。但这两个概念又有很大的类似之处,它们的实质都是:让某些对象拥有同名的方法或属性,但具体实现不必定相同。ssh
Java更注重这二者的特性,而Python、C++则更注重这二者的共性。也所以,Java不支持多重继承(固然,也是为了下降复杂性),用明确的接口类interface
来区分与abstract class
;而在Python和C++中,则用抽象基类充当接口。因此,在Python中,直接继承自抽象基类,更多代表的是”要实现某种接口或协议“,而非”要新建某个具体类的子类“。函数
若是要测试是否继承自抽象基类,推荐使用isinstance
和issubclass
方法,而不是is
运算。但也不要滥用这类方法,由于这种代码用多了说明面向对象设计得很差。测试
说道isinstance
,还有个与之相关的概念,至关于“鸭子类型”的强化版:
cls
是抽象基类,即cls
的元素是abc.ABCMeta
,就可使用isinstance(obj, cls)
。小插曲:这是书中给出的标准定义,笔者读到这的时候一脸懵逼。“白鹅类型”是个名词,但这定义倒是对一个过程的描述,因此“白鹅类型”究竟是个啥(这究竟是翻译的锅仍是做者的锅)?后来谷歌了一下,再本身反复推敲,得出以下总结:鸭子类型是指某个实例实现了某个方法,就能够说它属于某个类型,不必定要继承;而白鹅类型则是指能被断定成某抽象基类的子类的实例,即,能使isinstance(obj, cls)
返回True
的obj
就是白鹅类型,其中cls
是抽象基类。注意,这些子类并不必定是经过继承而来,也多是经过注册而来,还多是经过实现某些方法而来。
特别提醒:对于抽象基类(还有元类)的使用,并不建议在生产代码中自行定义新的抽象基类和元类。定义抽象基类和元类的工做通常由比较资深的Python程序员来作,适用于写框架的程序员。而即使是资深Python程序员也不常本身定义抽象基类和元类。
从Python2.6开始,标准库提供了抽象基类。大多数抽象基类在collections.abc
模块中定义,numbers
和io
中也有一些。
如下是collections.abc
中16个抽象基类的UML图(关于多重继承的内容将在之后的文章中讲解):
有几个抽象基类值得注意:
Iterable
、Container
和Sized
:各个集合类应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable
经过__iter__
方法支持迭代;Container
经过__contains__
方法支持in
运算;Sized
经过__len__
方法支持len()
函数;Sequence
、Mapping
和Set
:这三个是主要的不可变集合类型,并且各自都有可变的子类,即MutableSequence
、MutableMapping
和MutableSet
。Callable
和Hashable
:从图上能够看出,这两个抽象基类在标准库中没有子类。在numbers
包中的抽象基类的继承关系则很简单,都是线性的(“数字塔”)。下面5个类从左到右依次派生:
Number
,Complex
,Real
,Rational
,Integral
下面咱们将自行定义一个抽象基类并继承出它的子类。但这并非鼓励各位在生产代码中自定义抽象基类!
咱们将模拟一个随机抽奖机,它的抽象基类是Tombola
,它的4个方法以下:
.load(...)
:抽象方法,把元素放入容器;.pick()
:抽象方法,从容器中随机返回一个元素,并从容器中删除该元素;.loaded()
:当容器不为空是返回True
;.inspect()
:返回一个有序元组,由容器中的现有元素构成,不修改容器的内容(容器内部元素顺序不保留)。它和它的三个子类的UML图以下:
如下是Tombola
的定义:
# 代码3.1 import abc class Tombola(abc.ABC): @abc.abstractmethod def load(self, iterable): """从可迭代对象中添加元素""" @abc.abstractmethod def pick(self): """随机删除元素,而后将其返回。 若是实例为空,这个方法应该抛出LookupError, 这个异常是IndexError和KeyError的基类""" def loaded(self): # 比较耗时,子类可重写 """当容器不为空时返回True""" 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))
解释及补充:
super()
函数调用抽象方法,为它添加功能,而不是从头开始写;abc
的模块,一个是前面说的collections.abc
,另外一个就是这里的abc
模块。只有在新定义抽象基类的时候才用获得abc.ABC
,每一个抽象基类都依赖这个类。在abc
模块中原本还有@abstractclassmethod
,@abstractstaticmethod
和@abstractproperty
三个装饰器,但这三个从Python3.3起被废除了,由于这三个的功能都能在@abstractmethod
上堆叠其余装饰器获得,好比实现@abstractclassmethod
的功能:
# 代码3.2 class MyABC(abc.ABC): @classmethod @abc.abstractmethod def an_abstract_classmethod(cls, ...): pass
如下是它的两个子类的实现代码:
# # 代码3.3 class BingoCage(Tombola): # loaded()和inspect()延用抽象基类的实现 def __init__(self, items): self._randomizer = random.SystemRandom() # 它会调用os.urandom() self._items = [] self.load(items) # 委托给load()方法实现初始加载 def load(self, items): # 必须实现抽象方法! self._items.extend(items) self._randomizer.shuffle(self._items) def pick(self): # 必须实现抽象方法! try: return self._items.pop() except IndexError: raise LookupError("pick from empty BingoCage") def __call__(self): self.pick() class LotteryBlower(Tombola): def __init__(self, iterable): self._balls = list(iterable) # 副本 def load(self, iterable): self._balls.extend(iterable) def pick(self): try: position = random.randrange(len(self._balls)) except ValueError: # 为了兼容Tombola,并非抛出ValueError raise LookupError("pick from empty LotteryBlower") return self._balls.pop(position) def loaded(self): # 覆盖了抽象基类低效的版本 return bool(self._balls) def inspect(self): return tuple(sorted(self._balls))
上面两个子类都是直接继承自Tombola
,而白鹅类型有一个基本特性:即使不用继承,也能将一个类注册为抽象基类的虚拟子类。下面是TomboList
的实现:
# 代码3.4 @Tombola.register # 把TomboList注册为Tombola的虚拟子类 class TomboList(list): # 它同时仍是list的真实子类,而list实际上是MutableSequence的虚拟子类 def pick(self): if self: position = random.randrange(len(self)) return self.pop(position) else: raise LookupError("pick from empty LotteryBlower") load = list.extend # 当我看到竟然这么实现方法时,感受本身好肤浅...... def loaded(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombola.register(TomboList) 这是register的函数调用版本
下面是这个子类的简单使用:
# 代码3.5 >>> issubclass(TomboList, Tombola) True # TomboList是Tombola的子类 >>> t = TomboList(range(100)) >>> isinstance(t, Tombola) True # TomboList的实例也是Tombola类型 >>> TomboList.__mro__ (<class 'mytest.TomboList'>, <class 'list'>, <class 'object'>) >>> TomboList.__subclasses__() [<class 'mytest.BingoCage'>, <class 'mytest.LotteryBlower'>]
解释及补充:
__mro__
中,即方法解析顺序(Method Resolution Order)。它按顺序列出类及其超类,Python则会按照这个顺序搜索方法。从上述结果能够看出,这个属性只存储了“真实的”超类。__subclasses__
方法返回类的直接子类列表,不含虚拟子类;register
能够当作装饰器用,但更经常使用的作法仍是把它当函数使用。鹅的行为有可能像鸭子。先看以下代码:
# 代码3.6 >>> class Struggle: ... def __len__(self): return 23 ... >>> from collections import abc >>> isinstance(Struggle(), abc.Sized) True >>> issubclass(Struggle, abc.Sized) True
这里既没有继承,也没有注册,但Struggle
依然被issubclass
判断为abc.Sized
的子类。之因此会这样,是由于abc.Sized
实现了一个特殊的类方法__subclasshook__
:
# # 代码3.7,abc.Sized的实如今 _collections_abc.py 中 class Sized(metaclass=ABCMeta): __slots__ = () @abstractmethod def __len__(self): return 0 @classmethod def __subclasshook__(cls, C): if cls is Sized: # 源代码中是 return _check_methods(C, "__len__"),这里修改了一下 if any("__len__" in B.__dict__ for B in C.__mro__): return True return NotImplemented
这像不像鸭子类型?只要实现了__len__
方法,这个类就是abc.Sized
的子类。
在自定义的抽象基类中并不必定要实现__subclasshook__
方法,由于即便在Python源码中,目前也只见到Sized
这一个抽象基类实现了__subclasshook__
方法,并且Sized
只有一个特殊方法。在决定自行实现__subclasshook__
方法以前,请想清楚你必定须要这个方法吗?你的能力可以保证这个方法的可靠性吗?
本篇讨论的话题只有一个,即“接口”。首先咱们讨论了鸭子类型的高度动态性,它实现的是动态协议,也是非正式接口;随后咱们借助“白鹅类型”,使用抽象基类明确地、显示地声明接口,而后经过子类或注册来实现这些接口。期间,咱们自定义了一个抽象基类,并经过继承实现了它的两个子类,还经过注册实现了它的一个虚拟子类。
最后,仍是那句话:不要轻易自定义抽象基类,除非你想构件容许用户扩展的框架。平常使用中,咱们与抽象基类的联系应该是建立现有抽象基类的子类,或者使用现有的抽象基类注册。本身从头编写新抽象基类的状况很是少。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~