python的metaclass类元编程

什么是metaclass?

metaclass一般翻译成“元类”,但也有人认为应该更关注元类“超越变形”的能力,应该翻译成超越类之类的。但是个人觉得“元类”比较顺口,其“本元”的含义也是实打实存在的,故本文用元类作为metaclass的中文翻译。

metaclass(元类)可以理解成是类的抽象、本元。类元编程即使用自定义metaclass获取极强灵活性的一种编程方式。

不过,在多处都有提到的需要注意的是:如果不是开发框架,不要使用元类,除非只是为了寻找乐趣:)

什么是本元?

首先还是要搞清楚什么是本元?这个概念很抽象,必须弄清楚。

本元可以理解成一种更高程度的抽象。首先,类(class)是对象(object)的抽象,对象是类的实例。因此可以说类是对象的本元。而在python中,所有类都是type类的实例,也就是说type是所有类的抽象,所以你可以理解type就是那个元类

type

一般我们会常用type(obj)来获取obj对象的类型。但type也可以用来创建新类型。这时需要传入三个参数:类名父类tuple属性字典

class MyClass: ... 等价于MyClass = type(classname, superclasses, attributedict)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class SuperCls:
def __init__(self):
print("__init__ in super class.")


class A(SuperCls):
pass


B = type("B", (SuperCls, ), {})

a = A()

# 输出
"""
__init__ in super class.
"""

b = B()

# 输出
"""
__init__ in super class.
"""

print(A.mro())

# 输出
"""
[<class '__main__.A'>, <class '__main__.SuperCls'>, <class 'object'>]
"""

print(B.mro())

# 输出
"""
[<class '__main__.B'>, <class '__main__.SuperCls'>, <class 'object'>]
"""

上面类对象的mro()方法代表Method Resolution Order,即方法解析顺序,可以看到这个类的继承顺序。

通过上面的代码可以看出,使用type来动态创建类和你自己写代码创建一个类是完全一样的。

还有个问题,当我们使用type的时候,它自己是个type类还是type对象?如果你直接在解释器中输入print(type)会得到返回<class 'type'>,因此可以认为你使用的就是type类。

MetaClass

OK,搞清楚了什么是“本元”和type,我们再来看metaclass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 自定义metaclass
class MyMeta(type):
def __call__(self, *args, **kwargs):
print("__call__ in MyMeta")
return super().__call__(*args, **kwargs)

def __init__(cls, what, bases=None, dict=None):
print("__init__ in MyMeta")
super().__init__(what, bases, dict)

def __new__(*args, **kwargs):
print("__new__ in MyMeta")
return type.__new__(*args, **kwargs)


class SuperCls:
pass

# 使用metaclass方法1:类定义代码中指定
class MyClass(SuperCls, metaclass=MyMeta):
pass

# 输出
"""
__new__ in MyMeta
__init__ in MyMeta
"""

# 使用metaclass方法2:直接像调用type那样调用MyMeta
MyClass = MyMeta("MyClass", (SuperCls, ), {})

# 输出
"""
__new__ in MyMeta
__init__ in MyMeta
"""

# 实例化
my_obj = MyClass()

# 输出
"""
__call__ in MyMeta
"""

看,我们的元类继承自type类。你在使用这个元类去定义一个新的类时,会调用元类的__new____init__;当你用这个新类去实例化一个新对象时又会调用元类的__new__

因此,你可以可以重载type的这三个方法来修改type的行为,但要理清这几个方法被调用的时机

1
2
3
__init__(cls, what, bases=None, dict=None)
__new__(*args, **kwargs)
__call__(self, *args, **kwargs)

典型案例

来看几个类元编程的典型案例。

Singleton单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SingletonMeta(type):
instance = None
def __call__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls.instance


class CurrentUser(object, metaclass=SingletonMeta):
def __init__(self, name=None):
super(CurrentUser, self).__init__()
self.name = name

def __str__(self):
return repr(self) + ":" + repr(self.name)


if __name__ == '__main__':
u = CurrentUser("liu")
print(u)
u2 = CurrentUser()
u2.name = "xin"
print(u2)
print(u)
assert u is u2

