1.Python文化中的接口和协议
在Python中,我们把协议定义为非正式的接口,是让Python这种动态类型语言实现多态的方式。那么,接口在动态类型语言中是怎么运作的呢?
首先,基本的事实是,Python语言没有interface关键字,而且,除了抽象基类,每个类都有接口:类实现和继承的公开属性,包括特殊方法,如:_getitem_和_add_。按照定义,受保护的属性和私有属性不在接口中:即便是受保护的属性,也只能采用命名约定实现,私有属性可以轻松访问。
其次,不要觉得把公开数据属性放入对象的接口中不妥,因为如果需要总能实现读值方法和设值方法,把数据属性变成特性,使用obj.attr句法的客户代码不受到影响。
class Vector2d:
typecode = 'd'
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def __iter__(self):
return (i for i in (self.x, self.y))
class Vector2d:
typecode = 'd'
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def __iter__(self):
return (i for i in (self.x, self.y))
上面的代码中,第一段中x、y是公开的数据,而第二段x、y是使用特性实现的,将其变成了只读特性。这段代码是我们之前实现过的。
其实关于接口,这里还有一个补充的实用定义:对象公开方法的子集,让对象在系统中扮演特定的角色。也就是说,接口是实现特定角色的方法集合。像Python文档中所说的文件类对象和可迭代对象就是这个意思。另外,协议与继承也没有关系,一个类可能会实现多个接口,从而让实例扮演多个角色。
协议是接口,但不是正式的,因此协议不能像正式接口那样施加限制,一个类可能只实现部分接口,这是允许的。
2.Python喜欢序列
Python数据模型的哲学是尽量支持基本协议。对于序列来说,即便是最简单的实现,Python也会力求做到最好。
class Foo:
def __getitem__(self, pos):
return range(0, 30, 10)[pos]
f = Foo()
print(f[0])
print(20 in f)
print(15 in f)
在上面的实例中,定义了Foo类,它并没有继承abc.Sequence,而是只实现了序列协议的一个基本方法。通过运行代码可以发现,虽然没有_iter_方法,Foo实例也是可迭代的对象,因为存在_getitem_方法,Python会调用它,传入从零开始的整数索引,尝试迭代对象(当然,这是一种后备机制,之前讲过)。尽管没有实现_contains_方法,但是Python足够智能,等迭代Foo实例,也能运行in运算符。这一切都指向了协议的重要性。
3.是用猴子补丁在运行时实现协议
我们之前定义过FrenchDeck类有个重大的缺陷,就是无法进行洗牌。我们不能直接使用random.shuffle来进行洗牌,因为我们定义的FrenchDeck类是不可变的序列协议,要将其变成可变的需要提供_setitem_方法。
from random import shuffle
def set_card(deck, poisition, card):
deck._card[poisition] = card
FrenchDeck.__setitem__ = set_card()
shuffle(deck)
在这个实例中,我们将定义的函数赋值给FrenchDeck类的_setitem_属性,然后就可以用shuffle来进行卡牌的打乱了。
这个实例的关键是,set_card函数要知道deck对象有一个名为_cards的属性,而且它还是可变序列。然后,我们把set_card函数赋值给_setitem_这个特殊方法,从而将这个方法依附到FrenchDeck类上。这种技术叫猴子补丁:在运行时修改类和模块,而不改动源代码。但是,打补丁的代码要与打补丁的程序十分耦合,而且要处理好隐藏和没有文档的部分。
4.定义抽象基类的子类
在Python中,我们一般是先利用现有的抽象基类,然后在斗胆自己定义。下面我们将French
Deck2声明为collections.MutableMapping的子类。
import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck2(collections.MutableMapping):
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, poistion):
return self._cards[poistion]
def __setitem__(self, poistion, value):
self._cards[poistion] = value
def __delitem__(self, poistion):
del self._cards[poistion]
def insert(self, poistion, value):
self._cards.insert(poistion, value)
为了支持洗牌,我们只需要实现_setitem_方法,同时因为是继承collections.MutableMapping类,就必须实现_delitem_方法,并且还要实现_insert_方法。
通常,导入时,Python不会检查抽象方法的实现,在运行实例化时才会真正的检查。当检查到没有正确实现某个抽象方法时,就会抛TypeError异常。这也就是我们要定义insert等方法的原因。另外,要想实现子类,我们可以覆盖从抽象基类中继承的方法,以更高效的方式重新实现。
5.标准库中的抽象基类
Python标准库提供了一些抽象基类。大多数抽象基类在collections.abc模块中定义,不过其它地方也会有一些。如:numbers和io包。但是总的来说,collections.abc中抽象基类最常用。
5.1collections.abc模块中的抽象基类
在collections.abc模块中定义了16个抽象基类,简要的UML类图,如下图所示。
(1)Iterable、Container和Sized
各个集合应该继承这三个抽象基类,或者至少实现兼容的协议。Iterable通过_iter_方法支持迭代、Container通过_contains_方法支持in运算符、Sized通过_len_方法支持len()函数。
(2)Sequence、Mappling和Set
这三个是主要的不可变集合类型,而且各自都有可变的子类。
(3)MappingView
在Python3中,映射方法.items()、.keys()和.values()返回的对象是ItemsView、KeysView、ValuesView的实例。前两个类还从Set类继承了丰富的接口。
(4)Callable和Hashable
这两个抽象基类与集合没有太大关系,只不过是因为它们重要,才把它们放到该模块中。这两个抽象基类的作用是为内置函数isinstance提供支持,以一种安全的方式判断对象能不能调用或者散列。
(5)Iterator
它是Iterable的子类,后续我们会讲到。
5.2抽象基类的数字塔
numbers包定义的是数字塔(即各个抽象基类的层次结构是线性的)。其排序顺序如下。
Number | 位于顶端的超类 |
Complex | |
Real | 可以检查一个数是否是浮点数 |
Rational | |
Intergral | 可以用来检查一个数是否是整数 |
6.定义并使用一个抽象基类
为了证明有必要定义抽象基类,我们先要在框架中找到使用它的场景。想象一下下面这个场景:你要在网站和移动应用中随机显示广告,但是要在整个广告清单轮转一遍之前,不重复显示广告。假设我们构建一个广告管理框架,名为ADAM。它的职责之一是,支持用户提供随机挑选的无重复类。下面,我们将定义一个Tombola的类来实现这个功能。
import abc
class Tombola(abc.ABC):
@abc.abstractmethod
def load(self, iterable):
"""从可迭代对象中添加元素"""
@abc.abstractmethod
def pick(self):
"""随机删除元素,然后将其返回。如果实例为空,这个方法应该抛出LookupError"""
def loaded(self):
"""如果至少有一个元素,就返回Ture"""
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类。抽象方法使用@abstractmethod装饰器来标记,而且定义体中通常只有文档字符串,不定义具体的方法,但是,在有的情况下也需要定义具体方法,这时,抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用抽象基类中的其它具体方法、抽象方法或特性)。根据文档字符串,如果没有元素可选,应该抛出LookupError。当我们不知道具体子类如何存储元素时,我们可以不断调用pick()方法,吧Tombola清空,再使用load()把所有元素都加进去。
6.1抽象基类句法详解
声明抽象基类最简单的方法是继承abc.ABC或者其他抽象基类。但是,如果你使用的是旧版本的Python,并且继承现有的抽象基类不可取时,必须在class语句中使用metaclass=关键字。如下:
class Tombola(metaclass=abc.ABCmeta):
6.2定义Tombola抽象基类的子类
定义好Tombola抽象基类后,我们会开发两个具体的子类,满足Tombola具体的接口。首先,我们先定义一个名为bingocage的具体子类。
import random
from tombola import Tombola
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()
在该段代码中,我们首先指定其为Tombola的子类,假设我们将在线上游戏使用这个。我们先使用random模块来进行随机,再使用load()方法来进行初始加载。该类会从Tombola类中继承loaded和inspect方法,当然,我们也可以进行覆盖,使用个人能够高效的代码。
其次,我们定义一个LotteryBlower类。
import random
from tombola import Tombola
class LotteryBlower(Tombola):
def __init__(self, iterable):
self._balls = list(iterable)
def load(self, iterable):
self._balls.extend(iterable)
def pick(self):
try:
poistion = random.randrange(len(self._balls))
except ValueError:
raise LookupError('pick from empty LotteryBlower')
return self._balls.pop(poistion)
def loaded(self):
return bool(self._balls)
def inspect(self):
return tuple(sorted(self._balls))
同样,LotteryBlower类也是为Tombola的子类。该类首先初始化方法接收任何可迭代的对象:把参数构建成列表。随后,在pick方法中检查,如果范围为空,函数会抛出ValueError,当然,我们为了兼容,抛出LookupError。如果范围不为空,就从其中随机弹出元素。最后,通过重新定义loaded和inspect方法,覆盖了原有的代码。
6.3Tombola的虚拟子类
“白鹅”类型的一个基本特性:即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我们保证注册的类忠实地实现了抽象基类定义的接口,而Python也会相信我们。一旦我们说谎了,就会抛出异常。
注册虚拟子类的方法是在抽象基类上调用register方法。这样做之后,注册的类就会变成抽象基类的虚拟子类,而且issubclass和isinstance等函数都能识别。但是,注册的类不会从抽象基类中继承任何属性和方法。
register方法通常作为普通函数来进行使用,但有时也作为装饰器进行使用。如下:
from random import randrange
from tombola import Tombola
@Tombola.register
class Tombolist(list):
def pick(self):
if self:
poistion = randrange(len(self))
return self.pick(poistion)
else:
raise LookupError('pop from empty TomboList')
load = list.extend
def loaded(self):
return bool(self)
def inspect(self):
return tuple(sorted(self))
我们先是把Tombolist注册为Tombola的虚拟子类,又将Tombolist拓展为list,因此,用它继承list类中的方法,可以调用pop等方法。pick方法调用继承自list的pop方法,传入一个随机元素的索引。 7.Tombola子类的测试方法
我们编写的测试实例中有两个类的属性,用它们内省类的继承关系。
(1)_subclasses_
这个方法返回类的直接子类列表,不含虚拟子类。
(2)_abc_registry_
只有抽象基类有这个数据属性,其值是一个WeakSet对象,即抽象类注册的虚拟子类的弱引用。
今天的文章Python编程:接口分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/69031.html