接口和协议
Python没有Interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性,包括特殊方法(如__getitem__或__add__)。按照约定,受保护的属性和私有属性不在接口中(即使受保护属性也只是采用命名约定实现的;私有属性也可以轻松的访问)不要违背这些约定。
接口实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定角色。python文档中的"文件类对象"和"可迭代对象"就是这个意思,这种说法指的不是特定的类。一个类可能会实现多个借口,从而让实例扮演多个角色。
协议:协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。对象实现了协议某些部分,那个对象就叫做xxx类对象。其实对于Python,"X类对象","X协议","X接口"是同一个意思。
序列协议
序列协议是Python最基础的协议之一,即使对象只实现了那个协议最基本的部分,解释器也会负责任地处理。
抽象基类Sequence正式接口:
Foo类定义了__getitem__方法,只实现了序列协议的一部分,却能够访问,迭代和使用in运算符:
class Foo: def __getitem__(self, item): return range(0, 30, 10)[item] if __name__ == '__main__': f = Foo() print(f[1]) for i in f: print(i) print(20 in f) print(40 in f) 10 0 10 20 True False
虽然没有__iter__方法,但Foo实例是可迭代对象,因为发现由__getitem__方法时,python会调用它,传入从0开始的整数索引,尝试迭代对象。同样,没有实现__contains__,python会迭代Foo实例,因此也能使用in运算符查看有无指定元素。
运行时实现协议
为下面这份扑克增加洗牌功能:
import collections Card = 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, item): return self._cards[item]
一个可选的方法是实现洗牌方法shuffle。但是FrenchDeck实例是一个类序列对象,那么使用random就可以了,用法如下:
from random import shuffle l = list(range(10)) shuffle(l) print(l) [9, 5, 6, 4, 7, 2, 0, 1, 8, 3]
尝试:
from random import shuffle deck = FrenchDeck() shuffle(deck) TypeError: 'FrenchDeck' object does not support item assignment #对象不支持为元素赋值
FrenchDeck只实现了不可变的序列协议。可变的序列协议还需要实现__setitem__方法。
python是动态语言,可以在运行时修正这个问题:
deck = FrenchDeck() def set_card(deck ,position, card): #定义一个函数 deck._cards[position] = card from random import shuffle FrenchDeck.__setitem__ = set_card #把这个函数赋给FrenchDeck的__setitem__属性 shuffle(deck) print(deck[:5]) #乱序的扑克牌 [Card(rank='4', suit='diamonds'), Card(rank='8', suit='diamonds'), Card(rank='3', suit='spades'), Card(rank='9', suit='clubs'), Card(rank='A', suit='clubs')]
set_card函数要知道deck对象有一个名为_cards的属性,而且_cards的值必须是可变序列。然后把set_card函数赋值给特殊方法__setitem__,从而把它依附到FrenchDeck类上,这种技术叫做猴子补丁:运行时修改类或模块,而不改动源码。
抽象基类
抽象基类的使用
抽象基类的本质就是几个特殊方法。例如:
class Struggle: def __len__(self): return 20 from collections import abc print(isinstance(Struggle(), abc.Sized)) # True
可以看出,无需注册,abc.sized也能把Struggle识别为自己的子类,只要实现了特殊方法__len__即可。
如果类体现了numbers,collections.abc或其他框架中抽象基类的概念,要么继承相应的抽象基类,要么把类注册到相应的抽象基类中。开发程序时,不要使用提供注册功能的库或框架,要自己动手注册。
标准库中的抽象基类
标准库中提供了抽象基类,大多数抽象基类在collections.abc模块中定义,numbers和io包中也有一些。
collections.abc模块中的抽象基类
16个抽象基类
Iterable、Container 和 Sized
各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable 通过 __iter__ 方法支持迭代,Container 通过__contains__ 方法支持 in 运算符,Sized 通过 __len__ 方法支持len() 函数。
Sequence、Mapping 和 Set
这三个是主要的不可变集合类型,而且各自都有可变的子类
MappingView
在 Python 3 中,映射方法 .items()、.keys() 和 .values() 返回的对象分别是 ItemsView、KeysView 和 ValuesView 的实例。前两个类还从 Set 类继承了丰富的接口。
Callable 和 Hashable
这两个抽象基类与集合没有太大的关系,只不过因为collections.abc 是标准库中定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到 collections.abc 模块中。我从未见过Callable 或 Hashable 的子类。这两个抽象基类的主要作用是为内置函数 isinstance 提供支持,以一种安全的方式判断对象能不能调用或散列。
Iterator
注意它是 Iterable 的子类。
numbers包中的抽象基类
下面各个抽象类结构是线性的
Number
Complex
Real
Rational
Intergral
如果想检查一个数是不是整数,可以使用 isinstance(x,numbers.Integral),这样代码就能接受 int、bool(int 的子类),或者外部库使用 numbers 抽象基类注册的其他类型。为了满足检查的需要,你或者你的 API 的用户始终可以把兼容的类型注册为numbers.Integral 的虚拟子类。
与之类似,如果一个值可能是浮点数类型,可以使用 isinstance(x,numbers.Real) 检查。这样代码就能接受bool、int、float、fractions.Fraction,或者外部库(如NumPy,它做了相应的注册)提供的非复数类型。
定义一个抽象基类
我们把这个抽象基类命名为 Tombola,这是宾果机和打乱数字的滚动容器的意大利名。有四个方法
.load(...):把元素放入容器。 //抽象方法
.pick():从容器中随机拿出一个元素,返回选中的元素 //抽象方法
.loaded():如果容器中至少有一个元素,返回 True。 //普通方法
.inspect():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。 //普通方法
import abc 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""" 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))
子类不完全覆盖抽象基类的抽象方法看看是否可行:
class Fake(Tombola): #声明为Tombola的子类 def pick(self): return 13 print(Fake) f = Fake() #结果 <class '__main__.Fake'> #创建Fake类没有错误 TypeError: Can't instantiate abstract class Fake with abstract methods load #实例化时抛出错误,没有实现load方法,认为Fake是抽象类
使用这个抽象基类
用法一:直接继承
class BingoCage(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, *args, **kwargs): self.pick()
使用了更好的随机数生成器,继承了loaded方法,覆盖了inspect,实现了__call__方法。
另一种实现:
class BingoCage(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): #覆盖loaded方法 return bool(self._balls) def inspect(self): #覆盖inspect方法 return tuple(sorted(self._balls))
用法二:虚拟子类
即使不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,需要保证注册的类忠实地实现了抽象基类定义的接口,python不做检查,但如果没有实现,可能会产生运行时异常。
注册虚拟子类的方式是在抽象基类上调用register方法(register可以当作类装饰器使用)。这么做之后,注册的类会变为抽象基类的虚拟子类,而且isinstance和issubclass都能识别,但注册的类不会从抽象基类中继承任何方法或属性。
@Tombola.register #把TomboList注册为Tombola的虚拟子类 class TomboList(list): #扩展list def pick(self): if self: #从list中继承bool方法,不为空时返回True position = random.randrange(len(self)) #获取一个随机元素索引 return self.pop(position) else: raise LookupError('pop from empty TomboList') load = list.extend #load方法和list.extend方法一样 def loaded(self): return bool(self) def inspect(self): return tuple(sorted(TomboList))
判断是否为Tombola的子类:
print(issubclass(TomboList, Tombola)) t = TomboList(range(100)) print(isinstance(t, Tombola)) #结果 True True
查看方法解析顺序:
print(TomboList.__mro__)
(<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)
发现它只列出"真实的"超类。
以上来自《流畅的python》