类元编程是指在运行时创建或定制类的技艺,在 Python 中,类是一等对象,因此任何时候都可以使用函数新建类,无需使用 class 关键字。类装饰器也是函数,不公审查,修改甚至可以把被装饰类替换成其它类。最后,元类是类元编程最高级的工具,使用元类可以创建具有某种特质的全新类种,例如我们见过的抽象基类

类工厂函数

标准库的一个类工厂函数 -- collections.namedtuple。我们把一个类名和几个属性名传给这个函数,它会创建一个 tuple 的子类,其中元素通过名称获取,还为调试提供了友好的字符串表示(__repr__


In [2]:
class Dog:
    
    def __init__(self, name, weight, owner):
        self.name = name
        self.weight = weight
        self.owner = owner
    
rex = Dog('Rex', 30, 'Bob')
rex


Out[2]:
<__main__.Dog at 0x7ff0002e7ba8>

这段代码各个字段名都出现了三次,让人厌烦,字符串表现形式也不友好,我们编写一个 record_factory 类工厂函数解决这个问题


In [3]:
def record_factory(cls_name, field_names):
    try:
        # 这里体现了鸭子类型,尝试在都好或空格处拆分 field_names,如果失败,则假定 field_names 本身就是可迭代对象
        field_names = field_names.replace(',', ' ').split()
    except AttributeError: #不能调用 .replace 或 .split 方法
        pass # 假定 field_names 本就是标识符组成的序列
    field_names = tuple(field_names) #使用属性名构建元组,这将成为新建类的 __slots__属性

    # __slots__变量,来限制该class能添加的属性
    # 将变成新建类的 __init__ 方法
    def __init__(self, *args, **kwargs):
        attrs = dict(zip(self.__slots__, args))
        attrs.update(kwargs)
        for name, value in attrs.items():
            setattr(self, name, value)
            
    # 把类的实例变成可迭代对象,按照 __slots__ 设定的顺序产出字段值
    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__, # 相当于 '__init__': __init__
                     __iter__  = __iter__, 
                     __repr__  = __repr__)
    
    # 用 type 方法构造,构建新类,然后返回
    return type(cls_name, (object,), cls_attrs)

In [4]:
Dog = record_factory('Dog', 'name weight owner')
rex = Dog('Rex', 30, 'Bob')
rex


Out[4]:
Dog(name='Rex', weight=30, owner='Bob')

In [5]:
name, weight, _ = rex # 实例是可迭代对象,所以可以方便的拆包
name, weight


Out[5]:
('Rex', 30)

In [6]:
"{2}'s dog weight {1}kg".format(*rex) # 实例是可迭代对象,所以可以方便的拆包


Out[6]:
"Bob's dog weight 30kg"

In [7]:
rex.weight = 32 记录实例是可变的对象
rex


  File "<ipython-input-7-8bcb3e40d9ef>", line 1
    rex.weight = 32 记录实例是可变的对象
                             ^
SyntaxError: invalid syntax

In [8]:
Dog.__mro__ # 新建的类继承 Object 类,和我们的工厂函数没有关系


Out[8]:
(__main__.Dog, object)

通常,我们将 type 视为函数,因为我们像函数那样使用它,type(my_object) 获取对象所属的类 -- 作用与 my_object.__class__ 相同。然而,type 是一个类,当成类使用的时候传入三个参数可以新建一个类(是的,type 可以根据传入的不同参数有不同的用法

type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))

比如下面的代码是等价的

class MyShinyClass:
    pass

##############

type('MyShinyClass', (), {})

因此我们要新建如下类:

class Foo:
    bar = True

可以写成:


In [9]:
Foo = type('Foo', (), {'bar':True})
Foo


Out[9]:
__main__.Foo

In [10]:
Foo.bar


Out[10]:
True

In [11]:
f = Foo()
f


Out[11]:
<__main__.Foo at 0x7ff0002e7d68>

如果你继承 Foo 类,可以写成

FooChild = type('FooChild', (Foo,),{})

我们看到 type 函数可以创建一个类,因为 type 是元类,Python 中所有对象都是由 type 创建而来,注意,Python 中所有的东西都是对象,包括 整数,字符串、函数以及类,都由 type 创建而来


In [12]:
age = 35
print(age.__class__)
age.__class__.__class__


