类元编程是指在运行时创建或定制类。在Python中,类是一等对象,因此任何时候都可以使用函数创建新类,而无需用class关键字。类装饰器也是函数,不过能够审查、修改,甚至把被装饰的类替换成其他类。元类是类元编程最高级的工具:使用元类可以创建具有某种特性的全新类种,例如我们见过的抽象基类
首先,我们先尝试在运行时创建一个类,collections.namedtuple是一个类工厂函数。我们把一个类名和几个属性名传给这个函数,它会创建一个tuple的子类,其中的元素通过名称获取,还为调试提供了友好的字符串表示形式(__repr__)
假设我们有个宠物狗Dog的类,这个类有三个字段,狗的名字(那么)、重量(weight)和主人(owner),正常大家都会想这个类应该是这样实现的:
class Dog: def __init__(self, name, weight, owner): self.name = name self.weight = weight self.owner = owner
如果现在猫的类或者一个水果类,而这些类中只要有一个__init__来初始化一些字段,会发现产生许多冗余的代码,现在,让我们写一个函数record_factory,这个函数可以即时创建简单的类,这里我们先看一下record_factory的效果,再分析record_factory函数
>>> Dog = record_factory("Dog", "name weight owner") # <1> >>> rex = Dog("Rex", 30, "Bob") >>> rex # <2> Dog(name='Rex', weight=30, owner='Bob') >>> name, weight, _ = rex # <3> >>> name, weight ('Rex', 30) >>> "{2}'s dog weighs {1}kg".format(*rex) # <4> "Bob's dog weighs 30kg" Dog(name='Rex', weight=32, owner='Bob') >>> rex.weight = 32 # <5> >>> rex Dog(name='Rex', weight=32, owner='Bob') >>> Dog.__mro__ # <6> (<class 'factories.Dog'>, <class 'object'>)
- 这个工厂函数要求传入两个参数,第一个参数是类名,第二个参数是属性名,由若干空格隔开的属性名
- 字符串表示
- 实例是可迭代的对象,因此赋值时可以方便拆包
- 传给format等函数也可以拆包
- 实例是可变对象
- 新建的类继承自object,与我们的工厂函数没有关系
现在,我们来看下record_factory()函数
def record_factory(cls_name, field_names): try: field_names = field_names.replace(',', ' ').split() # <1> except AttributeError: # no .replace or .split pass # assume it's already a sequence of identifiers field_names = tuple(field_names) # <2> def __init__(self, *args, **kwargs): # <3> attrs = dict(zip(self.__slots__, args)) attrs.update(kwargs) for name, value in attrs.items(): setattr(self, name, value) def __iter__(self): # <4> for name in self.__slots__: yield getattr(self, name) def __repr__(self): # <5> values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__, self)) return '{}({})'.format(self.__class__.__name__, values) cls_attrs = dict(__slots__=field_names, # <6> __init__=__init__, __iter__=__iter__, __repr__=__repr__) return type(cls_name, (object,), cls_attrs) # <7>
- 这里尝试在逗号或空格处拆分field_names,如果失败则假定field_names是一个可迭代的对象,一个元素对应一个属性名
- 使用属性名构建元组,这将成为新建类的__slots__属性,此外,这么做还设定了拆包和字符串表示形式各字段的顺序
- 这个函数将成为新建类的__init__方法,参数有位置参数和关键字参数
- 实现__iter__函数,把类的实例变成可迭代的对象,按照__slots__设定的顺序产出字段值
- 迭代__slots__和self,生成类的字符串形式
- 组建类属性字典
- 调用type构造方法,构建新类,然后返回
通常,我们把type视为函数,利用它返回一个对象的类型,如type(obj),作用与obj.__calss__相同。然而,type是一个类,当成类使用时,传入三个参数可以组建一个新的类,如:
MyClass = type('MyClass', (MySuperClass, MyMixin), {'x': 42, 'x2': lambda self: self.x * 2})
type的三个参数分别是name,bases和dict。最后一个参数是一个映射,指定新类的属性名和值,上述代码与下面的代码有相同的作用
class MyClass(MySuperClass, MyMixin): x = 42 def x2(self): return self.x * 2
record_factory函数的最后一行会构建出一个类,类的名称是cls_name参数的值,唯一的直接超类是object,有__slots__、__init__、__iter__和__repr__四个类属性,其中后三个是实例方法
定义描述符的类装饰器:在Python属性描述符(一)这个章节中,LineItem类还有些问题没解决:存储属性的名称不具有描述性,即属性(如weight)的值存储名名_Quantity#{uuid},这样的名称不便于调试。由于实例化描述符时无法得到托管类属性,可是,一旦组件好整个类,而且把描述符实例绑定到类属性上之后,我们就可以审查类了。
import abc import uuid class AutoStorage: def __init__(self): cls = self.__class__ prefix = cls.__name__ identity = str(uuid.uuid4())[:8] self.storage_name = '_{}#{}'.format(prefix, identity) 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) class Validated(abc.ABC, AutoStorage): def __set__(self, instance, value): value = self.validate(instance, value) super().__set__(instance, value) @abc.abstractmethod def validate(self, instance, value): """return validated value or raise ValueError""" class Quantity(Validated): """a number greater than zero""" def validate(self, instance, value): if value <= 0: raise ValueError('value must be > 0') return value class NonBlank(Validated): """a string with at least one non-space character""" def validate(self, instance, value): value = value.strip() if len(value) == 0: raise ValueError('value cannot be empty or blank') return value def entity(cls): # <2> for key, attr in cls.__dict__.items(): # <3> if isinstance(attr, Validated): # <4> type_name = type(attr).__name__ attr.storage_name = '_{}#{}'.format(type_name, key) # <5> return cls # <6> @entity # <1> class LineItem: description = NonBlank() weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- 相比Python属性描述符(一)这个章节中的LineItem类,这里新增加了一个装饰器
- 装饰器的参数是一个类
- 迭代存储类属性的字典
- 判断属性是否是Validated描述符的实例
- 使用描述符类的名称和托管属性的名称重新命名storage_name
- 返回修改后的类
>>> raisins = LineItem('Golden raisins', 10, 6.95) >>> dir(raisins)[:3] ['_NonBlank#description', '_Quantity#price', '_Quantity#weight'] >>> LineItem.description.storage_name '_NonBlank#description' >>> raisins.description 'Golden raisins' >>> getattr(raisins, '_NonBlank#description') 'Golden raisins'
可以看出,类装饰器能以比较简单的方式做到以前需要使用元类做的事情,即创建类时定制类。类装饰器有一个缺点:只对依附类有效,这意味着,被装饰的类的子类可能继承也可能不继承装饰器所做的改动
导入时和运行时比较:为了正确地做元编程,必须知道Python解释器什么时候计算各个代码块。代码在导入时,解释器会从上到下一次性解析完.py模块的源码,然后生成用于执行的字节码。如果句法有错误,就在此时报告。如果本地__pycache__目录下中有最新的.pyc文件,解释器会跳过上述的步骤,因为已经有运行所需的字节码了
编译肯定是导入时的动作,不过那个时期还会做其他的事,因为Python中的语句几乎都是可执行的,也就是说语句可能会运行用户代码,修改用户程序的状态。尤其是import语句,它不只是声明,在进程中首次导入模块时,还会运行所导入模块的全部顶层代码,以后导入相同的模块则使用缓存,只做名称绑定。那些顶层代码可以做任何事,包括通常在运行时做的事,例如连接数据库。因此,“导入时”与“运行时”之间的界限是模糊的,import语句可以触发运行时行为
导入时会运行全部顶层代码,但是顶层代码会经过一些加工。导入模块时,解释器会执行顶层的def语句,解释器会编译函数的定义体(首次导入模块时),把函数对象绑定到对应的全局名称上,但是解释器显然不会执行函数的定义体。通常这意味着解释器在导入时定义顶层函数,但是仅当在运行时调用函数才会执行函数的定义体
对类来说,情况就不同了:在导入时,解释器会执行每个类的定义体,甚至会执行嵌套类的定义体,执行类定义体的结果是,定义了类属性和方法,并构建了类对象,从这个意义上理解,类的定义体属于“顶层代码”,因为它在导入时运行
先来看两个脚本:
脚本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): print('<[500]> MetaAleph.__init__') def inner_2(self): print('<[600]> MetaAleph.__init__:inner_2') cls.method_z = inner_2 print('<[700]> evalsupport module end')
脚本evaltime.py
from evalsupport import deco_alpha print('<[1]> evaltime module start') class ClassOne(): print('<[2]> ClassOne body') def __init__(self): print('<[3]> ClassOne.__init__') def __del__(self): print('<[4]> ClassOne.__del__') def method_x(self): print('<[5]> ClassOne.method_x') class ClassTwo(object): print('<[6]> ClassTwo body') @deco_alpha class ClassThree(): print('<[7]> ClassThree body') def method_y(self): print('<[8]> ClassThree.method_y') class ClassFour(ClassThree): print('<[9]> ClassFour body') def method_y(self): print('<[10]> ClassFour.method_y') if __name__ == '__main__': print('<[11]> ClassOne tests', 30 * '.') one = ClassOne() one.method_x() print('<[12]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[13]> ClassFour tests', 30 * '.') four = ClassFour() four.method_y() print('<[14]> evaltime module end')
现在让我们尝试在python控制台中导入evaltime.py模块,和用python解释器运行evaltime.py文件
首先是在python控制台导入evaltime.py模块
>>> import evaltime <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime module start <[2]> ClassOne body <[6]> ClassTwo body <[7]> ClassThree body <[200]> deco_alpha <[9]> ClassFour body <[14]> evaltime module end
- [100]:evalsupport模块中所有顶层代码在导入模块时运行:解释器会比编译deco_alpha函数,但是不会执行定义体
- [400]:MetaAleph类的定义体运行了
- [2]:每个类的定义体都执行了
- [6]:包括嵌套类的定义体执行了
- [200]:先执行被装饰类ClassThree的定义体,然后运行装饰器函数
- [14]:在这个场景中,evaltime模块时导入的,因此不会运行if __name__ == "__main__":块
这里有一点要注意的:解释器先执行类的定义体,然后再调用依附在类上的装饰器函数,这是合理的行为,因为必须先构建类对象,装饰器才有类对象处理
再来看另外一个例子,用python解释器执行evaltime.py文件
python evaltime.py <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime module start <[2]> ClassOne body <[6]> ClassTwo body <[7]> ClassThree body <[200]> deco_alpha <[9]> ClassFour body <[11]> ClassOne tests .............................. <[3]> ClassOne.__init__ <[5]> ClassOne.method_x <[12]> ClassThree tests .............................. <[300]> deco_alpha:inner_1 <[13]> ClassFour tests .............................. <[10]> ClassFour.method_y <[14]> evaltime module end <[4]> ClassOne.__del__
- [9]:到本条为止,输出和上一个例子相同
- [3]:类的标准行为
- [300]:deco_alpha装饰器修改了ClassThree.method_y方法,因此调用three.method_y()时运行inner_1函数的定义体
- [4]:只有程序结束时,绑定在全局变量one上的Classone实例才会被垃圾回收程序收回
在第二个例子中主要想说明的是,类装饰器可能对类的子类没有影响,我们把ClassFour定义为ClassThree的子类,ClassThree类上依附着@deco_alpha装饰器把method_y方法替换掉了,这对ClassFour类根本没有影响。当然,如果ClassFour.method_y()方法使用super()调用ClassThree.method_y()方法,我们便会看到装饰器起作用了,执行inner_1函数
根据Python对象模型,类是对象,因此,类肯定是另外某个类的实例。默认情况下,Python中的类是type类的实例,也就是说,type是大多数内置的内或用户定义的类的元类:
>>> "spam".__class__ <class 'str'> >>> str.__class__ <class 'type'> >>> LineItem.__class__ <class 'type'> >>> type.__class__ <class 'type'>
为了避免无限回溯,type是其自身的实例,注意,str或LineItem是type的实例,而并不是继承自type,这两个类都是object的子类,str、LineItem、type和object这几个对象的关系如下图:
两个示意图都是正确的,左边强调的是,str、type和LineItem都是object的子类,右边的示意图则表明,str、object和LineItem是type的实例,因为他们都是类。object类和type类之间的关系很独特:object是type的实例,而type是object的子类,除了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类继承了构建类的能力
Iterable是object的子类,是ABCMeta的实例。object和ABCMeta都是type的实例,但这里重要关系是,ABCMeta还是type的子类,因为ABCMeta是元类。所有类都是type的实例,但是元类还是type的子类,因此可以作为制造类的工厂。具体通过实现__init__方法定制实例,元类__init__方法可以做到类装饰器能做的事情,而且作用更大
evaltime_meta.py:ClassFive是MetaAleph元类的实例,evalsupport模块在上面的evalsupport.py中
from evalsupport import deco_alpha from evalsupport import MetaAleph print('<[1]> evaltime_meta module start') @deco_alpha class ClassThree(): print('<[2]> ClassThree body') def method_y(self): print('<[3]> ClassThree.method_y') class ClassFour(ClassThree): print('<[4]> ClassFour body') def method_y(self): print('<[5]> ClassFour.method_y') class ClassFive(metaclass=MetaAleph): print('<[6]> ClassFive body') def __init__(self): print('<[7]> ClassFive.__init__') def method_z(self): print('<[8]> ClassFive.method_y') class ClassSix(ClassFive): print('<[9]> ClassSix body') def method_z(self): print('<[10]> ClassSix.method_y') if __name__ == '__main__': print('<[11]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[12]> ClassFour tests', 30 * '.') four = ClassFour() four.method_y() print('<[13]> ClassFive tests', 30 * '.') five = ClassFive() five.method_z() print('<[14]> ClassSix tests', 30 * '.') six = ClassSix() six.method_z() print('<[15]> evaltime_meta module end')
现在,我们再用控制台和解释器执行的方式来执行evaltime_meta.py模块
在控制台导入evaltime_meta.py模块
>>> import evaltime_meta <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime_meta module start <[2]> ClassThree body <[200]> deco_alpha <[4]> ClassFour body <[6]> ClassFive body <[500]> MetaAleph.__init__ <[9]> ClassSix body <[500]> MetaAleph.__init__ <[15]> evaltime_meta module end
- [6]:创建ClassFive时,执行了ClassFive的定义体,同时调用了MetaAleph.__init__方法
- [9]:创建ClassFive的子类ClassSix时执行了ClassSix的定义体,同时调用了MetaAleph.__init__方法
Python解释器执行ClassFive类的定义体时没有调用type构建工具的类定义体,而是调用MetaAleph类,看下evalsupport.py文件中的MetaAleph.__init__方法,可以发现有四个参数:
- cls:这是要初始化的类对象
- name、bases、dic:与构建类时传给type的参数一样
evalsupport.py定义MetaAleph元类
class MetaAleph(type): print('<[400]> MetaAleph body') def __init__(cls, name, bases, dic): print('<[500]> MetaAleph.__init__') def inner_2(self): print('<[600]> MetaAleph.__init__:inner_2') cls.method_z = inner_2
通常编写元类时,__init__第一个参数self会改成cls,这样就能清楚表明要构建的实例是类
在命令行中执行evaltime_meta.py脚本
python evaltime_meta.py <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime_meta module start <[2]> ClassThree body <[200]> deco_alpha <[4]> ClassFour body <[6]> ClassFive body <[500]> MetaAleph.__init__ <[9]> ClassSix body <[500]> MetaAleph.__init__ <[11]> ClassThree tests .............................. <[300]> deco_alpha:inner_1 <[12]> ClassFour tests .............................. <[5]> ClassFour.method_y <[13]> ClassFive tests .............................. <[7]> ClassFive.__init__ <[600]> MetaAleph.__init__:inner_2 <[14]> ClassSix tests .............................. <[7]> ClassFive.__init__ <[600]> MetaAleph.__init__:inner_2 <[15]> evaltime_meta module end
- [300]:装饰器依附到ClassThree类上之后,method_y方法被替换成inner_1方法
- [5]:虽然ClassFour是ClassThree的子类,但是依附的装饰器并没有对ClassFour造成影响
- [600]:MetaAleph类的__init__函数把ClassFive和ClassSix的method_z函数替换成inner_2函数
定制描述符的元类:回到LineItem类,我们是否能提供一个类,通过继承可以代替描述符或元类?
import abc import uuid class AutoStorage: __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ identity = str(uuid.uuid4())[:8] self.storage_name = '_{}#{}'.format(prefix, identity) 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) class Validated(abc.ABC, AutoStorage): def __set__(self, instance, value): value = self.validate(instance, value) super().__set__(instance, value) @abc.abstractmethod def validate(self, instance, value): """return validated value or raise ValueError""" class Quantity(Validated): """a number greater than zero""" def validate(self, instance, value): if value <= 0: raise ValueError('value must be > 0') return value class NonBlank(Validated): """a string with at least one non-space character""" def validate(self, instance, value): value = value.strip() if len(value) == 0: raise ValueError('value cannot be empty or blank') return value class EntityMeta(type): """Metaclass for business entities with validated fields""" def __init__(cls, name, bases, attr_dict): super().__init__(name, bases, attr_dict) # <3> for key, attr in attr_dict.items(): # <4> if isinstance(attr, Validated): type_name = type(attr).__name__ attr.storage_name = '_{}#{}'.format(type_name, key) class Entity(metaclass=EntityMeta): # <2> """Business entity with validated fields""" class LineItem(Entity): # <1> description = NonBlank() weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- LineItem是Entity的子类
- Entity类的存在只是为了便利,用户直接继承这个类,而无需关心EntityMeta元类,甚至可以不用知道他的存在
- 在超类(即type)调用__init__方法
- 获取描述符实例的类名和属性名,重定义描述符的storage_name
元类的特殊方法__prepare__
某些应用中,可能需要知道类的属性定义的顺序,type构造方法及元类的__new__和__init__方法都会收到要执行的类的定义体,形式是名称到属性的映像。在默认情况下,那个映射是字典,也就是说,元类或类装饰器获得映射时,属性在类定义中的顺序已经丢失了
这个问题的解决办法是:使用Python3引入的特殊方法__prepare__。这个方法只在元类中有用,而且必须声明为类方法(即用@classmethod装饰器定义)。解释器调用元类的__new__方法之前会先调用__prepare__方法,使用类定义体中的属性创建映射。__prepare__方法的第一个参数是元类,随后两个参数分别是要构建的类名称和基类组成的元组,返回值必须是映射。元类构建新类时,__prepare__方法返回的映射会传给__new__方法的最后一个参数,然后再传给__init__方法
class EntityMeta(type): """Metaclass for business entities with validated fields""" @classmethod def __prepare__(cls, name, bases): return collections.OrderedDict() # <1> def __init__(cls, name, bases, attr_dict): super().__init__(name, bases, attr_dict) cls._field_names = [] # <2> for key, attr in attr_dict.items(): # <3> if isinstance(attr, Validated): type_name = type(attr).__name__ attr.storage_name = '_{}#{}'.format(type_name, key) cls._field_names.append(key) # <4> class Entity(metaclass=EntityMeta): """Business entity with validated fields""" @classmethod def field_names(cls): # <5> for name in cls._field_names: yield name
- 返回一个空的OrderDict实例,类属性将存储在里面
- 构建的类中创建一个_field_names属性
- attr_dict是之前那个OrderDict对象,由解释器在调用__init__方法之前调用__prepare__方法时获得。因此,这个for循环会按照添加属性的顺序迭代属性
- 找到各个Validated字段添加进_field_names列表
- _field_names类方法作用简单,按照添加字段的顺序产出字段的名称
class LineItem(Entity): description = NonBlank() weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price for name in LineItem.field_names(): print(name)
运行结果:
description weight price
在开发框架或库时,使用元类会协助我们执行很多任务,如:
- 验证属性
- 一次把装饰器依附到多个地方
- 序列化对象或转换数据
- 对象关系映射
- 基于对象的持久存储
- 动态转换使用其他语言编写的类结构
类作为对象:__mro__、__class__和__name__已经见了很多次了,除此之外,类对象还有以下属性:
- cls.__bases__:由类的基类组成的元组
- cls.__qualname__:即从模块的全局作用域到类的点分路径,例如在A类中定义了B类,那么B类对象的__qualname__就是A.B
- cls.__subclasses__():这个方法返回一个列表,包含类的直接子类
- cls.mro():构建类时,如果需要获取存储在类属性的__mro__中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类的解析顺序