您的位置:首页 > 编程语言 > Python开发

《流畅的Python》读书笔记——接口:从协议到抽象基类

2020-04-01 18:29 639 查看

Python文化中的接口和协议

接口是实现特定角色的方法集合。

协议是接口,但不是正式的,因此协议不能像正式接口那样施加限制。

序列协议是 Python 最基础的协议之一。即便对象只实现了那个协议最基
本的一部分,解释器也会负责任地处理。

Python喜欢序列

Python 数据模型的哲学是尽量支持基本协议。


Sequence 抽象基类和 collections.abc 中相关抽象类的UML 类图

下面我们来验证一下这句话:即便对象只实现了那个协议最基
本的一部分,解释器也会负责任地处理。

In [1]: class Foo:
...:     def __getitem__(self,pos):
...:         return range(0, 30, 10)[pos]
...:
In [2]: f = Foo()
In [3]: f[1]
Out[3]: 10
In [4]: for i in f: print(i)
0
10
20
In [5]: 20 in f
Out[5]: True
In [6]: 15 in f
Out[6]: False

可以看到,

foo
类没有继承
abc.Sequence
,只实现了序列协议的一个方法:
__getitem__
,这样就可以访问元素,迭代和使用
in
运算符了。

虽然没有

__iter__
方法,但是 Foo 实例是可迭代的对象,因为发现
__getitem__
方法时,Python 会调用它,传入从 0 开始的整数索
引,尝试迭代对象(这是一种后备机制)。
尽管没有实现
__contains__
方法,但是 Python 足够智能,能迭代 Foo 实例,因此也能使用
in
运算符:Python 会做全面检查,看看有没有指定的元素。
综上,鉴于序列协议的重要性,如果没有
__iter__

__contains__
方法,Python 会调用
__getitem__
方法,设法让迭
代和
in
运算符可用。

使用猴子补丁在运行时实现协议

如果遵守既定协议,很有可能增加利用现有的标准库和第三
方代码的可能性,这得益于鸭子类型。

实现序列协议的FrenchDeck类:

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
return self._cards[position]

在上例中的FrenchDeck(法国什么鬼牌)有个重大缺陷:无法洗牌。

如果FrenchDeck实例的行为像序列,那它就不需要

shuffle
(洗牌算法),因为
random.shuffle
函数的作用就是就地打乱序列。

我们尝试下:

>>> from random import shuffle
>>> from french_deck import FrenchDeck
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "...\Python36\lib\random.py", line 274, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

报错消息说该对象不支持元素赋值。

因为

shuffle
函数需要调换集合中元素的位置,因此该类还需要实现
__setitem__
方法。

Python 是动态语言,因此我们可以在运行时修正这个问题,甚至还可以在交互式控制台中。(一个字,强)

>>> def set_card(deck, position, card): #这里把self命名为deck
...     deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card
>>> shuffle(deck)
>>> deck[:5]
[Card(rank='7', suit='spades'), Card(rank='J', suit='hearts'), Card(rank='6', suit='hearts'), Card(rank='8', suit='diamonds'), Card(rank='A', suit='hearts')]

该示例强调了协议是动态的:

random.shuffle
函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来再提供也行。

Alex Martelli的水禽

其实,抽象基类的本质就是几个特殊方法。例如:

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

abc.Sized
能把
Struggle
识别为自己的子类,只要实现了特殊方法
__len__
即可。

定义抽象基类的子类

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck2(collections.MutableSequence):
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
return self._cards[position]

# 为了支持洗牌函数而实现的方法
def __setitem__(self, position, value):
self._cards[position] = value

# 继续MutableSequence的类必须实现的方法
def __delitem__(self, position):
del self._cards[position]

# 还要实现insert方法
def insert(self, position, value):
self._cards.insert(position,value)

继承

MutableSequence
抽象基类需要实现
__delitem__
insert
方法。

看了

MutableSequence
源码可知,里面有三个抽象方法,也就是说抽象方法子类必须要实现。

class MutableSequence(Sequence):
@abstractmethod
def __setitem__(self, index, value):
raise IndexError

@abstractmethod
def __delitem__(self, index):
raise IndexError

@abstractmethod
def insert(self, index, value):
'S.insert(index, value) -- insert value before index'
raise IndexError

为了充分使用抽象基类,我们要知道有哪些抽象基类可用。接下来介绍
集合抽象基类。

标准库中的抽象基类

collections.abc模块中的抽象基类

Python 3.4 在 collections.abc 模块中定义了 16 个抽象基类,简要
的 UML 类图(没有属性名称)如图所示

  • Iterable、Container 和 Sized
    各个集合应该继承这三个抽象基类,或者至少实现兼容的协
    议。Iterable 通过
    __iter__
    方法支持迭代,Container 通过
    __contains__
    方法支持 in 运算符,Sized 通过
    __len__
    方法支持
    len() 函数。
  • Sequence、Mapping 和 Set
    这三个是主要的不可变集合类型,而且各自都有可变的子类
  • MappingView
    在 Python 3 中,映射方法
    .items()
    .keys()
    .values()
    返回的对象分别是
    ItemsView
    KeysView
    ValuesView
    的实例。
  • Callable 和 Hashable
    这两个抽象基类与集合没有太大的关系,只不过因为
    collections.abc 是标准库中定义抽象基类的第一个模块,而它们
    又太重要了,因此才把它们放到 collections.abc 模块中。我从未
    见过 Callable 或 Hashable 的子类。这两个抽象基类的主要作用是
    为内置函数 isinstance 提供支持,以一种安全的方式判断对象能不
    能调用或散列。
  • Iterator
    它是 Iterable 的子类

抽象基类的数字塔

numbers 包定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中 Number 是位于最顶端的超类,随后是 Complex 子类,依次往下,最底端是Integral 类:

  • Number
  • Complex(复数)
  • Real(实数)
  • Rational
  • Integral(整数)

如果想检查一个数是不是整数,可以使用

isinstance(x,numbers.Integral)

如果一个值可能是浮点数类型,可以使用
isinstance(x,numbers.Real)

定义并使用一个抽象基类

为了证明有必要定义抽象基类,我们要在框架中找到使用它的场景。想
象一下这个场景:你要在网站或移动应用中显示随机广告,但是在整个
广告清单轮转一遍之前,不重复显示广告。假设我们在构建一个广告管
理框架,名为 ADAM。它的职责之一是,支持用户提供随机挑选的无重
复类。 为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么
意思,我们将定义一个抽象基类。

我们把这个抽象基类命名为 Tombola。


抽象基类:

import abc

class Tombola(abc.ABC): #自己定义的抽象基类要继承abc.ABC
@abc.abstractclassmethod #抽象方法
def load(self, iterable):
"""从可迭代对象中添加元素"""

@abc.abstractclassmethod
def pick(self):
"""随机删除元素,然后将其返回。

如果实例为空,这个方法应该抛出`LookupError`。
"""

def loaded(self):
"""如果至少有一个元素,返回`True`,否则返回`False`。"""
return bool(self.inspect()) #抽象基类的具体方法只能使用抽象基类中的其他具体方法、抽象方法或特性

def inspect(self):
"""返回一个有序元组,由当前元素构成。"""
items = []
while True:
try:
items.append(self.pick())#不断调用.pick方法
except LookupError:
break
self.load(items) #再使用load方法把所有元素放回去
return tuple(sorted(items))

IndexError
KeyError
都是
LookupError
的子类。

自己定义的抽象基类完成了,为了看一下抽象基类对接口所做的检查,尝试使用一个有缺陷的实现来糊弄

Tombola

>>> from tombola import Tombola
>>> class Fake(Tombola):
...     def pick(self):
...             return 13
...
>>> Fake
<class '__main__.Fake'>
>>> f = Fake() #尝试实例化时抛出了错误
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Fake with abstract methods load

抽象基类句法详解

声明抽象基类最简单的方式是继承 abc.ABC` 或其他抽象基类。

abc.ABC 是 Python 3.4 新增的类

定义Tombola抽象基类的子类

import random
from tombola import Tombola

class BingoCate(Tombola):
def __init__(self, items):
self._randomizer = random.SystemRandom()
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()

Tombola的虚拟子类

即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之
后,注册的类会变成抽象基类的虚拟子类,而且 issubclass 和
isinstance 等函数都能识别,但是注册的类不会从抽象基类中继承
任何方法或属性。

虚拟子类不会继承注册的抽象基类,而且任何时候都不会检
查它是否符合抽象基类的接口,即便在实例化时也不会检查。为了
避免运行时错误,虚拟子类要实现所需的全部方法。

from random import randrange
from tombola import Tombola

@Tombola.register  # 注册虚拟子类
class TomboList(list):  #
def pick(self):
if self:  # list 中继承 __bool__ 方法,列表不为空时返回True
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')

load = list.extend  # TomboList.load与list.extend一样

def loaded(self):
return bool(self)  # 委托bool函数

def inspect(self):
return tuple(sorted(self))

我们来测试一下:

# -*- coding: utf-8 -*
if __name__ == '__main__':
print(issubclass(TomboList,Tombola)) #True
t = TomboList(range(100))
print(isinstance(t,Tombola))#True

类的继承关系在一个特殊的类属性中指定——

__mro__
,即方
法解析顺序(Method Resolution Order)。这个属性的作用很简单,按顺
序列出类及其超类,Python 会按照这个顺序搜索方法。

print(TomboList.__mro__) #(<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)

Tombolist.__mro__
中没有 Tombola,因此 Tombolist 没有从Tombola 中继承任何方法。

Python使用register的方式

然现在可以把 register 当作装饰器使用了,但更常见的做法还是
把它当作函数使用,用于注册其他地方定义的类。例如,在
collections.abc 模块的源码中,是这样
把内置类型 tuple、str、range 和 memoryview 注册为 Sequence
的虚拟子类的:

Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
  • 点赞
  • 收藏
  • 分享
  • 文章举报
愤怒的可乐 博客专家 发布了151 篇原创文章 · 获赞 186 · 访问量 14万+ 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: