python 魔术方法(一) 自定义容器类与类属性控制

2019-04-06 18:56:38   最后更新: 2019-04-06 18:58:47   访问数量:158




上一篇文章中,我们介绍了 Python 面向对象编程及对象的继承和派生

python 面向对象与类及类属性

python 的继承与派生

本文我们来介绍 Python 解释器提供的魔术方法

 

在面向对象编程中,我们介绍了 __init__ 方法,这是由解释器默认实现,在构造对象是自动调用的特殊方法,类似的,Python 提供了一系列左右两边被一对双下划线包着的方法,这些方法被称为“魔术方法”,让我们方便的实现 Python 的核心需要特性,让你的类使用更加方便:

  • 迭代器
  • 集合类
  • 属性访问
  • 运算符重载
  • 函数和方法调用
  • 对象创建和销毁
  • 字符串表示形式和格式化
  • 运行上下文管理

 

同时,实现这些魔术方法后,大量 Python 标准库中的方法将可以直接用于你的类

特殊方法是 Python 解释器自动调用的,因此你无需自己处理,但是,需要注意的是,这些特殊方法是如此强大,同时也存在着很多的陷阱,在使用中必须处处小心谨慎

 

 

 

获取元素 -- __getitem__

__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 操作

in 操作也同样可以:

>>> Card('Q', 'hearts') in deck True >>> Card('7', 'beasts') in deck False

 

 

获取容量 -- __len__

__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__ 与 __delitem__

__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__ 与 __reversed__

__iter__(self) __reversed__(self)

 

 

定义 __getitem__ 以后,对象已经可以被循环迭代,但更好的方式是通过 __iter__ 方法返回迭代器来支持迭代

for x in containers 等方式的循环中,解释器会自动调用 __iter__ 方法获取迭代器进行迭代

而有时我们需要调用 python 的内建方法 reversed 来实现反向迭代,解释器就会自动调用 __reversed__ 方法

虽然上文提到,通过 __getitem__ 方法就可以实现上述功能,但迭代器会让这一过程的效率更高

 

容器元素的包含 -- __contains__ 与 __missing__

__contains__

__contains__(self, item)

 

 

当判断元素 in 或者 not in 容器时,python 解释器会自动调用 __contains__ 方法

 

__missing__

__missing__(self, key)

 

 

如果你的类是一个继承自 dict 的字典类,并且你没有实现自己的 __getitem__ 方法,那么当默认的 __getitem__ 方法发现 key 不存在时,就会调用你的 __missing__ 方法了

但是,需要注意的是,如果你自己实现了 __getitem__ 方法,并且没有调用父类的 __getitem__ 方法,那 __missing__ 将永远都不会被调用

这有两种方法可以解决:

  • 显式调用
class DictSubclass(dict): def __getitem__(self, key): if key not in self.data: return self.__missing__(key) return self.data[key]

 

 

  • 使用父类 __getitem__
class DictSubclass(dict): def __getitem__(self, key): if key not in self.data: return super(DictSubclass , self).__getitem__(key) return self.data[key]

 

 

获取不存在的属性名 -- __getattr__

__getattr__(self, name)

 

 

通过类实例点属性名可以实现类属性的访问,但有时我们需要定义当属性名不存在时的行为,这时就需要实现魔术方法:__getattr__

这个方法只有在用户访问的类属性不存在时才会被调用,通常,你可以在实现的 __getattr__ 中做兜底操作或抛出异常,也可以结合 __setattr__ 方法实现对某个属性的彻底控制

 

设置属性 -- __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__

__delattr__(self, name)

 

 

每一次用户使用 del 关键字删除某个属性时,解释器就会自动调用魔术方法 __delattr__

因此,与 __setattr__ 一样,__delattr__ 方法的实现也必须格外注意无限递归的产生

 

属性访问 -- __getattribute__

__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

 






python            class      面向对象      object      魔术方法      特殊方法     


京ICP备15018585号