python接口:从协议到抽象基类


抽象类表示接口。

——Bjarne Stroustrup

C++之父

Python文化中的接口和协议

Python语言没有interface关键字,而且除了抽象基类,每个类都有接口:类实现或继承的公开属性(方法或数据属性),包括特殊方法,如__getitem__或__add__。

受保护的属性和私有属性不在接口中:即便“受保护的”属性也只是采用命名约定实现的(单个前导下划线);私有属性可以轻松地访问。

不要觉得把公开数据属性放入对象的接口中不妥,因为如果需要,总能实现读值方法和设值方法,把数据属性变成特性,如下实例1

#示例1:x和y是公开数据属性
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)) 

把x和y变成了只读特性。这是一项重大重构,但是Vector2d的接口基本没变:用户仍能读取my_vector.x和my_vector.y。如下示例2:

#示例2:使用特性实现x和y(完整的代码清单参见示例9-9)
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)) 
    # 下面是其他方法(这个代码清单将其省略了)

这里有个实用的补充定义:对象公开方法的子集,让对象在系统中扮演特定的角色。

协议是接口,但不是正式的(只由文档和约定定义),因此协议不能像正式接口那样施加限制(本章后面会说明抽象基类对接口一致性的强制)。一个类可能只实现部分接口,这是允许的。有时,某些API只要求“文件类对象”返回字节序列的.read()方法。在特定的上下文中可能需要其他文件操作方法,也可能不需要。

python序列

图1展示了定义为抽象基类的Sequence正式接口。
在这里插入图片描述

Sequence抽象基类和collections.abc中相关抽象类的UML类图,箭头由子类指向超类,以斜体显示的是抽象方法

看看示例3中的Foo类。它没有继承abc.Sequence,而且只实现了序列协议的一个方法:getitem(没有实现__len__方法)。

#示例3:定义__getitem__方法,只实现序列协议的一部分,这样足够访问元素、迭代
和使用in运算符了
>>> class Foo: 
...     def __getitem__(self, pos): 
...         return range(0, 30, 10)[pos] 
... 
>>> f = Foo() 
>>> f[1] 
10 
>>> for i in f: print(i) 
... 
0 
10 
20 
>>> 20 in f 
True 
>>> 15 in f 
False

虽然没有__iter__方法,但是Foo实例是可迭代的对象,因为发现有__getitem__方法时,Python会调用它,传入从0开始的整数索引,尝试迭代对象(这是一种后备机制)。
尽管没有实现__contains__方法,但是Python足够智能,能迭代Foo实例,因此也能使用in运算符:Python会做全面检查,看看有没有指定的元素。

综上,鉴于序列协议的重要性,如果没有__iter__和__contains__方法,Python会调用__getitem__方法,设法让迭代和in运算符可用。

示例4中定义的FrenchDeck类也没有继承abc.Sequence,但是实现了序列协议的两个方法:__getitem__和__len__。

#示例4:实现序列协议的FrenchDeck类(代码与示例1-1相同)
import collections 
 
Card = collections.namedtuple('Card', ['rank', 'suit']) 
 
class FrenchDeck: 
    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, position): 
        return self._cards[position]

使用猴子补丁在运行时实现协议

示例4中的FrenchDeck类有个重大缺陷:无法洗牌。

标准库中的random.shuffle函数用法如下:

>>> from random import shuffle 
>>> l = list(range(10)) 
>>> shuffle(l) 
>>> l 
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]
#示例5:random.shuffle函数不能打乱FrenchDeck实例
>>> from random import shuffle 
>>> from frenchdeck import FrenchDeck 
>>> deck = FrenchDeck() 
>>> shuffle(deck) 
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
  File ".../python3.3/random.py", line 265, in shuffle 
    x[i], x[j] = x[j], x[i] 
TypeError: 'FrenchDeck' object does not support item assignment

错误消息相当明确,“‘FrenchDeck’ object does not support item assignment”('FrenchDeck’对象不支持为元素赋值)。
这个问题的原因是,shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列协议。可变的序列还必须提供__setitem__方法。

Python是动态语言,因此我们可以在运行时修正这个问题,如下示例6:

#示例6:为FrenchDeck打猴子补丁,把它变成可变的,让random.shuffle函数能处理(接示例5)
>>> def set_card(deck, position, card): #➊ 
...     deck._cards[position] = card 
... 
>>> FrenchDeck.__setitem__ = set_card #➋ 
>>> shuffle(deck) #➌ 
>>> deck[:5] 
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4', 
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]

➊ 定义一个函数,它的参数为deck、position和card。
➋ 把那个函数赋值给FrenchDeck类的__setitem__属性。
➌ 现在可以打乱deck了,因为FrenchDeck实现了可变序列协议所需的方法。

这里的关键是,set_card函数要知道deck对象有一个名为_cards的属性,而且_cards的值必须是可变序列。然后,我们把set_card函数赋值给特殊方法__setitem__,从而把它依附到FrenchDeck类上。
这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。猴子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而且往往要处理隐藏和没有文档的部分

除了举例说明猴子补丁之外,示例6还强调了协议是动态的:random.shuffle函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来再提供也行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值