《流畅的Python》读书笔记13(第十一章:接口:从协议到抽象基类)

11.1 Python文化中的接口和协议

协议被定义为非正式接口,是让Python这种动态类型语言实现多态的方式

11.2 Python喜欢序列

image.png 图 11-1展示了定义为抽象基类的Sequence正式接口

示例11-3 定义__getitme__方法,只实现了序列协议的一部分,这样足够访问元素、迭代和使用in运算符了。

>>> class Foo:

...     def __getitem__(self, pos):

...         return range(0, 30, 10)[pos]

... 

>>> f = Foo()

>>> f[1]

10

>>> for i in f:print(i)

... 

0

10

20

>>> 20 in f

True

>>> 15 in f

False
复制代码

鉴于序列协议的重要性,如果没有__iter__和__contains__方法,Python会调用__getitem__方法,设法让迭代和in运算符可用。

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

标注库中的random.shuffle函数用法如下:

>>> from random import shuffle

>>> l = list(range(10))

>>> l

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> shuffle(l)

>>> l

[4, 6, 8, 0, 3, 5, 1, 2, 7, 9]
复制代码

示例11-5 random.shuffle函数不能打乱FrenchDeck实例

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

复制代码

这个报错的原因是,shuffle函数要调换collection中元素的位置,而FrenchDeck只实现了不可变序列的协议。可变的序列还必须提供__setitem__方法。

示例11-6 为FrenchDeck打猴子补丁,把它变成可变的

def set_card(deck, position, card):
    deck._cards[position] = card
    
FrenchDeck.__setitem__ = set_card
shuffle(deck)
deck[:5]
[Card(rank='10', suit='diamonds'), Card(rank='8', suit='clubs'), Card(rank='3', suit='spades'), Card(rank='Q', suit='diamonds'), Card(rank='6', suit='diamonds')]
复制代码

猴子补丁:在运行时修改类或模块,而不改动源码。

11.4 Alex Martelli的水禽

11.5 定义抽象基类的子类

示例11-8 FrenchDeck2,collections.MutableSequence的子类

import collections

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


class FrenchDeck2(collections.MutableSequence):
    ranks = [str(n) for n in (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

    def __delitem__(self, position):
        del self._cards[position]

    def insert(self, index: int, value) -> None:
        self._cards[index] = value
复制代码

insert这个抽象方法是必须实现的,导入时,Python不会检查抽象方法的实现,在运行时实例化FrenchDeck2类时才会真正检查。

from frenchdeck2 import FrenchDeck2
deck = FrenchDeck2()
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class FrenchDeck2 with abstract methods insert
复制代码

image.png 图11-2 MutableSequence抽象基类

11.6 标准库中的抽象基类

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

image.png

11.6.2 抽象基类的数字塔

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

  • Number
  • Complex
  • Real
  • Rational
  • Integral

如果想检查一个数是不是整数,可以使用isinstance(x, numbers.Ingegral),这样的代码就能接受int、bool。

如果一个值可能是浮点数类型,可以使用isinstance(x, numbers.Real)检查。这样的代码能接受bool、int、float、fractions.Fraction。

11.7 定义并使用一个抽象基类

image.png

示例11-9 tombola.py: Tombola是抽象基类,有两个抽象方法和两个具体方法

import abc

class Tombola(abc.ABC):

    @abc.abstractmethod
    def load(self, iterable):
        """Add items from an iterable."""

    @abc.abstractmethod
    def pick(self):
        """Remove item at random, returning it.
        This method should raise `LookupError` when the instance is empty.
        """
    
    def loaded(self):
        """Return `True` if there's at least 1 item, `False` otherwise."""
        return bool(self.inspect)

    
    def inspect(self):
        """Return a sorted tuple with the items currently inside."""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))
复制代码

示例11-11 不符合Tombola要求的子类无法蒙混过关

from tombola import Tombola
class Fake(Tombola):
    def pick(self):
        return 13
    
Fake
<class '__main__.Fake'>
f = Fake()
Traceback (most recent call last):
  ...
TypeError: Can't instantiate abstract class Fake with abstract methods load

复制代码

尝试实例化Fake时抛出了TypeError。Python热舞Fake是抽象类,因为它没有实现load方法。

11.7.1 抽象基类语法详解

11.7.2 定义Tombola抽象基类的子类

示例11-12 bingo.py:BingoCage是Tombola的具体子类

import random

from tombola import Tombola


class BingoCage(Tombola):

    def __init__(self, items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    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()
复制代码

示例11-13 lotto.py:LotteryBlower是Tombola的具体子类,覆盖了继承的inspect和loaded方法

import random

from tombola import Tombola


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:
            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))
复制代码

11.7.3 Tombola的虚拟子类

即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我保证注册的类忠实地实现了抽象基类定义的接口,而Python会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会被我们捕获。

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

image.png

示例11-14 tombolist.py:TomboList是Tombola的虚拟子类

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

    def loaded(self):
        return bool(self)

    def inspect(self):
        return tuple(sorted(self))
复制代码

>>> from tombola import Tombola 
>>> from tombolist import TomboList 
>>> issubclass(TomboList, Tombola) 
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True
复制代码

类的继承关系在一个特殊的类属性中指定————__mro__,即方法解析顺序(Method Resolution Order).这个属性的作用很简单,按顺序列出类及超类,Python会按照这个顺序搜索方法。查看TomboList类的__mro__属性,会发现它只列出了“真实的”超类。

>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)
复制代码

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

11.8 Tombola子类的测试方法

__subclasses__()

这个方法返回类的直接子类列表,不包含虚拟子类。

__abc_register

只有抽象基类有这个数据属性,其值是一个WeakSet对象,即抽象类注册的虚拟子类的弱引用。

11.9 Python使用register的方式

虽然现在可以把register当作装饰器使用了,但更常见的做法还是把它当作函数使用,用于注册其他地方定义的类。

把内置类型tuple、str、range和memoryview注册为Sequence的虚拟子类:

Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)
复制代码

11.10 鹅的行为有可能像鸭子

即便不注册,抽象基类也能把一个类识别为虚拟子类。

>>> class Struggle:

...     def __len__(self): return 23

... 

>>> from collections import abc

>>> isinstance(Struggle(), abc.Sized)

True

>>> issubclass(Struggle, abc.Sized)

True
复制代码

Struggle是abc.Sized的子类,这是因为abc.Sized实现了一个特殊的类方法,名为__subclasshook__.

猜你喜欢

转载自juejin.im/post/7035444959180226591