类也是对象
在弄明白 metaclass 之前,你应该先清楚地知道什么是 Python 中的类(Class)。Python 中的这种从 Smalltalk 语言中借鉴而来的类十分奇怪。
在大部分的编程语言中,类就是一段用来描述怎样产生对象(Object)的代码。Python 也不例外:
但是 Python 中的类可不止是这些。类本身也是对象。
没错,就是对象。
你一使用 class
关键字,Python 就会执行它并创建一个对象。代码:
会在内存中创建一个名为 ObjectCreator 的对象。
这个对象(也就是这个类)本身拥有能力来创建对象(它的实例),这就是它之所以是类的原因。
但仍然,它还是一个对象,因此:
- 你可以将它赋值给一个变量
- 你可以复制(copy)它
- 你可以对它增加属性
- 你可以把它当做函数的参数
例如:
动态地创建类
既然类就是对象,那么你就可以像创建对象一样,动态地创建类。
首先,你可以在一个函数(function)中使用 class
创建类:
但这还不够动态,因为你还是写了定义类的全部代码。
既然类就是对象,那它一定是由什么东西生成的。
当你使用 class
关键字时,Python 会自动地创建一个对象。但就像 Python 中的大部分事情一样,你也可以手动地完成它。
还记得那个叫 type
的函数吗?这个经典地让你知道一个对象是什么类型的函数:
好吧,type
其实有着非常不一般的能力,它可以动态地创建类。type
可以接受那些描述类的参数,然后返回一个类。
(我懂的,根据传递的参数来决定两种完全不同的作用的函数是很二的,但这是 Python 为了向后兼容而不得不做的权衡)
type
是这样用的:
例如:
可以手动地写成这样:
你可能注意到了,我们使用“MyShinyClass”作为了类名并且用它作为了一个变量名来引用这个类。它们是可以不同的,但是没有理由把事情搞得更复杂。
type
接受一个字典(dictionary)来定义类的属性。所以:
可以被翻译成:
和使用一般的类没什么两样:
当然,你还可以继承它:
可以翻译成:
最后,你还可以给你的类定义方法。只需要正确地创建函数并且将它赋值成类的参数就可以了。
至此你应该明白了:在 Python 中,类本身也是对象,你可以动态地创建它。
这就是你在使用 class
关键字时 Python 所做的事情,你也可以用 metaclass 来完成。
什么是 metaclass(终于)
Metaclass 就是一种创建类的东西。
你定义类是为了创建对象,对不对?
但我们已经知道,Python 类也是对象了。
那么,metaclass 就是创建这些对象的东西。它是“类的类”,你可以把它们想象成这样:
你已经知道了,type
可以让你做类似这样的事情:
这是因为 type
函数本质上就是一个 metaclass。type
就是 Python 在底层用来创建所有类的 metaclass。
现在你也许在想,尼玛为什么要把它写成全小写呢,写成 Type
不行吗?
好吧,我猜这是为了和那个创建字符串对象的类 str
保持一致的风格,创建所有整数对象的 int
类也是如此。type
就是那个创建类对象的类。
你可以用 __class__
属性来验证。
在 Python 中,所有的东西,我是说一切的东西,都是一个对象。包括整数、字符串、函数和类。它们全都是对象,并且它们全都是被一个类来创建的:
现在想想,__class__
的 __class__
又是什么呢?
所以,metaclass 就是创建类对象的东西。
如果你乐意你可以称它为“类工厂”。
内置的 type
就是 Python 使用的 metaclass,当然,你也可以创建你自己的 metaclass。
__metaclass__
属性
你可以在编写类的时候增加一个 __metaclass__
属性:
如果你这样写,Python 将会使用这个 metaclass 来创建 Foo
类。
小心点,这是一种奇技淫巧。
你先写下了 class Foo(object)
,但是类对象 Foo
还并不会在内存中创建。
Python 会检查类中有没有 __metaclass__
声明,如果有的话,就用它来创建类 Foo
。如果没有,就还是用 type
来创建这个类。
熟记上面这段话。
当你这样做的时候:
Python 会做下面这几件事:
Foo
中有 __metaclass__
属性吗?
如果有的话,就用这个 __metaclass__
来在内存中创建一个名为 Foo
的类对象(我是说的类对象,仔细点)。
如果 Python 找不到 __metaclass__
属性,它就会检查 Bar(父类)有没有 __metaclass__
,然后重复同样的动作。
如果 Python 在所有父类中都找不到 __metaclass__
,它就会在模块(Module)级别来找,然后重复同样的动作。
如果还是找不到 __metaclass__
,它就用 type
来创建类对象。
现在的重点是,你可以把什么写成 __metaclass__
呢?
答案就是:可以创建类的东西。
那什么可以创建类呢?type
,使用过它的,或者它的子类。
自定义 metaclass
使用 metaclass 的主要目的就是在创建类的时候自动地改变它。
你通常可以为 API 做这些,因为你需要创建符合上下文环境的类。
这里有个比较蠢的例子,就是你决定把你某个模块里的所有类的属性都写成大写的。有很多种方法可以做到,其中一个就是在模块级别声明一个 __metaclass__
。
这样,这个模块中的所有类都是用这个 __metaclass__
创建的,我们只需要告诉这个 metaclass 把所有的属性转成大写就可以了。
幸运的是,__metaclass__
其实可以是任意能被执行(callable)的东西,它并不要求一定要是一个常规的类(我知道,那些名字中有“类”的东西并不一定要是一个类,猜猜看……这对你有些帮助)。
所以,我们用一个函数来写一个简单的例子作为开头:
现在,用一个真正的类作为 metaclass 来做同样的事情:
但是这还不够面向对象,我们直接调用了 type
而不是重写了父类的 __new__
。我们来写一个:
你可能注意到了那个 upperattr_metaclass
。这玩意儿一点都不特别:一个方法总是把当前实例当做第一个参数传进来。就像普通方法的 self
参数一样。
当然,我用的这个名字可能太长了点,不像 self
这种约定俗成的名字。所以真实产品中的 metaclass 会像这个样子:
我们可以使用 super
让它变得更清晰一点,这样就能更容易地继承(是的,你可以定义 metaclass,继承自 metaclass 或者 type):
就是这样。关于 metaclass 的东西真的就这么多了。
那些使用了 metaclass 代码,并不是因为 metaclass 复杂,而是因为你通常使用 metaclass 来处理那些依赖于自省(introspection)的奇怪的功能或者对类的继承关系进行操作,就像 __dict__
等。
实际上,metaclass 特别适合做一些像巫术之类的事情,因此它有点难懂,但是它本身其实很简单:
- 拦截类的创建
- 修改类
- 返回被修改的类
为什么你要用类 metaclass 而不是函数 metaclass?
既然 __metaclass__
可以是任意的可执行的东西,为什么你应该使用明显更为复杂的类而不是更简单的函数呢?
这样做有几个原因:
- 你的意图更加清晰。当你读到
UpperAttrMetaclass(type)
时,你就知道接下来会发生什么事情 - 你可以使用面向对象编程。metaclass 可以继承自别的 metaclass,重写父类的方法。metaclass 甚至都可以定义 metaclass。
- 你可以更好地组织你的代码。你从来不会使用 metaclass 来做上文例子中那些简单琐碎的事情,它们通常用来干更复杂的事。把多个方法组织到一个类里是一个让代码更容易读的非常有效的方法。
- 你可以 hook on
__new__
,__init__
和__call__
。这可以让你做一些不寻常的事情,即使你可以在__new__
这一个方法中做它们所有能做的事情,有些人则更习惯用__init__
- 它们居然叫「metaclass(元类)」,可恶,这一定意味着些什么!
你到底为什么要使用 metaclass
现在到了考虑这个重要问题的时候了,为什么你要使用这个晦涩难懂又容易出错的特性呢?
好吧,通常来说你不应该用。
Python 大师 Tim Peters
metaclass 最主要的使用场景就是创建 API。典型的一个例子就是 Django ORM。
它可以这样来定义一些东西:
但是如果你这样:
这并不会返回一个 IntegerField
对象。它会返回一个 int
,甚至可以直接从数据库中取出它。
这样之所以可行就是因为 models.Model
定义了 __metaclass__
,它会像有魔法似的返回 Person
,而你仅仅只是在复杂的数据库字段钩子函数里定义了一个简单的语句。
Django 使用 metaclass 让一些很复杂的东西变成了看起来很简单的 API。这些 API 重造出来的幕后代码才是真正工作的代码。
最后的话
首先,你知道了类是一些可以创造实例的对象。
好吧,实际上,类本身就是实例,metaclass 的实例。
Python 里所有的东西都是对象,所有的东西都是类的实例或者 metaclass 的实例。
除了 type
。
type
实际上是它自身的 metaclass。这点是你用纯 Python 代码重现不出来的,它是因为在实现层面做了点带欺骗性质的技巧而产生的结果。
其次,metaclass 是复杂的东西。你可能在做一些简单的修改类的工作时并不是真的需要它。你可以使用其他两种不同的技术来修改类:
- 猴子补丁(monkey patching)
- 类装饰器(class decorators)
99% 你需要修改类的时候,你最好使用这两种技术。
但是 99% 的这种时候,你其实根本就不需要修改类 :-)。