<class 'int'>
Out[12]:
type

In [13]:
name = 'bob'
print(name.__class__)
name.__class__.__class__


<class 'str'>
Out[13]:
type

In [14]:
def foo(): pass
foo.__class__
foo.__class__.__class__


Out[14]:
type

In [15]:
class Bar(object): pass
b = Bar()
b.__class__
b.__class__.__class__


Out[15]:
type

总之,前面的 record_factory 函数最后一行会构建一个类,类的名称是 cls_name 参数的值,唯一直接超类是 object,有 __slots__, __init__, __iter__, __repr__ 四个类属性,其中后 3 个是实例方法。

我们本来可以将 __slots__ 类属性改成其它值,不过那样就要实现 __setattr__ 方法,为属性赋值时验证属性的名称,而且顺序相同,然而第 9 章说过,__slots__ 属性最主要特点就是节省内存,能处理数百万个实例,不过也有一些缺点。

把 3 个参数传给 type 是动态创建类的常用方式,如果查看 collections.namedtuple 源码会发现另一种方式,先声明一个 _class_template 变量,其值是字符串形式源码模板,然后在 namedtuple 函数中调用 _class_template.format(...) 方法,填充模板里的空白,最后,使用内置的 exec函数计算得到源码字符串

在 Python 元编程时,最好不要使用 exec 和 eval 函数,如果接受字符串来自不可信的源,这两个函数会有严重的安全风险,Python 提供了足够的内省工具,大多数时候不需要这两个函数。

record_factory 函数创建的类不能够序列化,即不能使用 pikle 模块里的 dump/load 函数处理,

定制描述符的类装饰器

上一章的 LineItem 例子还有个问题,就是储存的属性不具有描述性,即属性 _Quantity#0 不便于调试,如果能存储成 _Quantity#weight 之类的就好多了,上一章说过,我们不能使用描述性的存储属性名称,因为实例化描述符时无法得知托管属性,如前面的 weight 的名称,可是如果组建好整个类,而且把描述符绑定到类属性后,我们就可以审查类,并为描述符设置合理的存储属性名称。LineItem 的 __new__ 方法可以做到这一点,因此,在 __init__ 方法中使用描述符时,存储属性已经设置了正确的名称。为了解决这个问题使用 __new__ 方法属于白费力气,每次新建 LineItem 实例时都会运行 __new__ 方法中的逻辑,可是一旦 LineItem 类构建好了,描述符与托管属性之间的绑定就不会变了。因此,我们要在创建类时设置存储属性的名称。使用类装饰器或元类可以做到这一点,我们先使用简单的方式。

类装饰器和函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类


In [16]:
import abc 

class AutoStorage: 
    __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) # 不进行验证
      
    
