前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python 魔术方法(一) 自定义容器类与类属性控制

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

作者头像
用户3147702
发布2022-06-27 13:23:43
6040
发布2022-06-27 13:23:43
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

此前的文章中,我们介绍了 Python 面向对象编程及对象的继承和派生。 接下来的几篇文章,我们将详细介绍 Python 解释器提供的一系列特殊方法 -- 魔术方法。

2. 魔术方法

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

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

同时,实现这些魔术方法后,大量 Python 标准库中的方法将可以直接用于你的类。 特殊方法是 Python 解释器自动调用的,因此你无需自己处理,但是,需要注意的是,这些特殊方法是如此强大,同时也存在着很多的陷阱,在使用中必须处处小心谨慎。

3. 自定义容器类

3.1. 获取元素 — __getitem__

代码语言:javascript
复制
__getitem__(self, key)

对于容器来说,获取元素是最重要的操作,魔术方法 __getitem__就完成了这个工作,每当对对象通过[]操作符获取元素时,解释器都会自动调用该魔术方法。

代码语言:javascript
复制
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]

3.1.1. 索引与切片

我们建立了一个纸牌类,有了 __getitem__ 方法,我们就定义了索引操作,所有 dict 通过 [] 可以做的事,我们的纸牌类都可以做到:

代码语言:javascript
复制
>>> 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')]

3.1.2. 迭代

同时,我们的类也因此变得可以被迭代:

代码语言:javascript
复制
>>> for card in deck: 
...   print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')

或者反向迭代:

代码语言:javascript
复制
>>> for card in reversed(deck): 
...   print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')

3.1.3. in 操作

in 操作也同样可以:

代码语言:javascript
复制
>>> Card('Q', 'hearts') in deck
True
>>> Card('7', 'beasts') in deck
False

3.2. 获取容量 — __len__

代码语言:javascript
复制
__len__(self)

对于容器类,一个很重要的操作是获取容器中元素的数量 — len() 我们曾经介绍过 Python 对象的内存结构: python 的内存管理与垃圾收集

len() 方法被调用时,Python 会自动调用对象的 __len__ 方法。 对于内部类型,比如 list、dict、str、bytearray 等,__len__ 方法直接返回 PyVarObject 中的 ob_size 字段,而对于自定义类对象,你就需要去实现 __len__ 方法了。

代码语言:javascript
复制
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。

3.3. 元素的更改与删除 — __setitem__ 与 __delitem__

代码语言:javascript
复制
__setitem__(self, key, value)
__delitem__(self, key)

上面我们实现的容器类是不可变的,如果你想要改变或删除其中的元素就会报错:

TypeError: ’FrenchDeck’ object does not support item assignment

__setiem__ 与 __delitem__ 就是分别在更改容器元素值和删除元素时被自动调用的魔术方法。

代码语言:javascript
复制
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’)

3.4. 容器的迭代 — __iter__ 与 __reversed__

代码语言:javascript
复制
__iter__(self)
__reversed__(self)

定义 __getitem__ 以后,对象已经可以被循环迭代,但更好的方式是通过 __iter__ 方法返回迭代器来支持迭代。 for x in containers 等方式的循环中,解释器会自动调用 __iter__ 方法获取迭代器进行迭代。 而有时我们需要调用 python 的内建方法 reversed 来实现反向迭代,解释器就会自动调用 __reversed__ 方法。 虽然上文提到,通过 __getitem__ 方法就可以实现上述功能,但迭代器会让这一过程的效率更高。

3.5. 容器元素的包含 — __contains__ 与 __missing__

3.5.1. __contains__

代码语言:javascript
复制
__contains__(self, item)

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

3.5.2. __missing__

代码语言:javascript
复制
__missing__(self, key)

如果你的类是一个继承自 dict 的字典类,并且你没有实现自己的 __getitem__ 方法,那么当默认的 __getitem__ 方法发现 key 不存在时,就会调用你的 __missing__ 方法了。 但是,需要注意的是,如果你自己实现了 __getitem__ 方法,并且没有调用父类的 __getitem__ 方法,那 __missing__ 将永远都不会被调用。 这有两种方法可以解决:

显式调用

使用父类 __getitem__

4. 控制类属性的访问

4.1. 获取不存在的属性名 — __getattr__

代码语言:javascript
复制
__getattr__(self, name)

通过类实例点属性名可以实现类属性的访问,但有时我们需要定义当属性名不存在时的行为,这时就需要实现魔术方法:__getattr__ 这个方法只有在用户访问的类属性不存在时才会被调用,通常,你可以在实现的 __getattr__ 中做兜底操作或抛出异常,也可以结合 __setattr__ 方法实现对某个属性的彻底控制。

4.2. 设置属性 — __setattr__

代码语言:javascript
复制
__setattr__(self, key, value)

如果只实现 __getattr__,那你无法实现对属性的完全控制和封装,因为 Python 独特的语言特性,只要在类外为不存在的属性赋值,改属性就会被创建,而 __getattr__ 只有在属性不存在的情况下才会被调用,此时,如果你需要定义独特的某个属性的行为,或彻底隐藏某个属性,就必须实现 __setattr__ 方法。 __setattr__ 方法会在每一次用户为某个属性赋值时被调用,因此要格外防范无限递归的产生:

代码语言:javascript
复制
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 成员尚未被创建,因此抛出了异常。

改成下面这样即可:

代码语言:javascript
复制
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’}

4.3. 删除属性 — __delattr__

代码语言:javascript
复制
__delattr__(self, name)

每一次用户使用 del 关键字删除某个属性时,解释器就会自动调用魔术方法 __delattr__ 因此,与 __setattr__ 一样,__delattr__ 方法的实现也必须格外注意无限递归的产生。

4.4. 属性访问 — __getattribute__

代码语言:javascript
复制
__getattribute__(self, name)

既然有 __setiem__ 与 __delitem__ 这样每一次设置、删除操作都会回调的魔术方法,当然也有每一次访问属性都会回调的魔术方法 — __getattribute__ 但是正如我们上面所说,绝大部分情况下 __getattr__ 与 __setattr__ 搭配就可以实现对类属性的绝对控制,其实是无需实现 __getattribute__ 方法,事实上,去主动实现 __getattribute__ 方法也是不建议的,因为这太容易造成无限递归。

5. 参考资料

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。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-04-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. 魔术方法
  • 3. 自定义容器类
    • 3.1. 获取元素 — __getitem__
      • 3.1.1. 索引与切片
      • 3.1.2. 迭代
      • 3.1.3. in 操作
    • 3.2. 获取容量 — __len__
      • 3.3. 元素的更改与删除 — __setitem__ 与 __delitem__
        • 3.4. 容器的迭代 — __iter__ 与 __reversed__
          • 3.5. 容器元素的包含 — __contains__ 与 __missing__
            • 3.5.1. __contains__
            • 3.5.2. __missing__
        • 4. 控制类属性的访问
          • 4.1. 获取不存在的属性名 — __getattr__
            • 4.2. 设置属性 — __setattr__
              • 4.3. 删除属性 — __delattr__
                • 4.4. 属性访问 — __getattribute__
                • 5. 参考资料
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档