Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾(例如__getitem__
)。
譬如,当使用len(obj)
时,解释器实际会调用obj.__len__
这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:
- 迭代 (
__iter__
,__reversed__
...) - 集合类 (
__getitem__
,__len__
...) - 属性访问 (
__getattr__
,__setattr__
...) - 运算符重载 (
__lt__
,__add__
...) - 对象的创建和销毁 (
__new__
,__del__
...) - 字符串表示形式和格式 (
__repr__
,__str__
...) - 管理上下文(即with模块)(
__enter__
,__exit__
)
下面来看一个例子,其中的类实现了2个特殊方法,__len__
和__getitem__
。
from collections import namedtuple
# 使用命名元组,可以简单的构建一个对象
Card = namedtuple("Card", ["rank", "suit"])
class FrenchDeck:
# 2-A
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
# 4种花色
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]
用len() 函数来查看一叠牌有多少张,由__len__
实现:
>>> deck = FrenchDeck()
>>> len(deck)
52
抽取特定的一张纸牌,由__getitem__
实现.
>>> deck[0]
Card(rank='2', suit='spades')
因为__getitem__
方法把[]
操作交给了self._cards
列表,所以deck 类自动支持切片(slicing)操作。
# 查看最上面3 张
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
# 只看牌面是A 的牌
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
另外,仅仅实现了__getitem__
方法,这一摞牌就变成可迭代的了:
>>> for card in deck: # doctest: +ELLIPSIS
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...
迭代通常是隐式的,譬如说一个集合类型没有实现__contains__
方法,那么in
运算符就会按顺序做一次迭代搜索。
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False
从上面的例子可以看出,通过实现__len__
和__getitem__
这两个特殊方法,FrenchDeck就跟一个Python 自有的序列数据类型一样,可以体现出Python 的核心语言特性(例如迭代和切片)
另外要明确的是,特殊方法的存在是为了被Python解释器调用的,自己并不需要调用它。也就是说没有obj.__len__
这种写法,而应该使用len(obj)
.
然而如果是Python内置类型,譬如列表、字符串、字节序列等,那么CPython会抄近路,__len__
实际上会直接返回PyVarObject里的ob_size
属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体,直接读取这个值比调用一个方法快很多。很多时候,特殊方法的调用的是隐式的,比如for i in x:
这个语句,背后其实用的是iter(x)
。而这个函数的背后则是x.__iter__()
方法。
另外,__init__
方法外比较特殊,代码里我们可能会经常用到它,目的是在自己的子类__init__
方法中调用超类的构造器。