第一章《流畅的python》里面的描述:Python 的魔术方法(magic method)是特殊方法的昵称。一般是用“双下划线+名称+双下划线”形式来表示,整体念起来也拗口,所以也有人把这种特殊方法名为称为“双下方法”(dunder method)。
有关于特殊方法一览,可以参考Data model
这边借用文章第一章中字牌一个小例子来说一下实现魔术方法对python数据模型好处:
## test.py
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 __setitem__(self, position, card):
self._cards[position] = card
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
建立了一个字牌类,52张牌,从2到A,花色为黑桃,方块,梅花,红心
并且实现了两个特殊方法“__len__”, "__getitem__",看到这里可能没感受到魔法方法魅力
先看一下输出,用内置函数len查看牌有几张
>>>from test import FrenchDeck
>>>deck = FrenchDeck()
>>>len(deck)
52
如果是从一叠牌中抽取一张牌,或者随机抽取一张牌,或者从上到下抽4张牌,洗牌后取第一张牌
>>>deck[0]
Card(rank='2', suit='spades')
>>>import random
>>>random.choice(deck)
Card(rank='5', suit='hearts')
>>>deck[:4]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades'), Card(rank='5', suit='spades')]
>>>from random import shuffle
>>>shuffle(deck)
>>>deck[0]
Card(rank='J', suit='spades')
在这个数据模型中,使用这些特殊方法,可以观察到:
- FrenchDeck实现__len__方法,在调用内置函数len(deck),实际上是调用__len__()方法。
- FrenchDeck实现__getitem__方法,在deck[0],“[0]”中括号取0位置的时候触发__getitem__(0)方法。
- FrenchDeck还实现了__setitem__方法,这边比较隐晦,实际上random.shuffle洗牌过程中,是通过互相调换元素位置来实现的,掉换位置时候涉及到赋值,也就是说必须提供赋值的方法,可以试验一下当把__setitem__方法都注释掉,按照一下试一下,注释掉时候报错,注释去掉即可赋值成功。:
>>>from test import Card
>>>from test import FrenchDeck
>>>deck = FrenchDeck()
>>>deck[0]
Card(rank='2', suit='spades')
>>>A = Card('spades','A')
>>>deck[0] = A
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'FrenchDeck' object does not support item assignment
这么做的好处是什么?
- 一致性:python有个很重要的品质就是“一致性”,当我们设计这样一个数据模型,我们实现这样特殊方法,调用者只需要调用内置函数len,我们不需要额外新建一个内置函数比如size()这样的方法,这样调用者不需要记住那么多方法名,只需要像操作列表那样操作即可。
- 兼容性:并且实现这样魔术方法,很大程度上更方便调用python标准库,比如上述的random库,我们用到了随机取值和打乱洗牌的动作。
上面我们也可以调用in运算符,或者for循环:
>>> for card in deck:
... 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('A','spades') in deck
True
虽然FrenchDeck类中没有实现__iter__方法和__contains__,但是也能使用for循环,也能用in运算符,实际上启动了后备机制,或者说python的for循环实际上是兼容两种机制的
- 一种是迭代器,实现了__iter__和__next__方法,它不断调用__next__方法来获取迭代器下一个值
- 一种是如果没有实现__iter__方法,但是实现了__getitem__方法的,会从0开始传入索引,尝试迭代对象。
in也是如此,也是按顺序迭代搜索。所以python这种机制,让没有实现__iter__方法和__contains__,但是有实现__getitem__方法也能用for语句和in运算符。
这里仿造这种方式定义一个表格的类,表格有行列,可以模仿numpy数组来取值[i,j],可以用len()函数来表示表格又多少行和列:
## test.py
class Table:
def __init__(self, rows, columns):
self._rows = int(rows)
self._columns = int(columns)
self._table = dict.fromkeys([(x, y) for x in range(rows)
for y in range(columns)])
def __setitem__(self, position, value):
x, y = position
self._table[(position)] = value
def __len__(self):
return self._rows, self._columns
def __getitem__(self, position):
x, y = position
return self._table[(x,y)]
def __repr__(self):
return "Table(%s, %s)"%(self._rows,self._columns)
查看一下输出结果:
>>>from test import Table
>>>table = Table(2,2)
>>> table
Table(2, 2)
>>>table[0,0] = 1
>>> table[0,0]
1
表格输入是建立几行几列的表格,并且用一个字典将他的坐标(i,j)作为键值,要成为字典的键值,须属于可散列数据类型,并且也得实现__hash__方法。原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,当然对于元组来说,要每个对象都得是散列数据类型也能成为键值,我们这边的,x,y都是数值类型,所以何在一起元组子自然也能成为键值。
这边 __getitem__ 和 __setitem__ 实现方法, 会在table[i,j]调用时候以(i,j)元组形式传进来,如__getitem__((i, j)),这里的position就是元组(i,j)
这边也仅限于取[i,j]的坐标的值,需要能实现切片,例如table[0,:],可以在__getitem__里面另作判断,加上isinstanc(position, slice)类似的判断,另作处理。获得整行或者整列的数据。