关于 Magic Methods
,课程的前面内容也在不断提及,它是一个比较奇特的存在。很多学习 Python 的人,甚至已经到了『用起来』的程度,也有可能会不清楚它。而当你更加深入地理解 Python,或将 Python 用于可以称之为 产品
的项目中,那么 Magic Methods 就会自然地浮现到眼前。
除了 Magic Methods
之外,后面章节会介绍的 修饰器 (Decorator)
也是差不多奇特的存在。
Magic Methods 相对 Decorator 而言,更常见,比如我们定义一个 Class 时候,用到的 __init__
就是 Magic Methods 之一,基本上可以认为 __??__
这种由 __
头尾包裹的,都是 Magic Methods。
本节内容,我们将以 如何获取对象的属性
为例,介绍 Magic Methods 的用法。
我们在前面的《赋值与实例化》中,已经见过了 先赋值后获取
的例子,这是一件很自然的事情。但实际上,一个 子属性
的获取有好几个步骤, 先赋值后获取
只是其中一个常见的而已。
简单的来说,获取对象上的子属性
,按照下面的(常见的)顺序走,命中了就返回对应的值:
缓存
的存在,它不是一个函数,是一个字典类型
没有特别的情况,不要重写__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__
相比 __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()
这就不做特别解释了,之前《赋值与实例化》中也有相应代码的示例出现。简单的可以把它理解为往一个对象上赋值,其最终存储数据的容器。