二十一、类元编程
(元类)是深奥的知识,99% 的用户都无需关注。如果你想知道是否需要使用元类,我告诉你,不需要(真正需要使用元类的人确信他们需要,无需解释原因)。
——Tim Peters
Timsort 算法的发明者,活跃的 Python 贡献者
类元编程是指在运行时创建或定制类的技艺。
在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,而无需使用 class 关键字。类装饰器也是函数,不过能够审查、修改,甚至把被装饰的类替换成其他类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,例如我们常见的抽象基类。
作者:
这是一个令人兴奋的话题,很容易让人忘乎所以。因此,进入本章的正文之前,我必须告诫你:
除非开发框架,否则不要编写元类——然而,为了寻找乐趣,或者练习相关的概念,可以这么做。
类工厂函数
类工厂函数——collections.namedtuple
我们把 类名 ,几个属性名 传给这个函数,会创建一个 tuple 子类。
假设编写一个宠物店应用程序,想要把狗的数据当做记录处理。如下令人厌烦:
class Dog:
def __init__(self, name, weight, owner):
self.name = name
self.weight = weight
self.owner = owner
各个字段名称出现了三次,样板代码,字符串表示形式不友好。
In [1]: def record_factory(cls_name, field_names):
...: try: # 鸭子类型:假定可以以逗号或空格分隔,失败则一个元素对应一个属性名
...: field_names = field_names.replace(',',' ').split()
...: except AttributeError:
...: pass
# 设定了拆包和字符串表示形式的字段顺序,构建类的 __slots__ 方法
# 记录类:属性是固定的几个,且顺序相同
# __slots__ 属性的主要特色是节省内存,能处理数百万个实例
...: field_names = tuple(field_names)
...: def __init__(self, *args, **kwargs): # 构建类的 __init__ 方法
...: attrs = dict(zip(self.__slots__,args))
...: attrs.update(kwargs)
...: for name,value in attrs.items():
...: setattr(self,name,value)
...: def __iter__(self): # 构建类产生对象是可迭代的
...: for name in self.__slots__:
...: yield getattr(self,name)
...: def __repr__(self): # 构建类的字符串表示形式
...: values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__,self))
...: return '{}({})'.format(self.__class__.__name__,values)
# 构建类的属性字典
...: cls_attrs = dict(__slots__=field_names,__init__=__init__,__iter__=__iter__,__repr__=__repr__) # type 构造方法,构建新类,返回构建类
...: return type(cls_name,(object,),cls_attrs)
通常 type
是查看对象所属类的函数。作用与 obj.__class__
相同。
然而,type
是一个类。当成类使用时,传入三个参数可以构建一个类。
type
参数:类名,基类,名称空间(类的属性和方法)
MyClass = type('MyClass', (MySuperClass, MyMixin), {'x': 42, 'x2': lambda self: self.x * 2})
# 一个意思
class MyClass(MySuperClass, MyMixin):
x = 42
def x2(self):
return self.x * 2
In [5]: type(object)
Out[5]: type
In [6]: object.__class__
Out[6]: type
如果查看 collections.namedtuple 函数的源码(https://hg.python.org/cpython/file/3.4/Lib/collections/init.py#l236),你会发现另一种方式:先声明一个 _class_template 变量,其值是字符串形式的源码模板;然后在 namedtuple 函数中调用 _class_template.format(...) 方法,填充模板里的空白;最后,使用内置的 exec 函数计算得到的源码字符串。
在 Python 中做元编程时,最好不用 exec 和 eval 函数。如果接收的字符串(或片段)来自不可信的源,那么这两个函数会带来严重的安全风险。Python 提供了充足的内省工具,大多数时候都不需要使用 exec 和 eval 函数。然而,Python 核心开发者实现 namedtuple 函数时选择了使用 exec 函数,这样做是为了让生成的类代码能通过 ._source 属性(https://docs.python.org/3/library/collections.html#collections.somenamedtuple._source)获取。
record_factory 函数创建的类,其实例有个局限——不能序列化,即不能使用 pickle 模块里的 dump/load 函数处理。这个示例是为了说明如何使用 type 类满足简单的需求,因此不会解决这个问题。如果想了解完整的方案,请分析 collections.nameduple 函数的源码(https://hg.python.org/cpython/file/3.4/Lib/collections/init.py#l236),搜索“pickling”这个词。
定制描述符的类装饰器
使用类装饰器
In [40]: def entity(cls):
...: for k,v in cls.__dict__.items():
...: if isinstance(v,A):
...: type_name = type(v).__name__
...: v.storage_name = '_{}#{}'.format(type_name, k)
...: print('EEE')
...: return cls
...:
In [41]:
In [41]: @entity # 类装饰器,类和函数具有相似的行为,装饰了类的装饰器就是类装饰器
...: class L:
...: d = A()
...: e = A()
...: print('LLL')
...: def __init__(self,des,w,p):
...: self.des = des
...: self.d = w
...: self.p = p
...: def subtotal(self):
...: return self.d * self.p
...: print('LLLend')
...:
LLL
LLLend
EEE
# 从结果看出,是先定义完成类,直接运行了装饰器
# 创建类时定制类
In [42]: class A:
...: __counter = 0
...: def __init__(self):
...: cls = self.__class__
...: prefix = cls.__name__
...: index = cls.__counter
...: self.storage_name = '_{}#{}'.format(prefix,index)
...: cls.__counter += 1
...: def __get__(self,instance,owner):
...: if instance is None:
...: return self
...: else:
...: return getattr(instance,self.storage_name)
...: def __set__(self,instance,value):
...: setattr(instance,self.storage_name,value)
类装饰器有个重大缺点:只对直接依附的类有效。这意味着,被装饰的类的子类可能继承也可能不继承装饰器所做的改动,具体情况视改动的方式而定。
导入时和运行时比较
导入时,解释器会从上到下一次性解释完 .py 模块的源码,然后生成用于执行的字节码。如果句法有错误,就在此时报告。如果本地的 __pycache__
文件夹中有最新的 .pyc 文件,解释器会跳过上述步骤,因为已经有运行所需的字节码了。
编译肯定是导入时的活动,不过还会做其他事。因为Python中的语句几乎都是可执行的,也就是说语句可能会运行用户代码,修改用户程序状态。尤其是 import 语句,它不只是声明,在进程中首次导入模块时,还会运行所导入模块中的全部顶层代码——以后导入相同模块则使用缓存,制作名称绑定。那些顶层代码可以做任何事,包括通常在 运行时 做的事,例如连接数据库。因此导入时与运行时之间的界限是模糊的:import 语句可以出发任何 运行时 行为。
java 中 import 仅仅是声明,告知编译器需要特定的包。
导入时,解释器会执行顶层的 def 语句,解释器会编译函数的定义体(首次导入模块时),把函数对象绑定到对应的全局名称上,但是显然解释器不会执行函数的定义体。通常这意味着解释器在导入时定义顶层函数,但是仅当在运行时调用函数才会执行函数的定义体。
对类来说,在导入时,解释器会执行每个类的定义体,甚至汇之星嵌套类的定义体。执行类的定义体的结果是,定义了类的属性和方法,并构建了类对象。从这个意义上理解,类的定义体属于顶层代码,因为它在导入时执行。
In [44]: def b(func):
...: print('bbb')
...: return func
...:
# 结果表明,函数定义体没有执行
In [45]: @b
...: def aaa():
...: print('aaa')
...:
bbb
# 结果表明,函数定义体并没有执行,只执行了装饰器的函数体,因为调用了装饰器函数
In [46]: class aaa:
...: print('aaa')
...: def __init__(self):
...: print('aaainit')
...:
aaa
# 结果表明,定义类的时候执行了类的定义体
In [47]: @b
...: class aaa:
...: print('aaa')
...: def __init__(self):
...: print('aaainit')
...:
aaa
bbb
# 结果表明,先执行了类定义体,后执行了装饰器定义体
In [48]: class aaa:
...: print('aaa')
...: class bbb:
...: print('bbb')
...:
aaa
bbb
# 结果表明,嵌套类执行了定义体。一个类被定义在另一个类中,这样的类就称为嵌套类。
导入代码会运行顶层代码,通常包括全局代码,类定义体,装饰器
运行代码一行一行执行,包括 if __name__ == '__main__'
代码块后面有代码,也是一行一行执行的,运行到哪一行执行哪一行,执行了代码块,在执行剩下的代码。
类装饰器一般对子类不起作用。
元类基础知识
元类是构建类的类
默认情况下,Python中的类是 type类的实例。
In [1]: str.__class__
Out[1]: type
In [2]: type.__class__
Out[2]: type
In [3]: type.__mro__ # type 是 object 的子类
Out[3]: (type, object)
In [4]: str.__mro__
Out[4]: (str, object)
In [6]: object.__class__ # object 是 type 的实例
Out[6]: type
ABCMeta 元类所属的类也是 type。所有类都直接或间接是 type 的实例。元类同时也是 type 的子类。
In [7]: import collections
In [8]: collections.Iterable.__class__ # Iterable 类 是 ABCMeta 的实例
Out[8]: abc.ABCMeta # 元类
In [14]: abc.ABCMeta.__class__
Out[14]: type
In [15]: abc.ABCMeta.__mro__
Out[15]: (abc.ABCMeta, type, object)
In [20]: collections.Iterable.__mro__
Out[20]: (collections.abc.Iterable, object)
In [21]: type.__class__
Out[21]: type
In [22]: type.__mro__
Out[22]: (type, object)
In [23]: object.__class__
Out[23]: type
所有类都是type的实例,元类是type的子类。
evalsupport.py
print('<[100]> evalsupport module start')
def deco_alpha(cls): # 装饰器
print('<[200]> deco_alpha')
def inner_1(self):
print('<[300]> deco_alpha:inner_1')
cls.method_y = inner_1
return cls
class MetaAleph(type): # 元类
print('<[400]> MetaAleph body')
def __init__(cls, name, bases, dic):
'''
cls: 这是要初始化的类对象(例如 ClassFive)。
name、bases、dic
与构建类时传给 type 的参数一样。
'''
print('<[500]> MetaAleph.__init__')
def inner_2(self):
print('<[600]> MetaAleph.__init__:inner_2')
cls.method_z = inner_2
print('<[700]> evalsupport module end')
class ClassFive(metaclass=MetaAleph):
print('<[6]> ClassFive body')
def __init__(self):
print('<[7]> ClassFive.__init__')
def method_z(self):
print('<[8]> ClassFive.method_z')
导入 from aaa import bbb
也会执行所有 aaa文件中的顶层代码
定义类产生元类对象,调用元类的 __init__
方法
定制描述符的元类
用户的类的元类为带有校验描述符功能的元类,但是,如果用户完全不用知道描述符或元类,直接继承类比较好,即可以间接设定元类,实际用户继承的是元类是自定义元类的类。
In [29]: class Validated:
...: __counter = 0
...: def __init__(self):
...: cls = self.__class__
...: prefix = cls.__name__
...: index = cls.__counter
...: self.storage_name = '_{}#{}'.format(prefix,index)
...: cls.__counter += 1
...: def __get__(self,instance,owner):
...: if instance is None:
...: return self
...: return getattr(instance,self.storage_name)
...: def __set__(self,instance,value):
...: setattr(instance,self.storage_name,value)
...:
In [30]: class A_type(type):
...: def __init__(cls,name,bases,dic):
# 在超累 type 上调用 __init__ 方法
...: super().__init__(name,bases,dic)
...: for k , attr in dic.items():
...: if isinstance(attr, Validated):
...: type_name = type(attr).__name__
...: attr.storage_name = '_{}#{}'.format(type_name,k)
...:
...:
In [31]: class E(metaclass=A_type): # 封装元类细节,让用户使用起来和普通的类一样
...: """验证字段业务实体"""
...:
In [34]: class L(E):
...: des = Validated()
...: w = Validated()
...: p = Validated()
...: def __init__(self,des,w,p):
...: self.des = des
...: self.w = w
...: self.p = p
...: def subtotal(self):
...: return self.w*self.p
...:
In [35]: l = L('aaa',3,5)
In [36]: l.__dict__ # 发现描述符自动变成定义的属性了
Out[36]: {'_Validated#des': 'aaa', '_Validated#w': 3, '_Validated#p': 5}
In [37]: class L:
...: des = Validated()
...: w = Validated()
...: p = Validated()
...: def __init__(self,des,w,p):
...: self.des = des
...: self.w = w
...: self.p = p
...: def subtotal(self):
...: return self.w*self.p
...:
In [38]: l = L('aaa',3,5)
In [39]: l.__dict__ # 不继承 E 类,发现描述符还是保证不重复的数字
Out[39]: {'_Validated#6': 'aaa', '_Validated#7': 3, '_Validated#8': 5}
关于元类的写法
除了把类链接到元类上的句法之外 5,目前编写元类使用的句法在 Python 2.2(这个版本对 Python 类型做了重大改造)之后都能使用。下一节介绍一个只能在 Python 3 中使用的功能。
5 Python 2.7 使用的是 __metaclass__ 类属性,类的声明体不支持 metaclass= 关键字参数。
元类的特殊方法 __prepare__
type 构造方法及元类的 __new__
和 __init__
方法都会收到要计算的类的定义体,形式是名称到属性的映像。默认情况下,映射是字典,顺序丢失。
问题解决方法:使用 Python3 引入的特殊方法 __prepare__
。
只在元类中有用,而且必须声明为类方法。
解释器调用元类的 __new__
方法之前会先调用 __prepare__
方法,使用类定义体中的属性创建映射。
__prepare__
方法的参数是元类,要构建类的名称,基类组成的元组,返回值必须为映射。
元类构建新类时, __prepare__
方法返回的映射会传给 __new__
方法的最后一个参数,然后再传给 __init__
方法。
执行顺序:
prepare(创建命名空间)-> 依次执行类定义语句 -> new(创建类)-> init(初始化类)
元类定义了 __prepare__
以后,会最先执行 __prepare__
方法,返回一个空的定制的字典,然后再执行类的语句,类中定义的各种属性被收集入定制的字典,最后传给 new
和 init
方法。
class EntityMeta(type):
"""元类,用于创建带有验证字段的业务实体"""
@classmethod
def __prepare__(cls, name, bases):
return collections.OrderedDict() # 返回定制的字典,生成命名空间
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict)
cls._field_names = [] # 类属性
# 这里的 attr_dict 是 OrderedDict 对象,有序的属性字典
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = '_{}#{}'.format(type_name, key)
cls._field_names.append(key) # 这个类属性是有序的
class Entity(metaclass=EntityMeta):
"""带有验证字段的业务实体"""
@classmethod
def field_names(cls): # 获取类属性
for name in cls._field_names:
yield name
类作为对象
Python 数据模型为每个类定义了很多模型
__mro__
: 查看类的继承关系
__class__
: 获取类
__name__
: 获取类名
__bases__
: 由类的基类组成的元组,存储指向超类的强引用。
__qualname__
: Python3.3 引入的新属性,类或函数的限定名称,即从模块的全局作用域到类的点分路径
In [5]: class A:
...: pass
...:
In [6]: class B(A):
...: pass
...:
In [7]: B.__qualname__
Out[7]: 'B'
In [8]: class A:
...: class B:
...: pass
...:
In [9]: B.__qualname__
Out[9]: 'B'
In [10]: class A:
...: class B:
...: pass
...: print(B.__qualname__)
...:
A.B
__subclasses__()
: 返回一个列表,包含类的直接子类,使用弱引用,防止出现在超类和子类之间出现循环引用。返回的列表中是内存中现存的类。
In [14]: class A:
...: pass
...:
In [15]: class B(A):
...: pass
...:
In [17]: A.__subclasses__()
Out[17]: [__main__.B]
In [18]: A.__subclasses__
Out[18]: <function A.__subclasses__>
cls.mro()
: 构建类时,获取存储在类属性 __mro__
中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法的顺序。
dir
函数不会列出如上属性。
__init_subclass__
作者:我们对类元编程的学习到此结束。这是个很大的话题,我只讲了皮毛。
类是一等对象。
type 是“根元类”。
定义 __prepare__
方法,返回一个 OrderedDict 对象,用于储存名称到属性的映射。这样做能保留要构建的类在定义体中绑定属性的顺序,提供给元类的 __new_
_ 和 __init__
等方法使用。在这个示例中,我们定义了类属性 _field_names,因此用户可以使用 Entity.field_names() 方法以 Validated 描述符出现在源码中的顺序获取描述符。
元类是充满挑战、让人兴奋的功能,有时会被故作聪明的程序员滥用。最后,我们回顾一下 Alex Martelli 在他写的“水禽和抽象基类”一文的最后给我们的建议:
此外,不要在生产代码中定义抽象基类(或元类)……如果你很想这样做,我打赌可能是因为你想“找茬”,刚拿到新工具的人都有大干一场的冲动。如果你能避开这些深奥的概念,你(以及未来的代码维护者)的生活将更愉快,因为代码简洁明了。
——Alex Martelli
说出上述至理名言的人不仅是 Python 元编程大师,还是造诣颇深的软件工程师,负责世界上几个最重要的 Python 应用。
Python 是给法定成年人使用的语言。
——Alan Runyan
Plone 的联合创始人
Alan 的精辟定义道出了 Python 最好的特质之一:它不妨碍你,让你做你该做的事。这也意味着,它不会给你提供工具,让你限制其他人能对你的代码和代码所构建的对象做什么。
当然,Python 不完美。对我来说,最没法接受的是,Python 在标准库中混用驼峰式和蛇底式,或者直接把单词连在一起。但是,语言的定义和标准库只是生态系统的一部分。用户和贡献者组成的社区才是 Python 生态系统最重要的部分。
“Só erra quem trabalha”,这是葡萄牙语,意思是“只有真正做事的人才会犯错”。
函数(function)
严格来说,是指 def 块或 lambda 表达式计算得到的对象。通常,函数这个词用于表示任何可调用的对象,例如方法,有时甚至表示类。官方文档中的内置函数列表(http://docs.python.org/library/functions.html)列出了几个内置的类,例如 dict、range 和 str。另见可调用的对象词条。
In [27]: import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!