——Bjarne Stroustrup
C++之父
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()方法。在特定的上下文中可能需要其他文件操作方法,也可能不需要。
看看示例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 "" , 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函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来再提供也行。