流畅的python读书笔记-第11章-接口:从协议到抽象基类

抽象基类

抽象基类的常见用途:

  1. 实现接口时做为超类使用。
  2. 而后,说明抽象基类如何检查具体子类是否符合接口定义,以及如何使用注册机制声明一个类实现了某个接口,而不进行子类化操做。
  3. 如何让抽象基类自动“识别”任何符合接口的类——不进行子类化或注册。

接口在动态类型语言中是怎么运做的呢?

  1. 按照定义,受保护的属性和私有属性不在接口中:
  2. 即使“受保护的”属性也只是采用命名约定实现的(单个前导下划线)
  3. 私有属性能够轻松地访问(参见 9.7 节),缘由也是如此。 不要违背这些约定。
  4. 不要以为把公开数据属性放入对象的接口中不妥,
  5. 由于若是须要,总能实现读值方法和设值方法,把数据属性变成特性,使用 obj.attr 句法的客户代码不会受到影响。

Python喜欢序列

  1. 协议是接口,但不是正式的(只由文档和约定定义),
  2. 所以协议不能像正式接口那样施加限制(本章后面会说明抽象基类对接口一致性的强制)。
  3. 一个类可能只实现部分接口,这是容许的。

看看示例 11-3 中的 Foo 类。它没有继承 abc.Sequence,并且只实现了序列协议
的一个方法: getitem (没有实现 len 方法)app

定义 getitem 方法,只实现序列协议的一部分,这样足够访问元
素、迭代和使用 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

综上,鉴于序列协议的重要性,若是没有 itercontains 方法,Python 会调
getitem 方法,设法让迭代和 in 运算符可用。框架

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

random.shuffle 函数打乱 FrenchDeck 实例dom

为FrenchDeck 打猴子补丁,把它变成可变的,让 random.shuffle 函
数能处理ssh

def set_card(deck, position, card): ➊
... deck._cards[position] = card
>>> FrenchDeck.__setitem__ = set_card ➋
>>> shuffle(deck) ➌
>>> deck[:5]
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4',
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]

❶ 定义一个函数,它的参数为 deck、position 和 card。
❷ 把那个函数赋值给 FrenchDeck 类的 setitem 属性。
❸ 如今能够打乱 deck 了,由于 FrenchDeck 实现了可变序列协议所需的方法。函数

这里的关键是,set_card 函数要知道 deck 对象有一个名为 _cards 的属性,并且
_cards 的值必须是可变序列。
而后,咱们把 set_card 函数赋值给特殊方法__setitem__,从而把它依附到 FrenchDeck 类上。
这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。

协议是动态的

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

抽象基类使用姿式

有时,为了让抽象基类识别子类,甚至不用注册。
其实,抽象基类的本质就是几个特殊方法。测试

>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
能够看出,无需注册,abc.Sized 也能把 Struggle 识别为本身的子类,只要实现
了特殊方法 len 便可(要使用正确的句法和语义实现,前者要求没有参数,后
者要求返回一个非负整数,指明对象的长度;

做者建议

若是实现的类体现了 numbers、collections.abc 或其余框架中
抽象基类的概念,
要么继承相应的抽象基类(必要时),要么把类注册到相应的抽象
基类中。
开始开发程序时,不要使用提供注册功能的库或框架,要本身动手注册网站

一句话:
1.要么继承基类
2.要么本身把类注册到相应的抽象基类中 ,别使用自动注册ui

isinstance 检查使用姿式

然而,即使是抽象基类,也不能滥用 isinstance 检查,用得多了可能致使代码异味,即代表面向对象设计得很差。编码

在一连串 if/elif/elif 中使用 isinstance 作检查,而后根据对象的类型执行不一样的操做,一般是很差的作法;spa

此时应该使用多态,即采用必定的方式定义类,让解释器把调用分派给正确的方法,而不使用 if/elif/elif 块硬编码分派逻辑。

鸭子类型 和 类型检查

在框架以外,鸭子类型一般比类型检查更简单,也更灵活。

  1. 本书有几个示例要使用序列,把它当成列表处理。
  2. 我没有检查参数的类型是否是list,而是直接接受参数,当即使用它构建一个列表。
  3. 这样,我就能够接受任何可迭代对象;
  4. 若是参数不是可迭代对象,调用当即失败,而且提供很是清晰的错误消息。

一句话:
看起来像鸭子(如序列),直接用序列的特性方法,(若是爆错就是类型不对),若是能够就是经过

这种作法省去了,用isinstance 作检查的痛苦(有时不知道什么类型)

标准库中的抽象基类急顺序 page 375 376

定义并使用一个抽象基类

重点来了

想象一下这个场景:

你要在网站或移动应用中显示随机广告,可是在整个广告清单轮转一遍以前,不重复显示
广告。

假设咱们在构建一个广告管理框架,名为 ADAM。

它的职责之一是,支持用户提供随机挑选的无重复类。

为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么意思,咱们将定义一个抽象基类。

我将使用现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的集合中挑选物品的机器,选出的物品没有重复,直到选完为止

Tombola 抽象基类有四个方法,其中两个是抽象方法。

  • .load(...):把元素放入容器。
  • .pick():从容器中随机拿出一个元素,返回选中的元素。

另外两个是具体方法。

  • .loaded():若是容器中至少有一个元素,返回 True。
  • .inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容 (内部的顺序不保留)。

clipboard.png

代码:

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,而后将其返回。
        若是实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """若是至少有一个元素,返回`True`,不然返回`False`。"""
        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))

