企业如何通过vue小程序开发满足高效运营与合规性需求
658
2022-11-17
《流畅的Python》读书笔记——接口:从协议到抽象基类
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]: 10In [4]: for i in f: print(i) 01020In [5]: 20 in f Out[5]: TrueIn [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 collectionsCard = 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 "
报错消息说该对象不支持元素赋值。
因为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 collectionsCard = 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
为了充分使用抽象基类,我们要知道有哪些抽象基类可用。接下来介绍 集合抽象基类。
标准库中的抽象基类
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 类:
NumberComplex(复数)Real(实数)RationalIntegral(整数)
如果想检查一个数是不是整数,可以使用 isinstance(x,numbers.Integral) 如果一个值可能是浮点数类型,可以使用 isinstance(x,numbers.Real)
定义并使用一个抽象基类
为了证明有必要定义抽象基类,我们要在框架中找到使用它的场景。想 象一下这个场景:你要在网站或移动应用中显示随机广告,但是在整个 广告清单轮转一遍之前,不重复显示广告。假设我们在构建一个广告管 理框架,名为 ADAM。它的职责之一是,支持用户提供随机挑选的无重 复类。 为了让 ADAM 的用户明确理解“随机挑选的无重复”组件是什么 意思,我们将定义一个抽象基类。
我们把这个抽象基类命名为 Tombola。
抽象基类:
import abcclass 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
抽象基类句法详解
abc.ABC 是 Python 3.4 新增的类
定义Tombola抽象基类的子类
import randomfrom tombola import Tombolaclass 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 randrangefrom 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__) #(
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)
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~