Python 中的元类
INFO
"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."
—Tim Peters
# 陌生的 metaclass
Python 中的 元类(metaclass) 是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。
# 类也是对象
在 Python 中,一切皆对象。字符串,列表,字典,函数是对象,类也是一个对象,因此你可以:
- 把类赋值给一个变量
- 把类作为函数参数进行传递
- 把类作为函数的返回值
- 在运行时动态地创建类
看一个简单的例子:
class Foo(object):
foo = True
class Bar(object):
bar = True
def echo(cls):
print cls
def select(name):
if name == 'foo':
return Foo # 返回值是一个类
if name == 'bar':
return Bar
>>> echo(Foo) # 把类作为参数传递给函数 echo
<class '__main__.Foo'>
>>> cls = select('foo') # 函数 select 的返回值是一个类,把它赋给变量 cls
>>> cls
__main__.Foo
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 熟悉又陌生的 type
在日常使用中,我们经常使用 object
来派生一个类,事实上,在这种情况下,Python 解释器会调用 type
来创建类。
这里,出现了 type
,没错,就是我们知道的那个 type
,我们经常使用它来判断一个对象的类型,比如:
class Foo(object):
Foo = True
>>> type(10)
<type 'int'>
>>> type('hello')
<type 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<type 'type'>
2
3
4
5
6
7
8
9
10
11
事实上,type
除了可以返回对象的类型,它还可以被用来动态地创建类(对象)。下面,我们看几个例子,来消化一下这句话。
使用 type
来创建类(对象)的方式如下:
>>> help(type)
Help on class type in module __builtin__:
class type(object)
| type(object) -> the object's type
| type(name, bases, dict) -> a new type
2
3
4
5
6
type(类名, 父类的元组(针对继承的情况,可以为空),包含属性和方法的字典(名称和值))
# 最简单的情况
假设有下面的类:
class Foo(object):
pass
2
现在,我们不使用 class
关键字来定义,而使用 type
,如下:
Foo = type('Foo', (object, ), {}) # 使用 type 创建了一个类对象
上面两种方式是等价的。我们看到,type
接收三个参数:
- 第 1 个参数是字符串 'Foo',表示类名
- 第 2 个参数是元组 (object, ),表示所有继承的父类
- 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法
在上面,我们使用 type()
创建了一个名为 Foo 的类,然后把它赋给了变量 Foo,我们当然可以把它赋给其他变量,但是,此刻没必要给自己找麻烦。
接着,我们看看使用:
>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo())
<__main__.Foo object at 0x10c34f250>
2
3
4
# 有属性和方法的情况
假设有下面的类:
class Foo(object):
foo = True
def greet(self):
print 'hello world'
print self.foo
2
3
4
5
用 type
来创建这个类,如下:
def greet(self):
print 'hello world'
print self.foo
Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})
2
3
4
5
上面两种方式的效果是一样的,看下使用:
>>> f = Foo()
>>> f.foo
True
>>> f.greet
<bound method Foo.greet of <__main__.Foo object at 0x10c34f890>>
>>> f.greet()
hello world
True
2
3
4
5
6
7
8
# 继承的情况
再来看看继承的情况,假设有如下的父类:
class Base(object):
pass
2
我们用 Base 派生一个 Foo 类,如下:
class Foo(Base):
foo = True
2
3
改用 type
来创建,如下:
Foo = type('Foo', (Base, ), {'foo': True})
# 什么是元类(metaclass)
元类(metaclass)是用来创建类(对象)的可调用对象。 这里的可调用对象可以是函数或者类等。但一般情况下,我们使用类作为元类。对于实例对象、类和元类,我们可以用下面的图来描述:
类是实例对象的模板,元类是类的模板
类是实例对象的模板,元类是类的模板
+----------+ +----------+ +----------+
| | | | | |
| | instance of | | instance of | |
| instance +------------>+ class +------------>+ metaclass|
| | | | | |
| | | | | |
+----------+ +----------+ +----------+
2
3
4
5
6
7
8
9
我们在前面使用了 type
来创建类(对象),事实上,type
就是一个元类。
那么,元类到底有什么用呢?要你何用……
元类的主要目的是为了控制类的创建行为。 我们还是先来看看一些例子,以消化这句话。
# 元类的使用
# 创建类时能够自动地改变类
先从一个简单的例子开始,假设有下面的类:
class Foo(object):
name = 'foo'
def bar(self):
print('bar')
2
3
4
现在我们想给这个类的方法和属性名称前面加上 my_
前缀,即 name 变成 my_name,bar 变成 my_bar,另外,我们还想加一个 echo 方法。当然,有很多种做法,这里展示用元类的做法。
1.首先,定义一个元类,按照默认习惯,类名以 Metaclass 结尾,代码如下:
class PrefixMetaclass(type):
def __new__(cls, name, bases, attrs):
# 给所有属性和方法前面加上前缀 my_并转化为字典
_attrs = dict(('my_' + name, value) for name, value in attrs.items())
_attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法
return type.__new__(cls, name, bases, _attrs) # 返回创建后的类
2
3
4
5
6
7
上面的代码有几个需要注意的点:
- PrefixMetaClass 从
type
继承,这是因为 PrefixMetaclass 是用来创建类的 __new__
是在__init__
之前被调用的特殊方法,它用来创建对象并返回创建后的对象,对它的参数解释如下:- cls:当前准备创建的类
- name:类的名字
- bases:类的父类集合
- attrs:类的属性和方法,是一个字典
2.接着,我们需要指示 Foo 使用 PrefixMetaclass 来定制类。
在 Python2 中,我们只需在 Foo 中加一个 __metaclass__
的属性,如下:
class Foo(object):
__metaclass__ = PrefixMetaclass
name = 'foo'
def bar(self):
print 'bar'
2
3
4
5
在 Python3 中,这样做:
class Foo(metaclass=PrefixMetaclass):
name = 'foo'
def bar(self):
print('bar')
2
3
4
现在,让我们看看使用:
>>> f = Foo()
>>> f.name # name 属性已经被改变
---------
AttributeError Traceback (most recent call last)
<ipython-input-774-4511c8475833> in <module>()
----> 1 f.name
AttributeError: 'Foo' object has no attribute 'name'
>>>
>>> f.my_name
'foo'
>>> f.my_bar()
bar
>>> f.echo('hello')
'hello'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到,Foo 原来的属性 name 已经变成了 my_name,而方法 bar 也变成了 my_bar,这就是元类的魔法。
再来看一个继承的例子,下面是完整的代码:
class PrefixMetaclass(type):
def __new__(cls, name, bases, attrs):
# 给所有属性和方法前面加上前缀 my_ 转化为字典
_attrs = dict(('my_' + name, value) for name, value in attrs.items())
_attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法
return type.__new__(cls, name, bases, _attrs)
class Foo(object):
__metaclass__ = PrefixMetaclass # 注意跟 Python3 的写法有所区别
name = 'foo'
def bar(self):
print('bar')
class Bar(Foo):
prop = 'bar'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
其中,PrefixMetaclass 和 Foo 跟前面的定义是一样的,只是新增了 Bar,它继承自 Foo。先让我们看看使用:
>>> b = Bar()
>>> b.prop # 发现没这个属性
---------
AttributeError Traceback (most recent call last)
<ipython-input-778-825e0b6563ea> in <module>()
----> 1 b.prop
AttributeError: 'Bar' object has no attribute 'prop'
>>> b.my_prop
'bar'
>>> b.my_name
'foo'
>>> b.my_bar()
bar
>>> b.echo('hello')
'hello'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们发现,Bar 没有 prop 这个属性,但是有 my_prop 这个属性,这是为什么呢?
原来,当我们定义 class Bar(Foo)
时,Python 会首先在当前类,即 Bar 中寻找 __metaclass__
,如果没有找到,就会在父类 Foo 中寻找 __metaclass__
,如果找不到,就继续在 Foo 的父类寻找,如此继续下去,如果在任何父类都找不到 __metaclass__
,就会到模块层次中寻找,如果还是找不到,就会用 type 来创建这个类。
# 强制子类实现特定方法
假设你是一个库的作者,例如下面的代码,其中的方法 foo
要求子类必须实现 bar
方法:
# library code
class Base(object):
def foo(self):
return self.bar()
# user code
class Derived(Base):
def bar():
return None
2
3
4
5
6
7
8
9
但作为库的作者,我们根本无法预测用户会写出什么样的代码,有什么方法能强制用户在子类中实现方法 bar
呢?用 metaclass 可以做到。
class Meta(type):
def __new__(cls, name, bases, namespace, **kwargs):
if name != 'Base' and 'bar' not in namespace:
raise TypeError('bad user class')
return super().__new__(cls, name, bases, namespace, **kwargs)
class Base(object, metaclass=Meta):
def foo(self):
return self.bar()
2
3
4
5
6
7
8
9
现在,我们尝试定义一个不包含 bar
方法的子类,在类的定义(或者说生成)阶段就会报错:
>>> class Derived(Base):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in __new__
TypeError: bad user class
2
3
4
5
6
7
# 注册所有子类
有时我们会希望获取继承了某个类的子类,例如,实现了基类 Fruit
,想知道都有哪些子类继承了它,用元类就能实现这个功能:
class Meta(type):
def __init__(cls, name, bases, namespace, **kwargs):
super().__init__(name, bases, namespace, **kwargs)
if not hasattr(cls, 'registory'):
# this is the base class
cls.registory = {}
else:
# this is the subclass
cls.registory[name.lower()] = cls
class Fruit(object, metaclass=Meta):
pass
class Apple(Fruit):
pass
class Orange(Fruit):
pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
之后,我们可以查看所有 Fruit
的子类:
Fruit.registory
{'apple': <class '__main__.Apple'>, 'orange': <class '__main__.Orange'>}
2
# 为什么要用 metaclass 类而不是函数
由于__metaclass__
可以接受任何可调用的对象,那为何还要使用类呢,因为很显然使用类会更加复杂啊?这里有好几个原因:
- 意图会更加清晰。当你读到
SomeMetaclass(type)
时,你知道接下来要发生什么。 - 你可以使用 OOP 编程。元类可以从元类中继承而来,改写父类的方法。元类甚至还可以使用元类。
- 你可以把代码组织的更好。当你使用元类的时候肯定不会是像我上面举的这种简单场景,通常都是针对比较复杂的问题。将多个方法归总到一个类中会很有帮助,也会使得代码更容易阅读。
- 你可以使用__new__, __init__以及__call__这样的特殊方法。它们能帮你处理不同的任务。就算通常你可以把所有的东西都在__new__里处理掉,有些人还是觉得用__init__更舒服些。
- 哇哦,这块代码使用了 metaclass,绝非善类,我要小心理解!
元类的主要用途是创建 API。一个典型的例子是 Django ORM。它允许你像这样定义:
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
2
3
但是如果你像这样做的话
guy = Person(name='bob', age='35')
print(guy.age)
2
此处并不会返回一个IntegerField
对象,而是会返回一个int
,甚至可以直接从数据库中取出数据。这是有可能的,因为models.Model
定义了__metaclass__
, 并且使用了一些魔法能够将你刚刚定义的简单的 Person 类转变成对数据库的一个复杂 hook。Django 框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的 API 将其化简,通过这个 API 重新创建代码,在背后完成真正的工作。
# 小结
- 在 Python 中,类也是一个对象。
- 类创建实例,元类创建类。
- 元类主要做了三件事:
- 拦截类的创建
- 修改类的定义
- 返回修改后的类
- 当你创建类时,解释器会调用元类来生成它,定义一个继承自 object 的普通类意味着调用 type 来创建它。
# 参考资料
- Python: 陌生的 metaclass (opens new window)
- oop - What are metaclasses in Python? - StackOverflow (opens new window)
- 深刻理解 Python 中的元类(metaclass)以及元类实现单例模式 - 苍松 - 博客园 (opens new window)
- 使用元类 - 廖雪峰的官方网站 (opens new window)
- Python 基础:元类 (opens new window)
- 在 Python 中使用 class decorator 和 metaclass (opens new window)
- 陌生的 metaclass - Python 之旅 - 极客学院 Wiki (opens new window)
- Python 元类 (MetaClass) 小教程 | 三点水 (opens new window)