文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Python编程:接口

2023-10-27 12:14

关注

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

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