看看示例 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
综上,鉴于序列协议的重要性,若是没有 iter 和 contains 方法,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 类上。
这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。
有时,为了让抽象基类识别子类,甚至不用注册。
其实,抽象基类的本质就是几个特殊方法。测试
>>> 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 检查,用得多了可能致使代码异味,即代表面向对象设计得很差。编码
在一连串 if/elif/elif 中使用 isinstance 作检查,而后根据对象的类型执行不一样的操做,一般是很差的作法;spa
此时应该使用多态,即采用必定的方式定义类,让解释器把调用分派给正确的方法,而不使用 if/elif/elif 块硬编码分派逻辑。
在框架以外,鸭子类型一般比类型检查更简单,也更灵活。
一句话:
看起来像鸭子(如序列),直接用序列的特性方法,(若是爆错就是类型不对),若是能够就是经过
这种作法省去了,用isinstance 作检查的痛苦(有时不知道什么类型)
重点来了
你要在网站或移动应用中显示随机广告,可是在整个广告清单轮转一遍以前,不重复显示
广告。
假设咱们在构建一个广告管理框架,名为 ADAM。
它的职责之一是,支持用户提供随机挑选的无重复类。
为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么意思,咱们将定义一个抽象基类。
我将使用现实世界中的物品命名这个抽象基类:宾果机和彩票机是随机从有限的集合中挑选物品的机器,选出的物品没有重复,直到选完为止
另外两个是具体方法。
代码:
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() 函数调用抽象方法,为它添加功能,而不是从头开始
实现。
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 方法,就能提供正确的结果。
球。
❷ 若是范围为空,random.randrange(...) 函数抛出 ValueError,为了兼容
Tombola,咱们捕获它,抛出 LookupError。
❹ 覆盖 loaded 方法,避免调用 inspect 方法(示例 11-9 中的 Tombola.loaded 方法是
这么作的)。咱们能够直接处理 self._balls 而没必要构建整个有序元组,从而提高速
度。
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
__subclasses__()
这个方法返回类的直接子类列表,不含虚拟子类。
_abc_registry
只有抽象基类有这个数据属性,其值是一个 WeakSet 对象,即抽象类注册的虚拟子
类的弱引用。
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
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 方法,而咱们并不须要这两个方法。