元类

刚开始学习Python的时候,在廖雪峰老师的教程中说到日常的开发中有80%的可能性不会用到元类,所以当时并没有继续往下深入,的确日常的普通开发中除了在ORM中会碰到一些__metaclass__使用,比较少机会会接触到使用元类。所以就一直没有机会往下学习,最近在阅读项目源码的过程中,看到了元类(metaclass)的使用。借此机会对元类进行了学习。

Python独特的类型系统

Python中所有的东西都是对象

Python开发哲学中简洁性在语言中体现得淋漓尽致。在许多教程和博客中,经常看到一句:在Python中所有东西都是对象。也就说在Python语言中,无论变量、常量、函数甚至是类都是对象。

类本身也是对象

我们学习过OOP,都知道通过类我们能够创建实例(instance),也就是在OOP中我们通过类创建实例。既然在Python中class是对象,那么class也一定是某个类的实例,而创建class的类,就称为元类(metaclass)。我们可以通过解释器来检验这句话

1
2
3
4
5
>>> class Foo(object):
... pass
...
>>> isinstance(Foo, object)
True

和静态语言不同,Python动态语言的特性给予我们非常大的自由,通过元类,我们能够在运行时动态的创建我们所需要的类,又由于类本身就是对象,我们又可以将类存入某个变量中、传入函数中或者作为函数返回值。

元类

如上文所讲,创建class的类就成为元类。Python中的内建类型type就是元类的真身。诶?type不是内建函数吗?怎么又是类型了。是的,type本身是一个内建函数,允许我们查看某个实例的类型。同时,type本身也是一个类,更是一个对象,解释器跟我们解释了这一现象。

1
2
3
4
5
6
>>> type(1)
<class 'int'>
>>> isinstance(type, type)
True
>>> isinstance(type, object)
True

既然type是元类,也就是说我们所有的类都是通过type创建的,同样的我们可以通过解释器来看

1
2
3
4
5
>>> class Foo(object):
... pass
...
>>> type(Foo)
<class 'type'>

又由于Foo是type的实例,所以Foo本身也是object

1
2
>>> isinstance(Foo, object)
True

我们可以通过一张图来了解type, class, objectinstance之间的关系
关系图

类的生成

那么Python如何生成”类实例”呢,首先我们要熟悉一下type的签名。

1
2
class type(object)
class type(name, bases, dict)

我们看到,type函数有两个函数签名,其中一个接受一个参数,另外一个接受三个参数。其中第一个函数接受一个对象,并返回该类的类型(type object), 而第二个函数则创建一个新的类。第一个参数name为类的名称、第二个参数bases为类父类, 第三个参数dict为类的属性。因此我们可以通过下面两种方式来创建Foo类, 这两个类的功能是一样的,并且有相同的类名称

1
2
3
4
5
6
# version 1
class Foo(object):
name = 'Foo'

# version 2
Foo = type('Foo', (object,), dict(name='Foo'))

正如我们看到的,Python可以通过调用type元类在运行阶段创建一个新的class。实际上Python解释器在执行我们的脚本的时候,也是通过调用type来创建类的。如下面这个类的定义

1
2
3
4
5
6
7
class Bar(object):
def __init__(self):
self.name = 'Bar'
self.maker = None

def say_hello(self):
print("Hello world")

Python解释器在阅读完代码后,通过调用type函数在内存中就生成了一个类实例, 也就是class Bar

1
2
say_hello = lambda self: print("Hello world")
Bar = type('Bar', (object,), {'name': 'Bar', 'maker': None, 'say_hello': say_hello})

所以,我们可以通过调用type函数,在运行时动态的创建我们的类,通过dict参数自定义行为。

在默认情况下我们编写的类都是由type负责创建的。

__metaclass__

既然type本身既是类又是实例(创造type也是它自己, 先有鸡还是先有蛋?),那么我们也可以创建一个type的子类。这个子类同样也是元类。我们可以通过制定普通类的元类为我们定制的元类来定制化类的生成。这回轮到我们成为”上帝”了,我们可以随意的修改生成类的行为。这也是Python黑魔法最独特的地方。先以Python2为例(Python3的语法略有不同,下一小节会说)。
Python2中我们可以给类增加__metaclass__来制定创建该类的元类

1
2
3
class Foo(object):
__metaclass__ = something...
# -- snip --

这个something可以是任何能够创建类的东西,包括类和函数。

custom __metaclass__

如何定制化满足我们自己需求的元类的时候,有一些需要注意的小细节。观察下面的例子

1
2
3
4
5
6
7
8
class MetaABCClass(type):
def __new__(cls, clsname, bases, dct):
# 方式1
klass = type.__new__(cls, clsname, bases, dct)
## 方式2
#klass = type(clsname, bases, dct)
setattr(klass, 'abc', 'I have `abc` attr')
return klass

代码中有两种创建class实例的方式,在定制化远元类的时候,我们需要使用第1种方式。
而方式2是一些新手在接触元类的时候经常犯的错误(包括我)。 明明第2中方式也能够创建出新的类,问题出现在哪里呢?

问题就出现在我们创建klass的时候仍然是调用type函数创建的。所以生成的类的元类仍然是type而非我们自己自定义的类。
我们仍然能够在__new__方法中对新建的类做一些自定义的操作,但是方式2的弊端会在类的继承上出现问题。查看如下的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from __future__ import print_function

class MetaABCClass(type):
def __new__(cls, clsname, bases, dct):
print("MetaABCClass is called with %s" % clsname)
klass = type(clsname, bases, dct)
setattr(klass, 'abc', 'I have `abc` attr')
return klass

class Bar(object):
__metaclass__ = MetaABCClass
# -- snip --

class Foo(Bar):
pass
# -- snip --

print("Bar.__class__ is:", Bar.__class__)
print("Foo.__class__ is:", Foo.__class__)

上述的代码的输出如下

1
2
3
MetaABCClass is called with Bar
Bar.__class__ is: <type 'type'>
Foo.__class__ is: <type 'type'>

要解释这个现象我们就需要先介绍一下元类是如何在类的继承中发挥作用的。
当解释器在看到我们定义了class Foo类之后,Python将会检查(这里讨论的都是新式类)

  • Foo 类中是否有 __metaclass__属性
    • 如果有,那么使用__metaclass__属性指定的类,创建类Foo
    • 如果没有,那么将使用父类的__metaclass__来创建类
    • 如果父类没有任何元类,那么将使用type来创建类Foo
      上文中的代码我们不是使用type.__new__()来创建Bar类的而是使用type()函数创建,那么即使我们定义了__metaclass__Bar类的元类还是type,因此,”MetaABCClass is called with Bar” 只会输出一次。

Python3的写法

Python3 不再使用__metaclass__这个属性,取而代之的是下面这种写法

1
2
class Foo(object, metaclass=something):
...

大部分的metaclass的行为和Python2一致,少许不同可以参考PEP 3115

总结

  • Python中所有都是对象,包括类也是对象
  • 创建实例的叫普通类,而创建类的是元类
  • type既是类又是对象,type的元类是它本身
  • 我们可以通过继承type实现自己的元类,并在普通类中指定元类来自定义类的行为。
  • 自定义的元类,一定要使用type.__new__()来创建类,而不是type函数。
Author: lisupy
Link: http://lisupy.github.io/2019/12/15/元类/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