前言 特殊的方法
Python 解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对
象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾(例如getitem)。
比如obj[key] 的背后就是getitem 方法,为了能求得my_collection[key] 的值,解释器实际上会调用my_collection.getitem(key)。
当自定义的类实现了__getitem__ 和 __len__
方法的时候, 就相当于是实现了自定义类的可迭代的功能
一摞有序的纸牌
import collections
Card = collections.namedtuple('Card', ['rank', 'suit']) # 使用nametuple 来实现带有名字的元组, 相当于是一个只有属性的类的使用方法
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]
创建对象
>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')
当调用len()函数的时候, 如果参数是自定义的类的话, 就会去调用类中实现的len()方法
当使用obj[key]的时候, 就会去类中调用实现的getitem函数来完成数据的取值, 操作, 从而可以通过键或者索引来进行取值的操作
从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易的:deck[0] 或
deck[-1]。这都是由getitem 方法提供的:
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
使用random.choic(deck) 来随机的抽取一张牌
>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='hearts')
>>> choice(deck)
Card(rank='K', suit='spades')
>>> choice(deck)
Card(rank='2', suit='clubs')
使用reversed
来实现可迭代对象的反向的迭代的操作, 使用reversed 实现的反向迭代是不会改变原来列表的顺序的, 会在产生一个新的列表
list1 = [1,2,3,4,5]
list2 = reversed(list1)
list2
Out[3]: <list_reverseiterator at 0x4a98518>
list1
Out[4]: [1, 2, 3, 4, 5]
for item in list2:
print(item)
5
4
3
2
1
通过实现len 和getitem 这两个特殊方法,FrenchDeck
就跟一个Python 自有的序列数据类型一样,可以体现出Python 的核心语言特性(例如迭
代和切片)。同时这个类还可以用于标准库中诸如random.choice、reversed 和sorted 这
些函数。
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 中被实现了。
字符串的表现形式
Python 有一个内置的函数叫repr,它能把一个对象用字符串的形式表达出来以便辨认,这
就是“字符串表示形式”。repr 就是通过repr 这个特殊方法来得到一个对象的字符串
表示形式的。如果没有实现repr,当我们在控制台里打印一个向量的实例时,得到的
字符串可能会是。
repr 和str 的区别在于,后者是在str() 函数被使用,或是在用print 函数打印
一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好.
如果你只想实现这两个特殊方法中的一个,repr 是更好的选择,因为如果一个对象没
有str 函数,而Python 又需要调用它的时候,解释器会用repr 作为替代
自定义的布尔值
尽管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。
特殊方法一栏
- 集合模拟
集合模拟__len__、__getitem__、__setitem__、__delitem__、__contains__
- 迭代的枚举
迭代枚举__iter__、__reversed__、__next__
- 可调用模拟
可调用模拟__call__
- 实例创建和销毁
实例创建和销毁__new__、__init__、__del__
- 属性管理
属性管理__getattr__、__getattribute__、__setattr__、__delattr__、__dir__
- 属性描述符
属性描述符__get__、__set__、__delete__
为什么len() 函数不是普通的函数
如果x 是一个内置类型的实例,那么len(x) 的速度会非常快。背后的
原因是CPython 会直接从一个C 结构体里读取对象的长度,完全不会调用任何方法。获取
一个集合中元素的数量是一个很常见的操作,在str、list、memoryview 等类型上,这个
操作必须高效。
换句话说,len 之所以不是一个普通方法,是为了让Python 自带的数据结构可以走后门,
abs 也是同理。但是多亏了它是特殊方法,我们也可以把len 用于自定义数据类型。这种
处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了
“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”