此前的文章中,我们介绍了 Python 面向对象编程及对象的继承和派生。 接下来的几篇文章,我们将详细介绍 Python 解释器提供的一系列特殊方法 -- 魔术方法。
在面向对象编程中,我们介绍了 __init__ 方法,这是由解释器默认实现,在构造对象是自动调用的特殊方法,类似的,Python 提供了一系列左右两边被一对双下划线包着的方法,这些方法被称为“魔术方法”,让我们方便的实现 Python 的核心需要特性,让你的类使用更加方便:
同时,实现这些魔术方法后,大量 Python 标准库中的方法将可以直接用于你的类。 特殊方法是 Python 解释器自动调用的,因此你无需自己处理,但是,需要注意的是,这些特殊方法是如此强大,同时也存在着很多的陷阱,在使用中必须处处小心谨慎。
__getitem__(self, key)
对于容器来说,获取元素是最重要的操作,魔术方法 __getitem__就完成了这个工作,每当对对象通过[]操作符获取元素时,解释器都会自动调用该魔术方法。
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 __getitem__(self, position):
return self._cards[position]
我们建立了一个纸牌类,有了 __getitem__ 方法,我们就定义了索引操作,所有 dict 通过 [] 可以做的事,我们的纸牌类都可以做到:
>>> deck = FrenchDeck()
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
同时,我们的类也因此变得可以被迭代:
>>> for card in deck:
... print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
或者反向迭代:
>>> for card in reversed(deck):
... print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
in 操作也同样可以:
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False
__len__(self)
对于容器类,一个很重要的操作是获取容器中元素的数量 — len() 我们曾经介绍过 Python 对象的内存结构: python 的内存管理与垃圾收集
len() 方法被调用时,Python 会自动调用对象的 __len__ 方法。 对于内部类型,比如 list、dict、str、bytearray 等,__len__ 方法直接返回 PyVarObject 中的 ob_size 字段,而对于自定义类对象,你就需要去实现 __len__ 方法了。
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]
执行 len(deck) 返回了 52。
__setitem__(self, key, value)
__delitem__(self, key)
上面我们实现的容器类是不可变的,如果你想要改变或删除其中的元素就会报错:
TypeError: ’FrenchDeck’ object does not support item assignment
__setiem__ 与 __delitem__ 就是分别在更改容器元素值和删除元素时被自动调用的魔术方法。
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 __getitem__(self, position):
return self._cards[position]
def __setitem__(self, key, value):
self._cards[key] = value
def __delitem__(self, key):
del self._cards[key]
if __name__ == '__main__':
deck = FrenchDeck()
print(deck[0])
deck[0] = Card('4', 'spades')
print(deck[0])
执行展示:
Card(rank=’2’, suit=’spades’) Card(rank=’4’, suit=’spades’)
__iter__(self)
__reversed__(self)
定义 __getitem__ 以后,对象已经可以被循环迭代,但更好的方式是通过 __iter__ 方法返回迭代器来支持迭代。 for x in containers 等方式的循环中,解释器会自动调用 __iter__ 方法获取迭代器进行迭代。 而有时我们需要调用 python 的内建方法 reversed 来实现反向迭代,解释器就会自动调用 __reversed__ 方法。 虽然上文提到,通过 __getitem__ 方法就可以实现上述功能,但迭代器会让这一过程的效率更高。
__contains__(self, item)
当判断元素 in 或者 not in 容器时,python 解释器会自动调用 __contains__ 方法。
__missing__(self, key)
如果你的类是一个继承自 dict 的字典类,并且你没有实现自己的 __getitem__ 方法,那么当默认的 __getitem__ 方法发现 key 不存在时,就会调用你的 __missing__ 方法了。 但是,需要注意的是,如果你自己实现了 __getitem__ 方法,并且没有调用父类的 __getitem__ 方法,那 __missing__ 将永远都不会被调用。 这有两种方法可以解决:
显式调用
使用父类 __getitem__
__getattr__(self, name)
通过类实例点属性名可以实现类属性的访问,但有时我们需要定义当属性名不存在时的行为,这时就需要实现魔术方法:__getattr__ 这个方法只有在用户访问的类属性不存在时才会被调用,通常,你可以在实现的 __getattr__ 中做兜底操作或抛出异常,也可以结合 __setattr__ 方法实现对某个属性的彻底控制。
__setattr__(self, key, value)
如果只实现 __getattr__,那你无法实现对属性的完全控制和封装,因为 Python 独特的语言特性,只要在类外为不存在的属性赋值,改属性就会被创建,而 __getattr__ 只有在属性不存在的情况下才会被调用,此时,如果你需要定义独特的某个属性的行为,或彻底隐藏某个属性,就必须实现 __setattr__ 方法。 __setattr__ 方法会在每一次用户为某个属性赋值时被调用,因此要格外防范无限递归的产生:
class TechlogTest:
def __init__(self):
self.values = dict()
def __setattr__(self, key, value):
self.values[key] = value
if __name__ == '__main__':
test = TechlogTest()
test.hello = 'world'
上面这段代码看上去非常简单,在初始化时,TechlogTest 类有一个 values 成员,用来存储所有该对象的属性。 但是,运行上述代码却抛出了异常:
AttributeError: ’TechlogTest’ object has no attribute ’values’ 这是为什么呢?因为在 __init__ 方法中,对 values 成员初始化的行为让解释器自动去调用了 __setattr__ 方法,而在 __setattr__ 方法中,values 成员尚未被创建,因此抛出了异常。
改成下面这样即可:
class TechlogTest:
def __init__(self):
super.__setattr__(self, 'values', dict())
def __setattr__(self, key, value):
self.values[key] = value
if __name__ == '__main__':
test = TechlogTest()
test.hello = 'world'
print(test.values)
运行结果打印出了:
{‘hello’: ’world’}
__delattr__(self, name)
每一次用户使用 del 关键字删除某个属性时,解释器就会自动调用魔术方法 __delattr__ 因此,与 __setattr__ 一样,__delattr__ 方法的实现也必须格外注意无限递归的产生。
__getattribute__(self, name)
既然有 __setiem__ 与 __delitem__ 这样每一次设置、删除操作都会回调的魔术方法,当然也有每一次访问属性都会回调的魔术方法 — __getattribute__ 但是正如我们上面所说,绝大部分情况下 __getattr__ 与 __setattr__ 搭配就可以实现对类属性的绝对控制,其实是无需实现 __getattribute__ 方法,事实上,去主动实现 __getattribute__ 方法也是不建议的,因为这太容易造成无限递归。
https://www.cnblogs.com/pyxiaomangshe/p/7927540.html。 https://stackoverflow.com/questions/1436703/difference-between-str-and-repr。 https://stackoverflow.com/questions/38261126/python-2-missing-method。 https://www.cnblogs.com/suntp/p/6445286.html。 https://blog.csdn.net/qq\_27825451/article/details/81358074。