类元编程是指在运行时创建或定制类。在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'>)

    

  1. 这个工厂函数要求传入两个参数,第一个参数是类名,第二个参数是属性名,由若干空格隔开的属性名
  2. 字符串表示
  3. 实例是可迭代的对象,因此赋值时可以方便拆包
  4. 传给format等函数也可以拆包
  5. 实例是可变对象
  6. 新建的类继承自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>

  

  1. 这里尝试在逗号或空格处拆分field_names,如果失败则假定field_names是一个可迭代的对象,一个元素对应一个属性名
  2. 使用属性名构建元组,这将成为新建类的__slots__属性,此外,这么做还设定了拆包和字符串表示形式各字段的顺序
  3. 这个函数将成为新建类的__init__方法,参数有位置参数和关键字参数
  4. 实现__iter__函数,把类的实例变成可迭代的对象,按照__slots__设定的顺序产出字段值
  5. 迭代__slots__和self,生成类的字符串形式
  6. 组建类属性字典
  7. 调用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

  

  1. 相比Python属性描述符(一)这个章节中的LineItem类,这里新增加了一个装饰器
  2. 装饰器的参数是一个类
  3. 迭代存储类属性的字典
  4. 判断属性是否是Validated描述符的实例
  5. 使用描述符类的名称和托管属性的名称重新命名storage_name
  6. 返回修改后的类
>>> 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这几个对象的关系如下图:

Python类元编程-LMLPHP

两个示意图都是正确的,左边强调的是,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类继承了构建类的能力

Python类元编程-LMLPHP

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

  

  1. LineItem是Entity的子类
  2. Entity类的存在只是为了便利,用户直接继承这个类,而无需关心EntityMeta元类,甚至可以不用知道他的存在
  3. 在超类(即type)调用__init__方法
  4. 获取描述符实例的类名和属性名,重定义描述符的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

  

  1. 返回一个空的OrderDict实例,类属性将存储在里面
  2. 构建的类中创建一个_field_names属性
  3. attr_dict是之前那个OrderDict对象,由解释器在调用__init__方法之前调用__prepare__方法时获得。因此,这个for循环会按照添加属性的顺序迭代属性
  4. 找到各个Validated字段添加进_field_names列表
  5. _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__中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类的解析顺序
04-25 21:19