class Validated(abc.ABC, AutoStorage): # 抽象类,也继承自 AutoStorage
    
    def __set__(self, instance, value):
        # __set__ 方法把验证委托给 validate 方法
        value = self.validate(instance, value) 
        #返回的 value 值返回给超类的 __set__ 方法,存储值
        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'''
    
    # 只需要根据不同的验证规则实现 validate 方法即可
    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 not-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 LineItem: # 托管类
#     weight = Quantity() 
#     price = Quantity()
#     description = NonBlank()
    
#     def __init__(self, description, weight, price):
#         self.description = description
#         self.weight = weight
#         self.price = price
        
#     def subtotal(self):
#         return self.weight * self.price
 
## --------------------
## 上面的和 上一章代码相同, LineItem 类只加了 1 行,在下面实现
## --------------------

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)
    return cls #返回修改后的类

In [17]:
@entity # 类装饰器,定义类的时候就会调用
class LineItem: 
    weight = Quantity() 
    price = Quantity()
    description = NonBlank()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

In [18]:
raisins = LineItem('Golden raisins', 10, 6.95)
dir(raisins)[:3]


Out[18]:
['_NonBlank#description', '_Quantity#price', '_Quantity#weight']

In [19]:
LineItem.description.storage_name


Out[19]:
'_NonBlank#description'

In [20]:
raisins.description


Out[20]:
'Golden raisins'

In [21]:
getattr(raisins, '_NonBlank#description')


Out[21]:
'Golden raisins'

类装饰器能以较简单的方式做到以前需要元类去做的事情 -- 创建类的时候定制类

类装饰器有个重大的缺点:只对直接依附的类有效,这意味着,被装饰的类的子类可能继承也可能不继承装饰类所做的改动,具体情况视改动方式而定

导入时和运行时比较

定义两个文件, evaltime.py


In [ ]:
#!/usr/bin/env python
# encoding: utf-8

from evalsupport import deco_alpha

print('<[0]> evaltime module start')

def test():
    class Test:
        print('<[1]> evaltime test Test')


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')

evalsupport.py


In [ ]:
#!/usr/bin/env python
# encoding: utf-8

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')
In [1]: import evaltime
<[100]> evalsupport module start #evalsupport 模块中所有顶层代码在导入模块时执行,解释器会编译 deco_alpha 函数,但不会执行定义体
<[400]> MetaAleph body        # 类定义体运行了
<[700]> evalsupport module end 
<[0]> evaltime module start
<[2]> ClassOne body # 每个类的定义体都执行了
<[6]> ClassTwo body #包括嵌套的类
<[7]> ClassThree body
<[200]> deco_alpha # 先计算被装饰的 ClassThree 类定义体,然后运行装饰器函数
<[9]> ClassFour body
<[14]> evaltime module end #这里,evaltime 是被导入的,不会运行 if __name == '__main__'
(py35) kaka@kaka-deep:~/kaka$ python3 evaltime.py 
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[0]> 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 # 类装饰器改变了 ClassThree.method_y 方法
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end
<[4]> ClassOne.__del__ # 程序结束后,绑定在全局变量 one 上的 ClassOne 实例才会被垃圾回收

元类基础知识

元类是制造类的工厂,不过不是函数,而是类。

根据 Python对象模型,类是对象,因此类肯定是另外某个类的实例,默认情况下,Python 中的类是 type 的实例,也就是说,type 是大多数内置的类和用户定义的类的元类,为了避免无限递归,type 是自身的实例。注意,我们没有说 str 或者 LineItem 继承自 type,而是说 str 和 LineItem 是 type 的实例。

object 类和 type 类之间的关系很独特,object 是 type 的实例,type 是 object 的子类,这种关系很独特,无法使用 Python 代码表述,因为其定义其中一个之前另一个必须存在,type 是自身的实例这一点也很神奇

除了 type,标准库中还有一些别的类,例如 ABCMeta 和 Enum。如下所示:


In [1]:
import collections
collections.Iterable.__class__


Out[1]:
abc.ABCMeta

In [2]:
import abc
abc.ABCMeta.__class__


Out[2]:
type

In [3]:
abc.ABCMeta.__mro__


Out[3]:
(abc.ABCMeta, type, object)

向上追溯,ABCMeta 最终所属的类也是 type,所有类都直接或间接的是 type 的实例,不过只有元类同事也是 type 的子类。若理解元类,一定要知道这种关系:元类(如 ABCMeta)从 type 类继承了构建类的能力。

我们要抓住的重点是,所有类都是 type 的实例,但元类还是 type 的子类,因此可以作为制造类的工厂,具体来说,元类可以通过实现 __init__ 方法来定制。元类的 __init__ 方法可以做到类装饰器能做的任何事情,但是作用更大

理解元类计算时间的练习

我们让 evalsupport.py 与原来相同,新建一个 evaltime_meta.py 作为主脚本:


In [ ]:
#!/usr/bin/env python
# encoding: utf-8

from evalsupport import deco_alpha
from evalsupport import MetaAleph

print('<[1]> evaltime 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 body')

	def method_z(self):
		print('<[8]> ClassFive.method_z')


class ClassSix(ClassFive):
	print('<[9]> ClassSix body')

	def method_z(self):
		print('<[10]> ClassSix.method_z')


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 module end')

引入操作:

In [1]: import evaltime_meta
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__ #与前面关键区别是,创建 ClassFive时调用了 MetaAleph.__init__ 方法
<[9]> ClassSix body
<[500]> MetaAleph.__init__ # 同上
<[15]> evaltime module end

Python 解释器计算 ClassFive 类的定义体时没有调用 type 构建具体的类定义体,而是调用 MetaAleph 类。MetaAleph 类的 __init__ 有 4 个参数。

self: 要初始化的对象,例如 ClassFive name, bases, dic: 与构建类时传给 type 的参数一样

重新看一下这个类:

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

编写元类时候,通常把 self 参数改成 cls。__init__ 方法的定义体中定义了 inner_2 函数,然后绑定给 cls.method_zMetaAleph.__init__ 方法签名中的 cls 指代要创建的类(例如 ClassFive)。而 inner_2 函数签名中的 self 最终是指代我们创建的类的实例(例如 ClassFive 类的实例)

运行脚本:

(pytorch) kaka@kaka-dell:~/kaka/python$ python3 evaltime_meta.py 
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime 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 body
<[600]> MetaAleph.__init__:inner_2 # MetaAleph 类的 __init__ 方法把ClassFive.method_z 方法替换成 inner_2 函数。
<[14]> ClassSix tests ..............................
<[7]> ClassFive body
<[600]> MetaAleph.__init__:inner_2 # ClassFive 的子类 ClassSix 也是一样
<[15]> evaltime module end

注意,ClassSix 类没有直接引用 MetaAleph 类,但是却收到了影响,因为它是 ClassFive 的子类,进而也是 MetaAleph 类的实例,所以由 MetaAleph.__init__ 实例化

定制描述符的元类


In [22]:
class EntityMeta(type):
    """元类,用于创建带有验证字段的业务实体"""
    def __init__(cls, name, bases, attr_dict): 
        super().__init__(name, bases, attr_dict) # 在超类(这里是 type)上调用 __init__
        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): # 这个类只是为了用起来便利,这个模块的用户直接继承它即可,不用关心元类
    '''带有验证字段的业务实体'''
    
    
class LineItem(Entity): 
    weight = Quantity() 
    price = Quantity()
    description = NonBlank()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

写成这种语法,用户完全不用知道描述符或元类,直接继承库中提供的类就能满足要求

元类的特殊用法 __prepare__

在某些应用中,可能要知道类属性的定义顺序,例如读写 csv 文件的库,用户定义的类可能想要把类中按顺序声明的字段与 csv 文件中的各列对应起来

前面说过,type 构造方法以及元类的 __new____init__ 都接收类的定义体,形式是一个名称到属性的字典,也就是说,当元类或装饰器获得映射时,属性的顺序已经丢失了。

在 Python 3 中可以使用 __prepare__, 这个特殊方法只能在元类中使用,而且要声明为类方法(即,要使用 classmethod 类装饰器定义)。解释器调用元类 __new__ 方法之前会调用 __prepare__ 方法,使用类定义提中的属性创建映射。__prepare 第一个参数是元类,随后两个参数是类的名称以及组成的元祖,返回值是映射。元类构建新类时,__prepare__ 方法返回的映射会传给 __new__ 方法的最后一个参数,然后再传给 __init__ 方法


In [24]:
import collections

class EntityMeta(type):
    """元类,用于创建带有验证字段的业务实体"""
    
    @classmethod
    def __prepare__(cls, name, bases):
        return collections.OrderedDict() # 返回空的 OrderedDict 实例,存储类属性
    
    def __init__(cls, name, bases, attr_dict): 
        super().__init__(name, bases, attr_dict) # 在超类(这里是 type)上调用 __init__
        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): # 这个类只是为了用起来便利,这个模块的用户直接继承它即可,不用关心元类
    '''带有验证字段的业务实体'''
    @classmethod
    def field_names(cls):
        for name in cls._field_names:
            yield name # 按照添加字段的顺序产出字段名称

In [26]:
class LineItem(Entity): 
    weight = Quantity() 
    price = Quantity()
    description = NonBlank()
    
    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)


weight
price
description

在现实世界中,框架和库会使用元类协助程序员执行很多任务,例如:

  • 验证属性
  • 一次把装饰器依附到多个方法上
  • 序列化对象或转换数据
  • 对象关系映射
  • 基于对象的持久存储
  • 动态转换使用其他语言写的类结构

类作为对象

Python 模型为每个类定义了很多属性,例如

  • cls.__mro__: 超类元组
  • cls.__class__: 所属类
  • cls.__name__: 类名
  • cls.__bases__: 由类的基类组成的元组
  • cls.__qualname__: 或函数的限定名称,即从模块的全局作用域到类的点分路径
  • cls.__subclasses__(): 这个方法返回一个列表,包含类的直接子类。
  • cls.mro(): 调用 cls.__mro__

In [ ]: