1.1 一摞Python风格的纸牌
Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名
字以两个下划线开头,以两个下划线结尾(例如__getitem__)。比如 obj[key] 的背后就是
__getitem__ 方法,为了能求得my_collection[key] 的值,解释器实际上会调用
my_collection.__getitem__(key)。
示例 1-1 一摞有序的纸牌
自Python 2.6 开始,namedtuple 就加入到 Python 里,用以构建只有少数属性但是没有方法的对象
,比如数据库条目。如下面这个控制台会话所示,利用 namedtuple,我们可以很轻松地得到一个纸
牌对象:
当然,我们这个例子主要还是关注 FrenchDeck 这个类,它既短小又精悍。首先,它跟任何标准
Python 集合类型一样,可以用 len() 函数来查看一叠牌有多少张:
从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易的:deck[0] 或deck[-1]。这
都是由 __getitem__ 方法提供的:
我们需要单独写一个方法用来随机抽取一张纸牌吗?没必要,Python 已经内置了从一个序列中随机
选出一个元素的函数 random.choice,我们直接把它用在这一摞纸牌实例上就好:
因为 __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以我们的 deck 类自动支持切片(
slicing)操作。下面列出了查看一摞牌最上面 3 张和只看牌面是 A 的牌的操作。其中第二种操作
的具体方法是,先抽出索引是 12 的那张牌,然后每隔 13 张牌拿 1张:
另外,仅仅实现了 __getitem__ 方法,这一摞牌就变成可迭代的了:
反向迭代也没关系:
迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序
做一次迭代搜索。于是,in 运算符可以用在我们的 FrenchDeck 类上,因为它是可迭代的:
那么排序呢?我们按照常规,用点数来判定扑克牌的大小,2 最小、A 最大;同时还要加上对花色的
判定,黑桃最大、红桃次之、方块再次、梅花最小。下面就是按照这个规则来给扑克牌排序的函数,
梅花 2 的大小是 0,黑桃 A 是 51:
有了 spades_high 函数,就能对这摞牌进行升序排序了:
虽然 FrenchDeck 隐式地继承了 object 类, 但功能却不是继承而来的。我们通过数据模型和一些
合成来实现这些功能。通过实现 __len__ 和 __getitem__ 这两个特殊方法,FrenchDeck 就跟一个
Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭代和切片)。同时
这个类还可以用于标准库中诸如random.choice、reversed 和 sorted 这些函数。另外,对合成的运
用使得 __len__ 和__getitem__ 的具体实现可以代理给 self._cards 这个 Python 列表(即 list
对象)。
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是
说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的
时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的
__len__ 方法。然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列
(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回PyVarObject 里的
ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C语言结构体。直接读取这个值比
调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是iter(x),而这
个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。通常你的代码无需
直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们
的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类
的 __init__ 方法中调用超类的构造器。通过内置的函数(例如 len、iter、str,等等)来使用特
殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的
类来说,它们的速度更快。14.12 节中有详细的例子。不要自己想当然地随意添加特殊方法,比如
__foo__ 之类的,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。
示例 1-2 一个简单的二维向量类
虽然代码里有 6 个特殊方法,但这些方法(除了 __init__)并不会在这个类自身的代码中使用。即
便其他程序要使用这个类的这些方法,也不会直接调用它们,就像我们在上面的控制台对话中看到的
。上文也提到过,一般只有 Python 的解释器会频繁地直接调用这些方法。接下来看看每个特殊方法
的实现。
1.2.2 字符串表示形式
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字
符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如
果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector
object at 0x10e100070>。
交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;在老的使用% 符号的字
符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新
式字符串格式化语法(https://docs.python.org/2/library/string.html#format-string-syntax)
也是利用了 repr,才把!r 字段变成字符串。
在 __repr__ 的实现中,我们用到了 %r 来获取对象各个属性的标准字符串表示形式——这是个好习
惯,它暗示了一个关键:Vector(1, 2) 和 Vector('1', '2') 是不一样
的,后者在我们的定义中会报错,因为向量对象的构造函数只接受数值,不接受字符串。
__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对
象。因此这里使用了类似调用对象构造器的表达形式(比如 Vector(3, 4)
就是个例子)。
__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对
象的时候才被调用的,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有
__str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。
通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的是,
这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或
other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对
象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。
1.2.4 自定义的布尔值
尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如if 或
while 语句,或者 and、or 和 not 运算符)。为了判定一个值 x 为真还是为假,Python 会调用
bool(x),这个函数只能返回 True 或者 False。默认情况下,我们自己定义的类的实例总被认为是
真的,除非这个类对 __bool__ 或者__len__ 函数有自己的实现。bool(x) 的背后是调用
x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返
回 0,则 bool 会返回False;否则返回 True。我们对 __bool__ 的实现很简单,如果一个向量的模
是 0,那么就返回 False,其他情况则返回 True。因为 __bool__ 函数的返回类型应该是布尔型,
所以我们通过bool(abs(self)) 把模值变成了布尔值。
Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名
字以两个下划线开头,以两个下划线结尾(例如__getitem__)。比如 obj[key] 的背后就是
__getitem__ 方法,为了能求得my_collection[key] 的值,解释器实际上会调用
my_collection.__getitem__(key)。
示例 1-1 一摞有序的纸牌
import collections
Card = collections.namedtuple('Card',['rank','suit']) #需要掌握命名元组的使用
class FrenchDeck:
ranks = [str(n) for n in range(2,11)] + list('JQKA') #13张牌
suits = 'spades diamonds clubs hearts'.split() #4个花色
#ranks和suits都是list类型
def __init__(self):
self._cards = [Card(rank,suit) for suit in self.suits
for rank in self.ranks] #两个for循环使用技巧,需
要掌握
def __len__(self):
return len(self._cards) #返回列表长度,使对象能直接嗲用len函数
def __getitem__(self,position):
return self._cards[position] #通过列表[下标]返回元素,从而支持getitem功能
自Python 2.6 开始,namedtuple 就加入到 Python 里,用以构建只有少数属性但是没有方法的对象
,比如数据库条目。如下面这个控制台会话所示,利用 namedtuple,我们可以很轻松地得到一个纸
牌对象:
In [2]: beer_card = Card('7','diamonds')
In [3]: beer_card
Out[3]: Card(rank='7', suit='diamonds')
当然,我们这个例子主要还是关注 FrenchDeck 这个类,它既短小又精悍。首先,它跟任何标准
Python 集合类型一样,可以用 len() 函数来查看一叠牌有多少张:
In [4]: deck = FrenchDeck()
In [5]: len(deck)
Out[5]: 52
从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易的:deck[0] 或deck[-1]。这
都是由 __getitem__ 方法提供的:
In [6]: deck[0]
Out[6]: Card(rank='2', suit='spades')
In [7]: deck[-1]
Out[7]: Card(rank='A', suit='hearts')
我们需要单独写一个方法用来随机抽取一张纸牌吗?没必要,Python 已经内置了从一个序列中随机
选出一个元素的函数 random.choice,我们直接把它用在这一摞纸牌实例上就好:
In [9]: choice(deck)
Out[9]: Card(rank='5', suit='hearts')
In [10]: choice(deck)
Out[10]: Card(rank='9', suit='spades')
In [11]: choice(deck)
Out[11]: Card(rank='7', suit='hearts')
因为 __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以我们的 deck 类自动支持切片(
slicing)操作。下面列出了查看一摞牌最上面 3 张和只看牌面是 A 的牌的操作。其中第二种操作
的具体方法是,先抽出索引是 12 的那张牌,然后每隔 13 张牌拿 1张:
In [12]: deck[:3]
Out[12]:
[Card(rank='2', suit='spades'),
Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
In [13]: deck[12:13]
Out[13]: [Card(rank='A', suit='spades')]
In [14]: deck[12::13]
Out[14]:
[Card(rank='A', suit='spades'),
Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'),
Card(rank='A', suit='hearts')]
另外,仅仅实现了 __getitem__ 方法,这一摞牌就变成可迭代的了:
In [16]: for card in deck: #doctest: +ELLIPSIS
...: print (card)
...:
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
Card(rank='5', suit='spades')
Card(rank='6', suit='spades')
Card(rank='7', suit='spades')
Card(rank='8', suit='spades')
Card(rank='9', suit='spades')
.....
反向迭代也没关系:
In [17]: for card in reversed(deck):#doctest:+ELLIPSIS
...: print (card)
...:
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
Card(rank='J', suit='hearts')
Card(rank='10', suit='hearts')
Card(rank='9', suit='hearts')
Card(rank='8', suit='hearts')
Card(rank='7', suit='hearts')
.......
迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方法,那么 in 运算符就会按顺序
做一次迭代搜索。于是,in 运算符可以用在我们的 FrenchDeck 类上,因为它是可迭代的:
In [19]: Card('Q','hearts') in deck
Out[19]: True
In [20]: Card('7','beass') in deck
Out[20]: False
那么排序呢?我们按照常规,用点数来判定扑克牌的大小,2 最小、A 最大;同时还要加上对花色的
判定,黑桃最大、红桃次之、方块再次、梅花最小。下面就是按照这个规则来给扑克牌排序的函数,
梅花 2 的大小是 0,黑桃 A 是 51:
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def spades_high(card):
rank_value = FrenchDeck.ranks.index(card.rank) #rank_value,'2' 的值为0,'A'的值为12
return rank_value * len(suit_values) + suit_values[card.suit]
有了 spades_high 函数,就能对这摞牌进行升序排序了:
>>> for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (46 cards ommitted)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')
虽然 FrenchDeck 隐式地继承了 object 类, 但功能却不是继承而来的。我们通过数据模型和一些
合成来实现这些功能。通过实现 __len__ 和 __getitem__ 这两个特殊方法,FrenchDeck 就跟一个
Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭代和切片)。同时
这个类还可以用于标准库中诸如random.choice、reversed 和 sorted 这些函数。另外,对合成的运
用使得 __len__ 和__getitem__ 的具体实现可以代理给 self._cards 这个 Python 列表(即 list
对象)。
1.2 如何使用特殊方法
首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是
说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。在执行 len(my_object) 的
时候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调用其中由你实现的
__len__ 方法。然而如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列
(bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回PyVarObject 里的
ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C语言结构体。直接读取这个值比
调用一个方法要快很多。
很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是iter(x),而这
个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。通常你的代码无需
直接使用特殊方法。除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于你去实现它们
的次数。唯一的例外可能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自己的子类
的 __init__ 方法中调用超类的构造器。通过内置的函数(例如 len、iter、str,等等)来使用特
殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的
类来说,它们的速度更快。14.12 节中有详细的例子。不要自己想当然地随意添加特殊方法,比如
__foo__ 之类的,因为虽然现在这个名字没有被 Python 内部使用,以后就不一定了。
示例 1-2 一个简单的二维向量类
from math import hypot
class Vector:
def __init__(self,x=0,y=0):
self.x = x
self.y = y
def __repr__(self):
return 'Vector(%r,%r)' % (self.x,self.y)
def __abs__(self):
return hypot(self.x,self.y)
def __bool__(self):
return bool(abs(self))
def __add__(self,other):
x = self.x + other.x
y = self.y + other.y
return Vector(x,y)
def __mul__(self,scalar):
return Vector(self.x * scalar,self.y * scalar)
虽然代码里有 6 个特殊方法,但这些方法(除了 __init__)并不会在这个类自身的代码中使用。即
便其他程序要使用这个类的这些方法,也不会直接调用它们,就像我们在上面的控制台对话中看到的
。上文也提到过,一般只有 Python 的解释器会频繁地直接调用这些方法。接下来看看每个特殊方法
的实现。
1.2.2 字符串表示形式
Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字
符串表示形式”。repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如
果没有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是 <Vector
object at 0x10e100070>。
>>> vect = Vector(3,4)
>>> vect #这里调用了__repr__函数,如果类中没有定义这个函数,这打印类似这
样的值<Vector object at 0x10e100070>
Vector(3,4)
交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;在老的使用% 符号的字
符串格式中,这个函数返回的结果用来代替 %r 所代表的对象;同样,str.format 函数所用到的新
式字符串格式化语法(https://docs.python.org/2/library/string.html#format-string-syntax)
也是利用了 repr,才把!r 字段变成字符串。
在 __repr__ 的实现中,我们用到了 %r 来获取对象各个属性的标准字符串表示形式——这是个好习
惯,它暗示了一个关键:Vector(1, 2) 和 Vector('1', '2') 是不一样
的,后者在我们的定义中会报错,因为向量对象的构造函数只接受数值,不接受字符串。
__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对
象。因此这里使用了类似调用对象构造器的表达形式(比如 Vector(3, 4)
就是个例子)。
__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或是在用 print 函数打印一个对
象的时候才被调用的,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有
__str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。
通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算术运算符。值得注意的是,
这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或
other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对
象,而是产出一个新的值。第 13 章会谈到更多这方面的问题。
1.2.4 自定义的布尔值
尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如if 或
while 语句,或者 and、or 和 not 运算符)。为了判定一个值 x 为真还是为假,Python 会调用
bool(x),这个函数只能返回 True 或者 False。默认情况下,我们自己定义的类的实例总被认为是
真的,除非这个类对 __bool__ 或者__len__ 函数有自己的实现。bool(x) 的背后是调用
x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x) 会尝试调用 x.__len__()。若返
回 0,则 bool 会返回False;否则返回 True。我们对 __bool__ 的实现很简单,如果一个向量的模
是 0,那么就返回 False,其他情况则返回 True。因为 __bool__ 函数的返回类型应该是布尔型,
所以我们通过bool(abs(self)) 把模值变成了布尔值。