元编程相关博文的目录及链接
这篇博文是元编程系列博文中的其中一篇、这个系列中其他博文的目录和连接见下:
- 使用 python 特性管理实例属性
- 浅析 python 属性描述符(上)
- 浅析 python 属性描述符(下)
- python 导入时与运行时
- python 元编程之动态属性
- python 元编程之类元编程
前言
元编程是一门程序运行时动态创建属性/类的技术,本篇文章主要讲述创建动态属性,也可以说是属性元编程技术。
元编程这个词汇看起来很高大上,但是在生产环境中用的也不少,而所谓的属性元编程实际上就是在程序运行时为实例动态增加属性。
这样一来,使用特性(property)管理实例属性其实也算是一类属性元编程,当用户访问实例本不存在的属性时,python 解释器从类中搜索有无特性(类属性承载),若有则会调用特性的相关接口并动态创建/赋给真实的实例属性。
同理,属性描述符也是一类属性元编程技术。
然而、在解释器从类和超类搜索不到相应的特性/属性描述符时,python 也提供了相应的接口来处理找不到的属性,这就为我们提供了实现属性元编程的方法。
特性能够动态创建实例属性
如果你读过系列文章第一篇,那么一定了解了 python 特性的概念以及如何使用,如果你读过系列文章的第二篇,那么一定了解 python 特性的本质,以及其在何时使用。
接下来我会用一个例子来说明特性如何动态创建实例属性。
还是 Fruits 类的例子,只不过这次为了更好的展示特性如何动态创建实例属性,有一些逻辑上的错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> class Fruits:
def __init__(self):
pass
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, value):
if value >= 0:
self.__weight = value
else:
raise ValueError("想干嘛呢?")
>>> apple = Fruits()
>>> vars(apple) # 1
{}
>>> apple.weight = 2 # 2
>>> vars(apple) # 3
{'_Fruits__weight': 2}
>>> apple.weight = -1
Traceback (most recent call last):
...
ValueError: 想干嘛呢?
解释:
- 通过 vars() 查看实例的属性、因为我让
__init__
方法为空,所以 apple 实例没有任何属性。 - 直接为实例的 weight 属性赋值,这里实际上会调用 Fruit 类 weight 特性的 “set” 方法。
- 赋值后再次查看 apple 实例的变量,这一次 apple 实例却有属性了还是私有属性
_Fruits__weight
。
在上例中,直接 pass 掉
__init__
是逻辑错误的,但是却能够帮助我们看清特性是如何动态创建实例属性的。
重点是、在运行时解释器为 apple 实例动态创建了一个私有属性 _Fruits__weight
并能对其的设值进行动态验证。
如果你读过使用 python 特性管理实例属性,你应该会知道在使用 obj.attr = value
语句时解释器会去类中搜索有无特性/覆盖型属性描述符,有特性/覆盖型属性描述符时会调用它们的接口、若都没有,才会去搜索实例的属性字典。
实际上、这个过程是由 python 提供的一对 magic 方法 —— __getattribute__
和 __setattr__
实现的,也就是说语句 obj.attr
或 getattr(obj,attr)
无论如何也会调用 obj 的 __getattribute__(self,name)
,语句 obj.attr = value
无论如何也会调用 obj 的 __setattr__(self,name,value)
,并由它们来进行寻找特性/属性描述符的工作。
而当实例没有该属性,所属的类也没有对应的特性/属性描述符时,__getattribute__(self,name)
方法不会直接抛出异常,而是会调用另一个 magic 方法 __getattr__(self,name)
。
使用 __getattr__
创建属性
现在,水果店老板给了你一个水果列表,包括水果的名字和该水果对应的描述,这个列表有数百种水果。麻烦的是,产品经理小姐姐提出了一个奇怪的需求:要求你必须使用 “obj.某水果的名字” 的形式来获取该水果的描述,而不能使用字典的形式 dict[“某水果的名字”] 获取。
在小姐姐向你抛了一个媚眼后,你答应了她,并写下了如下代码。(๑´ㅂ`๑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> class FruitsList:
def __init__(self, fdict):
self.fruit_dict = fdict
def __getattr__(self, item):
return self.fruit_dict[item]
>>> fdict = {"apple": "apple description", "pear" : "pear description", "banana" : "banana description"} # 1
>>> fruitlist = FruitsList(fdict) # 2
>>> vars(fruitlist) # 3
{'fruit_dict': {'apple': 'apple description', 'pear': 'pear description', 'banana': 'banana description'}}
>>> fruitlist.apple # 4
apple description
>>> fruitlist.pear # 5
pear description
>>> vars(fruitlist) # 6
{'fruit_dict': {'apple': 'apple description', 'pear': 'pear description', 'banana': 'banana description'}}
解释:
- fdict 是老板给的水果数据的模拟测试数据,只包含三种水果。
- 使用 fdict 构建 FruitsList 实例。
- 查看 fruitslist 实例的属性、发现其只有一个属性 —— fruit_dict 。
- 访问实例的 apple 属性,并成功拿到其值。
- 访问实例的 pear 属性,并成功拿到其值。
- 此时 fruitslist 实例还是只有一个属性。
上面这个例子表明了解释器在实例没有对应属性、实例所属的类没有对应特性/属性描述符时,python 解释器会如何处理实例属性的访问 —— 调用 __getattr__(self,name)
特殊方法。
这个例子中,在访问 fruitlist 实例不存在的属性时,解释器动态返回了值,看上去就好像运行时依据 fruit_dict 动态为 fruitlist 实例创建了属性一样。没错,使用 __getattr__(self,name)
很容易就实现了属性元编程。
当有数百种水果都需要用像访问实例属性的方式来访问时,我们不可能为 FruitList 类手动创建数百种水果属性或者特性,显然使用 __getattr__(self,name)
相对合理。
产品经理小姐姐提出的需求并不合理、但是,日常生产中的确有类似的应用场景,例如,当 python 加载如下的 json 格式的数据时,我们想要拿到最里层 “d1” 的值,通常使用字典访问:data["a"]["d"][0]["d1"]
,这很麻烦。
1
2
3
4
5
6
7
{
"a":{
"b":[{"b1":"bv"}],
"c":[{"c1":"cv"}],
"d":[{"d1":"dv"}]
}
}
但是,当我们合理编写 __getattr__(self,name)
magic 方法,我们就能实现 data.a.b.d[0].d1
这样的访问方式,少了一堆中括号和双引号看上去会舒服得多,这就是动态属性访问的魅力。
使用动态属性访问 JSON 类数据的代码有很多(网上有大量实现),不过《流畅的 python》 第 19 章有一个实现相对简单,这里是源码 -> FrozenJSON
使用 __dict__
快速创建属性
对于上面这个例子来讲,还有一个简单的方法能够在运行时的动态为 fruitlist 创建属性 —— 使用 __dict__
特殊属性。
将上述代码重构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class FruitsList:
def __init__(self, fdict):
self.__dict__.update(fdict) # 1
>>> fdict = {"apple": "apple description", "pear" : "pear description", "banana" : "banana description"}
>>> fruitlist = FruitsList(fdict) # 2
>>> vars(fruitlist) # 3
{'apple': 'apple description', 'pear': 'pear description', 'banana': 'banana description'}
>>> fruitlist.apple # 4
apple description
>>> fruitlist.pear
pear description
解释:
- FruitsList 类的构造方法将传进来的参数都放进实例的
__dict__
中。 - 使用 fdict 构造实例。
- 查看 fruitlist 实例的属性,发现此时实例属性已经更新,其拥有了 fdict 里的所有数据。
- 访问实例的 apple 属性,发现可以拿到正确的数据。
在 python 中,__dict__
是存储着对象或者类的可写属性。有 __dict__
属性的对象任何时候都能随意设置新属性值。如果类有 __slots__
属性,那么它的实例可能就没有 __dict__
属性。
类可以定义
__slots__
属性,限制实例能够有哪些属性。__slots__
属性的值是一个字符串组成的元组,指明允许有的属性。
End
总的来说、python 里动态创建属性难度不大、除了使用特性和属性描述符在解释器调用 __getattribute__(self,name)
阶段动态创建属性、我们还可以在解释器调用 __getattr__(self,name)
阶段动态创建属性,甚至,我们能够直接使用 __dict__
属性直接批量动态创建实例属性。
但是使用 __dict__
不太安全,因为你永远不知道用户传递的数据是怎样的,规范与否,字典嵌套了几层等都不是我们能控制的,所以我更推荐使用像 FrozenJSON 这样的处理方式,自己在 __getattr__(self,name)
里编写控制代码总是要安心的多。
除了动态创建属性,python 甚至可以动态创建/修改类,这就涉及到类的类 —— 元类。