(元类) 是深奥的知识,99% 的用户都无需关注。如果你想知道是否需要使用元类,我告诉你,不需要 (真正需要使用元类的人确信他们需要,无需解释原因)。
--- Timsort 算法的发明者,活跃的 Python 贡献者
类元编程是指在运行时创建或定制类的技艺。在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,而无需使用 class
关键字。类装饰器也是函数,不过能够审查,修改,甚至把被装饰的类替换成其他类。最后,元类是类元编程最高级的工具:使用元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类。
元类功能强大,但是难以掌握,类装饰器能使用更简单的方式解决很多问题。
本章还会谈及导入时和运行时的区别 — 这是有效使用 Python 元编程的重要基础。
首先本章探讨如何在运行时创建类。
类工厂函数
假设我在编写一个宠物店应用程序,我想把狗的数据当作简单的记录处理。编写下面的样板代码让人厌烦:
class Dog:def __init__(self, name, weight, owner):self.name = nameself.weight = weightself.owner = owner
各个字段名称出现了三次。写了这么多样板代码,甚至字符串表示形式都不友好。参考 collections.namedtuple
,下面我们创建一个 record.factory
函数,即时创建简单的类(如 Dog)。
def record_factory(cls_name, field_names):try:field_names = field_names.replace(',', ' ').split()except AttributeError:passfield_names = tuple(field_names)def __init__(self, *args, **kwargs):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 iin zip(self.__slots__, self))return '{}({})'.format(self.__class__.__name__, values)cls_attrs = dict(__slots__ = field_names,__init__ = __init__,__iter__ = __iter__,__repr__ = __repr__)return type(cls_name, (object, ), cls_attrs)Dog = record_factory('Dog', 'name weight owner')
rex = Dog('Rex', 30, 'Bob')
print(rex)
name, weight, _ = rex
print(name, weight)
rex.weight = 32
print(rex)
print(Dog.__mro__)""" output: Dog(name='Rex', weight=30, owner='Bob') Rex 30 Dog(name='Rex', weight=32, owner='Bob') (<class '__main__.Dog'>, <class 'object'>) """
定制描述符的类装饰器
20节中的 LineItem
示例还有个问题没有解决:储存属性的名称不具有描述性,即属性(如 weight
) 的值存储在名为 _Quantity#0
的实例属性中,这样的名称有点不便于调试。
我们不能使用描述性的储存属性名称,因为实例化描述符时无法得知托管属性(即绑定到描述符上的类属性,例如前述示例的 weight
)的名称。可是,一旦组建好整个类,而且把描述符绑定到类属性上之后,我们就可以审查类,并为描述符设置合理的储存属性名称。因此,我们在创建类时设置储存属性的名称。使用类装饰器或元类可以做到这一点。我们首先使用较简单的方式。
类装饰器与函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类。
@model.entity
class LineItem:description = model.NonBlank()weight = model.Quantity()price = model.Quantity()def __init__(self, description, weight, price):self.description = descriptionself.weight = weightself.price = pricedef subtotal(self):return self.weight * self.price
def entity(cls):for key, attr in cls.__dict__.items():if isinstance(attr, Validated):type_name = type(attr).__name__attr.storage_name = '_{}#{}'.format(type_name, key)
类装饰器有个重大缺点:只对直接依附的类有效。这意味着,被装饰的类的子类可能继承也可能不继承装饰器所做的改动。如果想定制整个类层次结构,而不是一次只定制一个类,使用下一节介绍的元类更高效。
元类基础知识
元类是制造类的工厂,不过不是函数,而是类。
根据 Python 对象模型,类是对象,因此类肯定是另外某个列的实例。默认情况下,Python 中的类是 type
类的实例。也就是说, type
是大多数内置的类和用户定义的类的元类。为了避免无限回溯,type
是其自身的实例。
object
类和 type
类之间的关系很独特,object
是 type
的实例,而 type
是 object
的子类。这种关系很神奇,无法用 Python 代码表述,因为定义其中一个之前另一个必须存在。
除了 type
,标准库中海油一些别的元类,例如 ABCMeta
和 Enum
。collections.Iterable
所属的类是 abc.ABCMeta
。Iterable
是抽象类,而 ABCMeta
不是,Iterable
是 ABCMeta
的实例。
>>> import collections
>>> collections.Iterable.__class__
<class 'abc.ABCMeta'>
>>> import abc
>>> abc.ABCMeta.__class__
<class 'type'>
>>> abc.ABCMeta.__mro__
(<class 'abc.ABCMeta'>, <class 'type'>, <class 'object'>)
向上追溯,ABCMeta
最终所属的类也是 type
.所有类都直接或间接地是 type
的实例,不过只有元类同时也是 type
的子类。若想理解元类,一定要知道这种关系:元类(如 ABCMeta
) 从 type
类继承了构建类的能力。
我们要抓住的重点是,所有类都是 type
的实例,但是元类还是 type
的子类,因此可以作为制造类的工厂。具体来说,元类可以通过实现 __init__
方法定制实例。元类的 __init__
方法可以做到类装饰器能做的任何事情,但是作用更大。
定制描述符的元类
class LineItem(model.Entity):description = model.NonBlank()weight = model.Quantity()price = model.Quantity()def __init__(self, description, weight, price):self.description = descriptionself.weight = weightself.price = pricedef subtotal(self):return self.weight * self.priceclass EntityMeta(type):"""元类,用于创建带有验证字段的业务实体"""def __init__(cls, name, bases, attr_dict):super().__init__(name, bases, attr_dict)for key, attr in attr_dict.items():if isinstance(attr, Validated):type_name = type(attr).__name__attr.storage_name = '_{}#{}'.format(type_name, key)class Entity(metaclass=EntityMeta):"""带有验证字段的业务实体"""
元类的特殊方法 __prepare__
在某些应用中,可能需要知道类的属性定义的顺序。 type
构造方法及元类的 __new__
和 __init__
方法都会受到计算的类的定义体,形式是名称到属性的映射。然而在默认情况下,那个映射是字典;也就是说,元类或类装饰器获得映射时,属性在类定义体中的顺序已经丢失了。
这个问题的解决办法是,使用 Python3
引入的特殊方法 __prepare__
.这个特殊方法只在元类中有用,而且必须声明为类方法(即,要使用 @classmethod
装饰器定义)。解释器调用元类的 __new__
方法之前会先调用 __prepare__
方法,使用类定义体中的属性创建映射。__prepare__
方法第一个参数是元类,随后两个参数分别是要构建的类的名称和基类组成的元组,返回值必须是映射。元类构建新类时,__prepare__
方法返回的映射会传给 __new__
方法的最后一个参数,然后再传给 __init__
方法。
import collectionsclass EntityMeta(type):"""元类,用于创建带有验证字段的业务实体"""@classmethoddef __prepare__(metacls, name, bases):return collections.OrderedDict()def __init__(cls, name, bases, attr_dict):super().__init__(name, bases, attr_dict)cls._field_names = []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):"""带有验证字段的业务实体"""@classmethoddef field_names(cls):for name in cls._field_names:yield name
类作为对象
Python 数据模型为每个类定义了很多属性,其中三个属性在本书中已经见过多次: __mro__
, __class__
和 __name__
.此外,还有以下属性。
cls.__bases__
: 由类的基类组成的元组。cls.__qualname__
: Python3.3 新引入的属性,其值是类或函数的限定名称,即从模块的全局作用域到类的点分路径。cls.__subclasses__()
:这个方法返回一个列表,包含类的直接子类。这个方法的实现使用了弱引用,防止在超类和子类(子类在__bases__
属性中储存指向超类的强引用)之间出现循环引用。这个方法返回的列表中是内存里现存的子类。cls.mro()
: 构建类时,如果需要获取储存在类属性__mro__
中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法顺序。