本身定义的抽象基类要继承 abc.ABC。
根据文档字符串,若是没有元素可选,应该抛出 LookupError。
❹ 抽象基类能够包含具体方法。
❻ 咱们不知道具体子类如何存储元素,不过为了获得 inspect 的结果,咱们能够不断调
用 .pick() 方法,把 Tombola 清空……
❼ ……而后再使用 .load(...) 把全部元素放回去。

其实,抽象方法能够有实现代码。即使实现了,子类也必须覆盖抽象方法,但
是在子类中可使用 super() 函数调用抽象方法,为它添加功能,而不是从头开始
实现。

定义Tombola抽象基类的子类

BingoCage 类是在示例 5-8 的基础上修改的,使用了更好的随机发生
器。
BingoCage 实现了所需的抽象方法 load 和 pick,从 Tombola 中继承了 loaded 方
法,覆盖了 inspect 方法,还增长了 call 方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,而后将其返回。
        若是实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """若是至少有一个元素,返回`True`,不然返回`False`。"""
        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))


import random

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()

❹ 没有使用 random.shuffle() 函数,而是使用 SystemRandom 实例的 .shuffle() 方法。
这里想表达的观点是:咱们能够偷懒,直接从抽象基类中继承不是那么理想的具体方法。

从 Tombola 中继承的方法没有BingoCage 本身定义的那么快,不过只要 Tombola 的子类正确实现 pick 和 load 方法,就能提供正确的结果。

LotteryBlower 打乱“数字球”后没有取出最后一个,而是取出一个随机位置上的

球。

❷ 若是范围为空,random.randrange(...) 函数抛出 ValueError,为了兼容
Tombola,咱们捕获它,抛出 LookupError。

❹ 覆盖 loaded 方法,避免调用 inspect 方法(示例 11-9 中的 Tombola.loaded 方法是
这么作的)。咱们能够直接处理 self._balls 而没必要构建整个有序元组,从而提高速
度。

有个习惯作法值得指出:

  • init 方法中,self._balls 保存的是list(iterable),而不是 iterable 的引用(即没有直接把iterable 赋值给self._balls)。
  • 前面说过, 这样作使得 LotteryBlower 更灵活,由于 iterable 参数能够是任何可迭代的类型。
  • 把元素存入列表中还确保能取出元素。
  • 就算 iterable 参数始终传入列表,list(iterable)
    会建立参数的副本,这依然是好的作法,由于咱们要从中删除元素,而客户可能不但愿本身提供的列表被修改。

Tombola的虚拟子类

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

3.虚拟子类不会继承注册的抽象基类,为了不运行时错误,虚拟子类要实现所需的所有方法。

import abc


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """从可迭代对象中添加元素。"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,而后将其返回。
        若是实例为空,这个方法应该抛出`LookupError`。
        """

    def loaded(self):
        """若是至少有一个元素,返回`True`,不然返回`False`。"""
        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))


import random


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()


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')

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

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


from random import randrange


@Tombola.register
class TomboList(list):
    def pick(self):
        if self:
            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))


# Tombola.register(TomboList)

把 Tombolist 注册为 Tombola 的虚拟子类。
❸ Tombolist 从 list 中继承 bool 方法,列表不为空时返回 True。
❹ pick 调用继承自 list 的 self.pop 方法,传入一个随机的元素索引。

注册以后,可使用 issubclass 和 isinstance 函数判断 TomboList 是否是Tombola的子类:

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

Tombola子类的测试方法

__subclasses__()
  这个方法返回类的直接子类列表,不含虚拟子类。
_abc_registry
  只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子
类的弱引用。

Python使用register的方式

Tombola.register 看成类装饰器使用。在 Python 3.3 以前的版本中不能这
样使用 register

虽然如今能够把 register 看成装饰器使用了,但更常见的作法仍是把它看成函数使用,
用于注册其余地方定义的类。

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

>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
  1. issubclass 函数确认(isinstance 函数也会得出相同的结论)
  2. Struggle 是abc.Sized 的子类,
  3. 这是由于 abc.Sized 实现了一个特殊的类方法,名为__subclasshook__。
Sized 类的源码:
class Sized(metaclass=ABCMeta):
 __slots__ = ()
 @abstractmethod
 def __len__(self):
 return 0
 @classmethod
 def __subclasshook__(cls, C):
 if cls is Sized:
 if any("__len__" in B.__dict__ for B in C.__mro__): # ➊
 return True # ➋
 return NotImplemented # ➌

对 C.__mro__ (即 C 及其超类)中所列的类来讲,若是类的 dict 属性中有名为
len 的属性……

小结

1.抽象基类的使用姿式
2.定义一个随机抽象基类
3.虚拟子类 只是注册就行,(没继承),必须实现全部方法
4.Tombola 这个自定义的抽象基类多写几回

其余:

非正式接口(称为协议)的高度动态本性,
以及使用 subclasshook 方法动态识别子类。

咱们发现 Python 对序列协议的支持十分深刻。
若是一个类实现了__getitem__ 方法,此外什么也没作,那么 Python 会设法迭代它,并且 in 运算符也随之可使用。

显式继承抽象基类的优缺点。
继承abc.MutableSequence 后,必须实现 insert 和 delitem 方法,而咱们并不须要这两个方法。

相关文章
相关标签/搜索