Fluent_Python_Part4面向对象,11-iface-abc,协议(接口),抽象基类

第四部分第11章,接口:从协议到抽象基类(重点讲抽象基类)

  1. 接口就是实现特定角色的方法集合。
  2. 严格来说,协议是非正式的接口(只由文档约束),正式接口会施加限制(抽象基类对接口一致性的强制)。
  3. 在Python中,“X类对象”、“X协议”、“X接口”都是一个意思。如“文件类对象”、“可迭代对象”,指的不是特定的类。
  4. 一个类可能会实现多个接口,从而让实例扮演多个角色。
  5. Python语言没有interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__或__add__。受保护的属性和私有属性不在接口中:即便是“受保护的”属性也只是采用命名约定实现的(单个前导下划线,可以轻松访问)。不要违背这些约定。

1. Python喜欢序列

例子1. 只实现序列协议的一部分,即只定义__getitem__,但这样足够访问元素、迭代和使用in运算符。
鉴于序列协议很重要,如果没有__iter__和__contains__方法,Python会调用__getitem__方法,设法让迭代和in运算符可用。

class Foo:
    def __getitem__(self, pos):
        return range(0, 30, 10)[pos]

f = Foo()
# 1. f[]调用__getitem__
print(f[1])
# 2. 没有__iter__方法,转而调用__getitem__方法,传入从0开始的整数索引,尝试迭代对象(这是一种后备机制)
for i in f:
    print(i)
# 3. 没有__contains__方法,转而调用__getitem__方法
print(20 in f)

Python中的迭代是鸭子类型的一种极端形式:为了迭代对象,解释器会尝试调用两个不同的方法。

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

2.1 如果遵守既定协议,很有可能增加利用现有的标准库的可能性,得益于鸭子类型。例如FrenchDeck实例的行为像序列,就可以用random.shuffle就地地打乱序列。
2.2 猴子补丁:在运行时修改类或模块,而不改动源码。
2.3 例如在FrenchDeck使用猴子补丁之前,虽然部分行为表现得像序列,但仍然不能使用random.shuffle(错误提示为'FrenchDeck' object does not support item assignment)。原因是shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列。可变的序列还必须提供__setitem__方法。

2.4 在运行时修正FrenchDeck为可变序列,让random.shuffle能处理

def set_card(deck, position, card):
    deck._cards[position] = card

#猴子补丁,把自定义的函数赋值给__setitem__,把它变成可变的。
FrenchDeck.__setitem__ = set_card

2.5 猴子补丁缺点:打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分。(例如这里的关键是set_card函数要知道deck对象有一个名为_cards属性,而且_cards的值必须是可变序列)

2.6 还要注意的是:

3. 直接使用抽象基类,而不只将其当作文档。

3.1 Python的抽象基类有一个重要的实用优势:可以实用register类方法在终端用户的代码中把某个类“声明“为一个抽象基类的”虚拟“子类。做register时,我们保证注册的类忠实地实现了抽象基类定义的接口,而Python会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会把我们捕获。(注册的类不会从抽象基类中继承任何方法或属性)
3.2 有时,为了让抽象基类识别子类,甚至不用注册。无需注册,abc.Sized也能把Struggle识别为自己的子类,只要实现了特殊方法__len__即可。

class Struggle:
        def __len__(self):
            return 23

from collections import abc
#True
print(isinstance(Struggle(), abc.Sized))

3.3 检查是不是”序列“,那就这样做:

isinstance(the_arg, collections.abc.Sequence)

3.4 最好避免自定义抽象基类。如果自己想创建新的抽象基类,先试着通过常规的鸭子类型来解决问题。

3.5 colletions.namedtuple处理field_names参数的原理
field_names的值可以是单个字符串,以空格或者逗号分隔标识符,也可以是一个标识符序列。
例子3.5.1 用鸭子类型处理单个字符串或由字符串组成的可迭代对象

#1. 假设是单个字符串风格(EAFP风格)
try:
#2. 把逗号替换成空格,然后拆分成名称列表。
    field_names = 
    field_names.replace(',', ' ').split()
#3. 如果field_names看起来不像是字符串
except AttributeError:
#4. 假设已经是由名称组成的可迭代对象
    pass
#5. 为了确保的确是可迭代对象,也为了保存一份副本,使用所得值创建一个元组。
field_names = tuple(field_names)

4. !继承抽象基类

例子1. 继承抽象基类,抽象基类会对子类作出限制。

导入时(加载并编译.py模块时),Python不会检查抽象方法的实现。在运行时实例化子类才会真正检查。

例子2. 假如我有一个FrenchDeck2的类继承MutableSequence抽象基类。

例子3. 我们可以覆盖子类中的从抽象基类中继承的方法,以更高效的方式重新实现。
例如,__cotains__方法会全面扫描序列。如果你定义的序列按顺序保存元素,那就可以重新定义__cotains__方法,使用bisect函数做二分查找,从而提升搜索速度。

5. !标准库中的抽象基类

collections.abc中的抽象基类最常用。numbers和io包中也有一些抽象基类。

5.1 collections.abc模块中的抽象基类

  1. 官方文档:https://docs.python.org/3/library/collections.abc.html
  2. 简要的UML类图(没有属性名称)



5.2 numbers的抽象基类

  1. https://docs.python.org/3/library/numbers.html
  2. numbers包定义的是”数字塔“(各个抽象基类的层次结构是线性的),其中Number是位于最顶端的超类,依次往下,最底端的是Integral类。
    Number、Complex、Real、Rational、Integral.
  3. 因此,如果想检查一个数是不是整数,可以使用isinstance(x, numbers.Integral),

6. 定义并使用一个抽象基类

  1. 最好不要自定义抽象基类
  2. 此处自定义抽象基类的目的是学会怎么阅读标准库和其他包中的抽象基类源码。
  3. 中文电子书P491

7. Python使用register

  1. 当装饰器用:@Sequence.register
  2. 当作函数用:Sequence.register(tuple)

8. 鹅(本章提到了goose typing)的行为有可能像鸭子

  1. 即便不注册,抽象基类也能把一个类别识别为虚拟子类。
class Struggle:
    def __len__(self):
        return 23

from collections import abc
isinstance(Struggle(), abc.Sized)
issubclass(Struggle, abc.Sized)

原理:


posted @ 2018-04-18 23:39  Rocin  阅读(776)  评论(0编辑  收藏  举报