# 第11章 接口:从协议到抽象基类
"""
本章探讨从鸭子类型的代表特征动态协议,到使接口更明确、能雅正实现是否符合规定的抽象基类
本章将实现一个抽象基类,看看它的运作方式,但作者不建议自己编写抽象基类,因为容易过度设计
"""
# 11.1 Python文化中的接口和协议
# 协议是接口,但不能像正式接口那样施加限制,python通过抽象基类对接口作一致性的强制,
# 一个类可以只实现部分接口
# 11.2 Python喜欢序列
# Foo类没有继承abc.sequence,而且只实现了序列协议的一个方法
# 示例 11-3 实现了__getitem__就能访问元素/迭代/使用in运算符
"""
class Foo:
def __getitem__(self, pos):
return range(0,30,10)[pos]
f = Foo()
print(f[1])
for i in f:
print(i)
print(20 in f)
print(15 in f)
"""
# 如果没有__iter__和__contains__方法,python解释器会尝试调用__getitem__来迭代对象或者使用in
# 示例11-4 实现序列协议的FrenchDeck类(代码和第一章相同)
"""
import collections
from random import choice
Card = collections.namedtuple('card',['rank','suit'])
class FrenchDeck:
ranks = [str(x) for x 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]
"""
# 11.3 使用猴子补丁在运行时实现协议
# 示例11-4中的FrenchDeck无法实现洗牌
# 标准库中random.shuffle函数用法
"""
from random import shuffle
l = list(range(10))
shuffle(l) # shuffle是就地打乱序列
print(l)
deck = FrenchDeck()
"""
# shuffle(deck)
# TypeError: 'FrenchDeck' object does not support item assignment
# FrenchDeck 不支持为元素赋值
# 这是因为FrenchDeck只实现了不可变的序列协议
# 可变的还必须提供__setitem__方法
# python是动态语言,在运行时修正这个问题,这被称为猴子补丁
# 猴子补丁:在运行时修改类或者模块,而不改动源码
# 实例11-5 在运行时把FrenchDeck变成可变的,让shuffle能处理
"""
def set_card(deck,position,card):
deck._cards[position] = card
# 把函数赋给类的__setitem__方法
FrenchDeck.__setitem__ =set_card
shuffle(deck)
print(deck[:5])
"""
# 11.4 Alex Martelli的水禽
# 鸭子类型:忽略对象的真正类型,转而关注对象有没有实现所需的方法\签名和语义
# 白鹅类型(goose typing):
# 只要cls是抽象基类,即cls的元类是abc.ABCMeta,就可以使用isinstance(obj,cls)来检查obj的类型
# 继承抽象基类只需实现规定的方法,或者通过注册虚拟子类来实现
# 示例11-7 使用鸭子类型处理单个字符串或由字符串组成的可迭代对象
"""
try:# 假设是单个字符串
# 把逗号替换成空格在拆分成列表
field_names = field_names.replace(',',' ').splt()
except AttributeError:# field_names看起来不像字符串,或者返回值不能使用split拆分
pass # 假设已经是由名称组成的可迭代对象了
field_names = tuple(field_names) # 为了确保已经是可迭代对象了,也为了创建一份副本,使用所得值创建元组
"""
# 11.5定义抽象基类的子类
# 将Frenchdeck定义为collections.MutableSequence的子类
# frenchdeck2.py
# 11.6 标准库中的抽象基类
# 大多数抽象基类在collections.abc模块中
# numbers和io包中也有一些抽象基类
# 11.6.1 collections.abc模块中的抽象基类
# 集合s应该继承的三个抽象基类 Iterable Container
# Iterable:通过__iter__支持迭代
# Container:通过__contains__支持in运算符
# Sized:通过__len__支持len()函数
# 不可变集合应该继承的三个抽象基类:Sequence Mapping Set
# 可变集合应该继承的三个抽象基类:MutableSequence MutableMapping MutableSet
# MappingView:如.items() .keys() .values()返回的对象
# Callable Hashable:这两个抽象子类一般不子类化,主要作用是为isinstance提供支持
# 判断对象能不能调用或者哈希
# Iterator:是Iterable的子类
# 11.6.2 Numbers 抽象基类的数字塔
# Numbers
# Complex
# Real
# Rational
# Integral
# 这样检查一个对象是不是整数就可以使用 isinstance(obj,Numbers.Integral)
# 这样的代码能接受int\bool,或者外部库使用Numbers抽象基类注册的其他类型
# 如果一个对象可能是浮点数类型,可以使用 isinstance(obj,Numbers.Real)
# 这样的代码能接受int\bool\float\fractions.Fraction,或者外部库(如Numpy)提供的非复数类型
# 11.7 定义并使用一个抽象基类及其子类
# 这么做是为了教你阅读标准库中的抽象基类源码 tombola.py
# __mro__:方法解析顺序 (Method Resolution Order)
# 按顺序列出对象的类和超类
# 11.8 Tombola子类的测试方法
# tombola_runner.py
# python使用register的方式
# collections.abc源码中,把内置类型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
print(isinstance(Struggle(), abc.Sized)) # True
# Struggle是Sized的子类,这是因为abc.Sized实现了一个特殊的类方法__subclasshook__
# 示例 11-17 Sized源码
"""
class Sized(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __len__(self):
return 0
@classmethod
def __subclasshook__(cls,C):
# 对于C.__mro__中所列的类中,如果__dict__属性中有__len__)
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
# 否则让子类检查
return NotImplemented
"""
# 总结:
"""
本章首先介绍了非正式接口(协议)的高度动态本性
然后讲解了抽象基类的静态接口声明
最后指出了抽象基类的动态特性:虚拟子类,
以及使用__subclasshook__方法动态识别子类
"""
# frenchdeck2.py
# frenchdeck2.py
import collections
Card = collections.namedtuple('card',['rank','suit'])
class FrenchDeck2(collections.MutableSequence):
ranks = [str(x) for x 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
def __del__(self,position):
'''继承collections.MutableSequence的类必须实现__del__方法'''
del self._cards[position]
def insert(self, position, value) :
'''继承collections.MutableSequence的类必须实现insert方法'''
self._cards.insert(position,value)
"""
导入时python不会检查抽象方法的实现
在运行时,FrenchDeck2实例化才会真正检查
"""
tombola_runner.py
# 定义一个抽象基类及其子类
"""
运用场景:
在网站或者移动应用中显示随机广告,在整个广告清单轮转一遍之前不重复显示广告
假设我们在构建一个广告管理框架ADAM
它的职责之一是,支持用户提供随机挑选的无重复类
定义一个抽象基类:Tombola
它有四个方法
两个抽象方法:
.load():把元素放入容器
.pick():从容器随机拿出一个元素,返回选中的元素
两个具体方法
.loaded():如果容器中至少有一个元素,返回True
.inspect():
返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容
Tombola的抽象基类和三个具体实现:
BingoCage:
__init__
load
pick
__call__
LotteryBlower:
__init__
load
pick
loaded
inspect
Tombolist:通过registered注册的
抽象基类的一些编程规则:
抽象基类会检查子类
abc模块定义了三个装饰器:
@abc.abstractmethod
@abc.abstractproperty
@abc.abstractstaticmethod
@abc.abstractclassmethod
但是因为python3.3以后装饰器可以堆叠后三个装饰器就被废弃了
因为可以这样写
@classmethod
@@abc.abstractmethod # 注意这个在最内层
def xxx():
pass
"""
import abc
import random
class Tombola(abc.ABC): # 自己定义的抽象基类必须继承abc.ABC
@abc.abstractmethod # 抽象方法用@abc.abstractmethod装饰,而且定义体中通常只有文档字符串
def load(self,iterable):
'''从可迭代对象中添加元素'''
@abc.abstractmethod
def pick(self):
'''随机删除元素,然后将其返回
如果实例为空,这个方法应该抛出LookupError'''
def loaded(self): # 抽象基类可以包含具体方法
'''如果至少有一个元素,返回True
否则返回False
抽象基类只能依赖抽象基类定义的接口如inspect'''
return bool(self.inspect())
def inspect(self):
'''返回一个有序元组,有当前元素构成
我们不知道具体子类如何存储元素,不过为了得到inspect的结果
可以不断pick把Tombola清空,然后调用
.load把所有元素放回去'''
items = []
while 1:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))
# 11.7.2定义抽象基类的子类
# 定义BingoCage类
class BingoCage(Tombola):
def __init__(self,items):
'''假设我们将在线上游戏中使用这个,random.SystemRandom
使用os.urandom()函数实现random API
生成适合用于加密的字节序列'''
self._randomizer = random.SystemRandom()
self._items = []
self.load(items) # 委托load实现初始加载
def loaded(self,items):
self._items.extend(items)
# 没有使用random.shuffle而是使用self._randomize的shuflle方法
self._randomizer.shuffle(self._items)
def pick(self): # 重写了pick方法
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')
def __call__(self):
return self.pick()
# 定义LotteryBlower子类 覆盖了继承的inspect和loaded方法
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))
# 如果范围为空,抛出ValueError
except ValueError:
# 为了兼容Tombola抛出LookuopError
raise LookupError('pick from empty LotteryBlower')
return self._balls.pop(position)
def loaded(self):
'''覆盖loaded方法,避免调用insepect方法
直接处理self._balls而不必构建整个有序元组'''
return bool(self._balls)
def inspect(self):
'''使用一行代码覆盖inspect'''
return tuple(sorted(self._balls))
# 11.7.3 Tombola的虚拟子类
# 白鹅类型的一个基本特性:
# 即便不继承,也有办法把一个类注册为抽象基类的虚拟子类
# 注册虚拟子类的方法:
# 在抽象基类上调用register方法
# 这样注册的类会变成抽象基类的虚拟子类,而且isinstance和issubclass都能识别
# ,但是注册的类不会从抽象基类继承任何的属性和方法
# 虚拟子类在任何时候都不会检查是否符合抽象基类的接口,即便实例化时也不会检查
# 为了避免运行错误,虚拟子类要实现所需的全部方法
# 11-14 Tombola的虚拟子类 TomboList
from random import randrange
@Tombola.register # 把TomboList注册为Tombola的虚拟子类
class TomboList(list): # 继承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))
# Tombola.register(TomboList) 3.3以前的版本
if __name__ == '__main__':
# 示例11-11不符合Tombola要求的子类无法蒙混过关
class Fake(Tombola):
def pick(selfs):
return 13
print(repr(Fake))
# fake = Fake() # TypeError: Can't instantiate abstract class Fake with abstract method load
# print(repr(fake))
print(issubclass(TomboList, Tombola))
t = TomboList(range(100))
print(isinstance(t, Tombola))
print(TomboList.__mro__) # (<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)
tombola_runner.py这个测试文件没有完全实现,应该是要在类里面添加相关属性
import doctest
from tombola import Tombola,BingoCage,LotteryBlower,TomboList
TEST_FILE = 'tombola_tests.rst'
TEST_MSG = '{0:16} {1.attempted:2} tests, {1.failed:2} failed-{2}'
def main(argv):
verbose = '-v' in argv
real_subclasses = Tombola.__subclasses__()
virtual_subclasses = list(Tombola._abc_registry)
for cls in real_subclasses+virtual_subclasses:
test(cls,verbose)
def test(cls,verbose = False):
res = doctest.testfile(
TEST_FILE,
globs={'ConcreteTombola':cls},
verbose=verbose,
optionflags = doctest.REPORT_ONLY_FIRST_FAILURE
)
tag = 'FAIL' if res.failed else 'OK'
print(TEST_MSG.format(cls.__name__,res,tag))
if __name__ == '__main__':
import sys
main(sys.argv)
35岁学Python,也不知道为了啥?