注:代码来自 https://github.com/Meteorix/python-design-patterns

这里重载的是type__call__方法。改变了用户类实例化的行为,使其只能得到唯一的实例。

简易ORM框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type

def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)


class StringField(Field):
def __init__(self, name):
super().__init__(name, 'varchar(100)')


class IntegerField(Field):
def __init__(self, name):
super().__init__(name, 'bigint')


class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
print('Found model: %s' % name)
mappings = dict()
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
for k in mappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
attrs['__table__'] = name # 假设表名和类名一致
return type.__new__(cls, name, bases, attrs)


class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)

def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)

def __setattr__(self, key, value):
self[key] = value

def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))


class User(Model):
# 定义类的属性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')


# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()

# 输出
"""
Found model: User
Found mapping: id ==> <IntegerField:id>
Found mapping: name ==> <StringField:username>
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
SQL: insert into User (id,username,email,password) values (?,?,?,?)
ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd']
"""

注:代码来自 https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072

这里重载的是type__new__方法,因此改变的是用户类在定义时的行为。这里用户类是一个数据模型,元类编程做的是在用户定义一个数据模型类的时候,把所有是Field类型的属性收集起来,统一放到mapping属性中。这样基类的方法save可以直接通过mapping属性来保存需要保存的Field属性们。

YAMLObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class YAMLObjectMetaclass(type):
def __init__(cls, name, bases, kwds):
super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
# 省略其余定义

class YAMLObject(metaclass=YAMLObjectMetaclass):
yaml_loader = Loader

# 使用
class Monster(yaml.YAMLObject):
yaml_tag = u'!Monster'
def __init__(self, name, hp, ac, attacks):
self.name = name
self.hp = hp
self.ac = ac
self.attacks = attacks
def __repr__(self):
return "%s(name=%r, hp=%r, ac=%r, attacks=%r)" % (
self.__class__.__name__, self.name, self.hp, self.ac,
self.attacks)

yaml.load("""
--- !Monster
name: Cave spider
hp: [2,6] # 2d6
ac: 16
attacks: [BITE, HURT]
""")

Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])

print yaml.dump(Monster(
name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT']))

# 输出
"""
!Monster
ac: 16
attacks: [BITE, HURT]
hp: [3, 6]
name: Cave lizard
"""

这里重载的是type__init__方法,因此改变的是用户类在定义时的行为。在定义了用户类Monster时,便通过cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)Monster类注册到系统。

PyText

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ComponentMeta(type):
def __new__(metacls, typename, bases, namespace):
if "Config" not in namespace:
# We need to dynamically create a new Config class per
# instance rather than inheriting a single empty config class
# because components are registered uniquely by config class.
# If a parent class specifies a config class, inherit from it.
parent_config = next(
(base.Config for base in bases if hasattr(base, "Config")), None
)
if parent_config is not None:

class Config(parent_config):
pass

else:

class Config(ConfigBase):
pass

namespace["Config"] = Config

component_type = next(
(
base.__COMPONENT_TYPE__
for base in bases
if hasattr(base, "__COMPONENT_TYPE__")
),
namespace.get("__COMPONENT_TYPE__"),
)
new_cls = super().__new__(metacls, typename, bases, namespace)

new_cls.Config.__COMPONENT_TYPE__ = component_type
new_cls.Config.__name__ = f"{typename}.Config"
new_cls.Config.__COMPONENT__ = new_cls
new_cls.Config.__EXPANSIBLE__ = namespace.get("__EXPANSIBLE__")
if component_type:
Registry.add(component_type, new_cls, new_cls.Config)
return new_cls

注:代码来自 https://github.com/facebookresearch/pytext/blob/351a060b479cbfe1db46b74f1fc975a5b6523122/pytext/config/component.py#L87

这里重载的是type__new__方法,因此改变的是用户类在定义时的行为。在定义了用户组件类时,便通过Registry.add(component_type, new_cls, new_cls.Config)将该组件类注册到系统。

参考资料