魔术方法 (Magic Methods)

奇特的存在

关于 Magic Methods,课程的前面内容也在不断提及,它是一个比较奇特的存在。很多学习 Python 的人,甚至已经到了『用起来』的程度,也有可能会不清楚它。而当你更加深入地理解 Python,或将 Python 用于可以称之为 产品 的项目中,那么 Magic Methods 就会自然地浮现到眼前。
除了 Magic Methods 之外,后面章节会介绍的 修饰器 (Decorator) 也是差不多奇特的存在。
Magic Methods 相对 Decorator 而言,更常见,比如我们定义一个 Class 时候,用到的 __init__ 就是 Magic Methods 之一,基本上可以认为 __??__ 这种由 __ 头尾包裹的,都是 Magic Methods。

本节内容,我们将以 如何获取对象的属性 为例,介绍 Magic Methods 的用法。
关于 Magic Methods 更多的内容,你可以通过搜索引擎了解。

如何获取对象的属性?

我们在前面的《赋值与实例化》中,已经见过了 先赋值后获取 的例子,这是一件很自然的事情。但实际上,一个 子属性 的获取有好几个步骤, 先赋值后获取 只是其中一个常见的而已。
简单的来说,获取对象上的子属性,按照下面的(常见的)顺序走,命中了就返回对应的值:

  1. __getattribute__: 没有特殊情况下,不要重写这个函数!!
  2. __dict__ 上是否存在,某种角度来看相当于 缓存 的存在,它不是一个函数,是一个字典类型
  3. __getattr__: 之前都没有命中,再走这个函数

__getattribute__

没有特别的情况,不要重写__getattribute__ ,如果你不清楚它的副作用,又启用它的逻辑,那基本会把当前对象直接废掉
我们先构建一个名为 AttributeCaution 的Class,里面定义 __getattribute__,不返回值 (也就相当于返回 None),然后,我们将其实例化,获得一个变量对象attribute_caution_object,先给这个对象设定一个属性 my_key,然后再尝试读取这个属性,会失败。尝试去获取 __dict__ 这种属性,也失败了。甚至尝试 dir(attribute_caution_object) 都不能如意……
参考代码如下:

>>> class AttributeCaution(object):
...     def __getattribute__(self, item):
...         print('__getattribute__ for %s' % item)
...
>>> attribute_caution_object = AttributeCaution()
>>> attribute_caution_object.my_key = 'hello world'
>>> print(attribute_caution_object.my_key)
__getattribute__ for my_key
None
>>> print(attribute_caution_object.__dict__)
__getattribute__ for __dict__
None
>>> dir(attribute_caution_object)
__getattribute__ for __dict__
__getattribute__ for __class__
[]

__getattr__

__getattr__ 相比 __getattribute__ 就温和了很多,可以简单理解为:其它方法都尝试了,仍然没有获得对象的属性,然后 __getattr__ 才会参与进来。

我们直接看代码吧:

import random
class TheClass(object):
    def __init__(self):
        self._my_value = 123
    def __getattr__(self, item):
        # 返回个随机数吧...
        return random.random()

然后,实例化这个 Class,并调用数据对象上的任意属性:

>>> the_value = TheClass()
>>> print(the_value.a)
0.4903098356821243
>>> print(the_value.b)
0.3099720782643004
>>> print(the_value.c)
0.48459837977023346

Magic Methods 常被当做 Python 的 黑魔法,它对我们既有认知产生了冲击,但它又不是为了 ,其存在或者是为了 Python 程序结构的扩充、或者是为了让写代码的人更加轻松。
就 Magic Methods 的实际效果而言,是不是感觉可以为所欲为了?
如果隐约有了这种感觉, 那么就要特别注意了 ! 比如上面的示例中,任意属性都可以获取,真的是我们使用 __getattr__ 的本意吗?是否会因此而导致潜在的、不必要的错误呢?
有多大的黑魔法,也会产生多大的副作用,所以在使用 Magic Methods 的时候,务必要多一分谨慎。


我们再把示例改造一下,允许属性名hello 开始的才返回值,其它的时候,则正常地抛出错误。这还算常见的场景,一些属性名动态的,不调用的时候不运行,调用的时候根据属性名不同计算出对应的结果,不在允许的属性范围内则抛出错误。

import random
class TheClass(object):
    def __init__(self):
        self._my_value = 123
    def __getattr__(self, item):
        if item.startswith('hello'):
            return random.random()
        else:
            raise AttributeError('has no attribute `%s`'%item)

进行实际的调用:

>>> the_value = TheClass()
>>> print(the_value.hello)
0.9034638124366223
>>> print(the_value.hello2)
0.6064794626384964

如果遇到不支持的属性,则抛出错误:

>>> print(the_value.a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __getattr__
AttributeError: has no attribute `a`

AttributeError 是一个常规错误,如果没有 __getattr__ 动态对应,原本不存在的属性被调用的时候,也会抛出这个错误。

这个示例也在另外一个方面,提醒我们: 当重写继承 某个函数的时候,务必要判断,是否有必要沿用原有的部分逻辑
比如 AttributeError 就是一个原有的逻辑,只是 class TheClass(object) 的时候,容易忽略它的存在。有些逻辑,因为很常用,自己用着的过程中没有明显感觉到它的存在,而一旦重写了某个方法(函数),这个 习以为常 的东西可能就不工作了,需要自己手工重新加回来。
如果 __getattr__ 写成下面的逻辑,没有主动触发 AttributeError,那么最终不以 hello 开头的属性,也都会返回一个 None 值,就不是我们所期望的了。

def __getattr__(self, item):
    if item.startswith('hello'):
        return random.random()

__dict__

这就不做特别解释了,之前《赋值与实例化》中也有相应代码的示例出现。简单的可以把它理解为往一个对象上赋值,其最终存储数据的容器。