Python 官方文档:入门教程 => 点击学习
1.Python文化中的接口和协议 在python中,我们把协议定义为非正式的接口,是让Python这种动态类型语言实现多态的方式。那么,接口在动态类型语言中是怎么运作的呢? 首先,基本的事实是,Python
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 shuffledef set_card(deck, poisition, card): deck._card[poisition] = cardFrenchDeck.__setitem__ = set_card()shuffle(deck)
在这个实例中,我们将定义的函数赋值给FrenchDeck类的_setitem_属性,然后就可以用shuffle来进行卡牌的打乱了。
这个实例的关键是,set_card函数要知道deck对象有一个名为_cards的属性,而且它还是可变序列。然后,我们把set_card函数赋值给_setitem_这个特殊方法,从而将这个方法依附到FrenchDeck类上。这种技术叫猴子补丁:在运行时修改类和模块,而不改动源代码。但是,打补丁的代码要与打补丁的程序十分耦合,而且要处理好隐藏和没有文档的部分。
4.定义抽象基类的子类
在Python中,我们一般是先利用现有的抽象基类,然后在斗胆自己定义。下面我们将French
Deck2声明为collections.MutableMapping的子类。
import collectionsCard = 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 abcclass 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 randomfrom tombola import Tombolaclass 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 randomfrom tombola import Tombolaclass 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 randrangefrom tombola import Tombola@Tombola.registerclass 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对象,即抽象类注册的虚拟子类的弱引用。
来源地址:https://blog.csdn.net/weixin_56051592/article/details/128441632
--结束END--
本文标题: Python编程:接口
本文链接: https://lsjlt.com/news/490602.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-03-01
2024-03-01
2024-03-01
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
2024-02-29
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0