接口和协议
Python没有Interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性,包括特殊方法(如__getitem__或__add__)。按照约定,受保护的属性和私有属性不在接口中(即使受保护属性也只是采用命名约定实现的;私有属性也可以轻松的访问)不要违背这些约定。
接口实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定角色。python文档中的"文件类对象"和"可迭代对象"就是这个意思,这种说法指的不是特定的类。一个类可能会实现多个借口,从而让实例扮演多个角色。
协议:协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制。对象实现了协议某些部分,那个对象就叫做xxx类对象。其实对于Python,"X类对象","X协议","X接口"是同一个意思。
序列协议
序列协议是Python最基础的协议之一,即使对象只实现了那个协议最基本的部分,解释器也会负责任地处理。
抽象基类Sequence正式接口:
Foo类定义了__getitem__方法,只实现了序列协议的一部分,却能够访问,迭代和使用in运算符:
classFoo:def __getitem__(self, item):return range(0, 30, 10)[item]if __name__ == '__main__':
f=Foo()print(f[1])for i inf:print(i)print(20 inf)print(40 inf)10010
20True
False
虽然没有__iter__方法,但Foo实例是可迭代对象,因为发现由__getitem__方法时,python会调用它,传入从0开始的整数索引,尝试迭代对象。同样,没有实现__contains__,python会迭代Foo实例,因此也能使用in运算符查看有无指定元素。
运行时实现协议
为下面这份扑克增加洗牌功能:
importcollections
Card= collections.namedtuple('Card', ['rank', 'suit'])classFrenchDeck:
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 inself.suitsfor rank inself.ranks]def __len__(self):returnlen(self._cards)def __getitem__(self, item):return self._cards[item]
一个可选的方法是实现洗牌方法shuffle。但是FrenchDeck实例是一个类序列对象,那么使用random就可以了,用法如下:
from random importshuffle
l= list(range(10))
shuffle(l)print(l)
[9, 5, 6, 4, 7, 2, 0, 1, 8, 3]
尝试:
from random importshuffle
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] =cardfrom random importshuffle
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类上,这种技术叫做猴子补丁:运行时修改类或模块,而不改动源码。
抽象基类
ABC,Abstract Base Class(抽象基类),主要定义了基本类和最基本的抽象方法,可以为子类定义共有的API,不需要具体实现。相当于是Java中的接口或者是抽象类。
抽象基类可以不实现具体的方法(当然也可以实现,只不过子类如果想调用抽象基类中定义的方法需要使用super())而是将其留给派生类实现。
抽象基类提供了逻辑和实现解耦的能力,即在不同的模块中通过抽象基类来调用,可以用最精简的方式展示出代码之间的逻辑关系,让模块之间的依赖清晰简单。同时,一个抽象类可以有多个实现,让系统的运转更加灵活。而针对抽象类的编程,让每个人可以关注当前抽象类,只关注其方法和描述,而不需要考虑过多的其他逻辑,这对协同开发有很大意义。极简版的抽象类实现,也让代码可读性更高。
抽象基类的使用
1:直接继承
直接继承抽象基类的子类就没有这么灵活,抽象基类中可以声明”抽象方法“和“抽象属性”,只有完全覆盖(实现)了抽象基类中的“抽象”内容后,才能被实例化,而虚拟子类则不受此影响。
2:虚拟子类
将其他的类”注册“到抽象基类下当虚拟子类(调用register方法),虚拟子类的好处是你实现的第三方子类不需要直接继承自基类,可以实现抽象基类中的部分API接口,也可以根本不实现,但是issubclass(), issubinstance()进行判断时仍然返回真值。
抽象基类的本质就是几个特殊方法。例如:
classStruggle:def __len__(self):return 20
from collections importabcprint(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():返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部的顺序不保留)。 //普通方法
importabcclass Tombola(abc.ABC): #自己定义的抽象基类要继承自abc.ABC
@abc.abstractmethod #抽象方法使用@abc.abstractmethod标记,而且定义体中通常只有文档字符串
defload(self, iterable):"""从可迭代对象中添加元素"""@abc.abstractmethoddefpick(self):"""随机删除元素,然后将其返回
如果实例为空,应该抛出LookupError"""
defloaded(self):"""如果有一个元素返回True,否则返回False"""
returnbool(self.inspect())def inspect(self): #抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中其他具体方法,抽象方法和特性)
"""返回一个有序元组,由当前元素构成"""items=[]whileTrue:try:
items.append(self.pick())exceptLookupError:breakself.load(items)return tuple(sorted(items))
子类不完全覆盖抽象基类的抽象方法看看是否可行:
class Fake(Tombola): #声明为Tombola的子类
defpick(self):return 13
print(Fake)
f=Fake()#结果
#创建Fake类没有错误
TypeError: Can't instantiate abstract class Fake with abstract methods load #实例化时抛出错误,没有实现load方法,认为Fake是抽象类
使用这个抽象基类
用法一:直接继承
classBingoCage(Tombola):def __init__(self, items):
self._randomizer= random.SystemRandom() #操作系统生成随机数
self._items =[]
self.load(items)#委托load实现初始加载
defload(self, items):
self._items.extend(items)
self._randomizer.shuffle(self._items)defpick(self):try:returnself._items.pop()exceptIndexError:raise LookupError('pick from empty BingoCage')def __call__(self, *args, **kwargs):
self.pick()
使用了更好的随机数生成器,继承了loaded方法,覆盖了inspect,实现了__call__方法。
另一种实现:
classBingoCage(Tombola):def __init__(self, iterable):
self._balls= list(iterable) #创建列表
defload(self, iterable):
self._balls.extend(iterable)defpick(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方法
returnbool(self._balls)def inspect(self): #覆盖inspect方法
return tuple(sorted(self._balls))
用法二:虚拟子类
即使不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,需要保证注册的类忠实地实现了抽象基类定义的接口,python不做检查,但如果没有实现,可能会产生运行时异常。
注册虚拟子类的方式是在抽象基类上调用register方法(register可以当作类装饰器使用)。这么做之后,注册的类会变为抽象基类的虚拟子类,而且isinstance和issubclass都能识别,但注册的类不会从抽象基类中继承任何方法或属性。
@Tombola.register #把TomboList注册为Tombola的虚拟子类
class TomboList(list): #扩展list
defpick(self):if self: #从list中继承bool方法,不为空时返回True
position = random.randrange(len(self)) #获取一个随机元素索引
returnself.pop(position)else:raise LookupError('pop from empty TomboList')
load= list.extend #load方法和list.extend方法一样
defloaded(self):returnbool(self)definspect(self):return tuple(sorted(TomboList))
判断是否为Tombola的子类:
print(issubclass(TomboList, Tombola))
t= TomboList(range(100))print(isinstance(t, Tombola))#结果
True
True
查看方法解析顺序:
print(TomboList.__mro__)
(, , )
发现它只列出"真实的"超类。
以上来自《流畅的python》