Python编程:接口

更新时间:2023-03-30 11:20:18 点击次数:478次
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对象,即抽象类注册的虚拟子类的弱引用。                                                                                                                                                                

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!