Python 有哪些黑魔法?
Python 中有很多黑魔法,比如 metaclass。对于这些语言特性有两种极端的观点。
一种人觉得这些语言特性太牛了,简直是无所不能的阿拉丁神灯,必须找机会用上才能显示自己的 Python 实力。另一种观点则是认为这些语言特性太危险了,会蛊惑人心去滥用,一旦打开就会释放“恶魔”,让整个代码库变得难以维护。
meta-class 的 meta 这个词根,起源于希腊语词汇 meta,包含下面两种意思:“Beyond”,例如技术词汇 metadata,意思是描述数据的超越数据;“Change”,例如技术词汇 metamorphosis,意思是改变的形态。
metaclass,一如其名,实际上同时包含了“超越类”和“变形类”的含义,完全不是“基本类”的意思。所以,要深入理解 metaclass,我们就要围绕它的超越变形特性。接下来,我将为你展开 metaclass 的超越变形能力,讲清楚 metaclass 究竟有什么用?怎么应用?Python 语言设计层面是如何实现 metaclass 的 ?以及使用 metaclass 的风险。
metaclass 的超越变形特性有什么用?YAML 是一个家喻户晓的 Python 工具,可以方便地序列化/逆序列化结构数据。YAMLObject 的一个超越变形能力,就是它的任意子类支持序列化和反序列化(serialization & deserialization)。比如说下面这段代码:
class Monster(yaml.YAMLObject):
yaml_tag = u'!Monster'
def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks
def __repr__(self):
return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % (
self.__class__.__name__, self.name, self.hp, self.ac,
self.attacks)
yaml.load("""
--- !Monster
name: Cave spider
hp: [2,6] # 2d6
ac: 16
attacks: [BITE, HURT]
""")
Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
print yaml.dump(Monster(
name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']))
# 输出
!Monster
ac: 16
attacks: [BITE, HURT]
hp: [3, 6]
name: Cave li
这里 YAMLObject 的特异功能体现在哪里呢?调用统一的 yaml.load(),就能把任意一个 yaml 序列载入成一个 Python Object;而调用统一的 yaml.dump(),就能把一个 YAMLObject 子类序列化。对于 load() 和 dump() 的使用者来说,它们完全不需要提前知道任何类型信息,这让超动态配置编程成了可能。
在我的实战经验中,许多大型项目都需要应用这种超动态配置的理念。
比方说,在一个智能语音助手的大型项目中,我们有 1 万个语音对话场景,每一个场景都是不同团队开发的。作为智能语音助手的核心团队成员,我不可能去了解每个子场景的实现细节。
在动态配置实验不同场景时,经常是今天我要实验场景 A 和 B 的配置,明天实验 B 和 C 的配置,光配置文件就有几万行量级,工作量不可谓不小。而应用这样的动态配置理念,我就可以让引擎根据我的文本配置文件,动态加载所需要的 Python 类。
对于 YAML 的使用者,这一点也很方便,你只要简单地继承 yaml.YAMLObject,就能让你的 Python Object 具有序列化和逆序列化能力。是不是相比普通 Python 类,有一点“变态”,有一点“超越”?
Python 底层语言设计层面是如何实现 metaclass 的?metaclass 能够拦截 Python 类的定义。它是怎么做到的?要理解 metaclass 的底层原理,你需要深入理解 Python 类型模型。下面,我将分三点来说明。
第一,所有的 Python 的用户定义类,都是 type 这个类的实例。可能会让你惊讶,事实上,类本身不过是一个名为 type 类的实例。在 Python 的类型世界里,type 这个类就是造物的上帝。这可以在代码中验证:
# Python 3和Python 2类似
class MyClass:
pass
instance = MyClass()
type(instance)
# 输出
<class '__main__.C'>
type(MyClass)
# 输出
<class 'type'>
instance 是 MyClass 的实例,而 MyClass 不过是“上帝”type 的
第二,用户自定义类,只不过是 type 类的 __call__ 运算符重载。当我们定义一个类的语句结束时,真正发生的情况是 Python 调用 type 的 __call__ 运算符。简单来说,当你定义一个类,写成下面这样时:
class MyClass:
data = 1
Python 真正执行的是下面这段代码:
class = type(classname, superclasses, attributedict)
这里等号右边的 type(classname, superclasses, attributedict),就是 type 的 __call__ 运算符重载,它会进一步调用:
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
这一切都可以通过代码验证,比如下面这段代码示例:
class MyClass:
data = 1
instance = MyClass()
MyClass, instance
# 输出
(__main__.MyClass, <__main__.MyClass instance at 0x7fe4f0b00ab8>)
instance.data
# 输出
1
MyClass = type('MyClass', (), {'data': 1})
instance = MyClass()
MyClass, instance
# 输出
(__main__.MyClass, <__main__.MyClass at 0x7fe4f0aea5d0>)
instance.data
# 输出
第三,metaclass 是 type 的子类,通过替换 type 的 __call__ 运算符重载机制,“超越变形”正常的类。
其实,理解了以上几点,我们就会明白,正是 Python 的类创建机制,给了 metaclass 大展身手的机会。一旦你把一个类型 MyClass 的 metaclass 设置成 MyMeta,MyClass 就不再由原生的 type 创建,而是会调用 MyMeta 的 __call__ 运算符重载。
class = type(classname, superclasses, attributedict)
# 变为了
class = MyMeta(classname, superclasses, attributedict)
所以才能在上面 YAML 的例子中,利用 YAMLObjectMetaclass 的 __init__ 方法,为所有 YAMLObject 子类偷偷执行 add_constructor()。
不过,凡事有利必有弊,尤其是 metaclass 这样“逆天”的存在。正如你所看到的那样,metaclass 会“扭曲变形”正常的 Python 类型模型。所以,如果使用不慎,对于整个代码库造成的风险是不可估量的。
换句话说,metaclass 仅仅是给小部分 Python 开发者,在开发框架层面的 Python 库时使用的。而在应用层,metaclass 往往不是很好的选择。
通过解读 YAML 的源码,围绕 metaclass 的设计本意“超越变形”,解析了 metaclass 的使用场景和使用方法。metaclass 是 Python 黑魔法级别的语言特性。天堂和地狱只有一步之遥,你使用好 metaclass,可以实现像 YAML 那样神奇的特性;而使用不好,可能就会打开潘多拉魔盒了。
文章的内容,一方面是帮助有需要的同学,深入理解 metaclass,更好地掌握和应用;另一方面,也是对初学者的科普和警告:不要轻易尝试 metaclass。