一文带你完全理解Python中的metaclass

Class也是Object

在理解metaclass之前,我们需要先理解Python中的class。从某种程度上来说,Python中的class的定位比较特殊。

对于大部分面向对象语言来说,class是一段定义了如何产生object的代码块。在Python中这一定义也成立:

>>> class example(object):
...     pass
...
>>> object1 = example()
>>> print(object1)
<__main__.example object at 0x102e26990>

但是在Python中,class并不只有这一角色。class实际上也是object。当我们使用class定义一个类的时候,Python会执行相应代码并在内存中创建一个名为example的object。

但该object(class)是具有创建其他object(instance)的能力的。这也是这个object是一个class的原因。由于本质上class任然是一个object,所以我们可以对class做出以下操作:

  • 我们可以将其赋给一个变量
  • 我们可以对其进行拷贝
  • 我们可以赋给其新的变量
  • 我们可以将其作为参数赋给其他的函数

举例如下:

# print a class since it's an object
>>> print(example)
<class '__main__.example'>

# assign an attribute to the class
>>> print(hasattr(example, 'new_attribute'))
False
>>> example.new_attribute = 'assign an attribute to the class'
>>> print(hasattr(example, 'new_attribute'))
True
>>> print(example.new_attribute)
assign an attribute to the class

# assign the class to a variable
>>> example_mirror = example
>>> print(example_mirror)
<class '__main__.example'>
>>> print(example_mirror())
<__main__.example object at 0x102e26a90>

# pass class as a parameter
>>> def echo(cls):
...     print(cls)
...
>>> echo(example)
<class '__main__.example'>

动态创建class

既然class也是object,那么我们就可以像创建普通的object一样动态创建class。

第一种方法,我们可以在方法中创建class。如下面的例子所示:

>>> def dynamic_class_creater(name):
...     if name == 'name1':
...         class class1(object):
...             pass
...         return class1
...     else:
...         class class2(object):
...             pass
...         return class2
...
>>> first_class = dynamic_class_creater('name1')
>>> print(first_class)
<class '__main__.class1'>
>>> print(first_class())
<__main__.class1 object at 0x10e4149d0>

但通过这种方式创建class并没有特别动态。我们任然需要自己定义类的具体内容。考虑到class也是object,那么也一定有某种方法能够像产生instance一样产生类。

当我们使用class关键字创建类的时候,Python会自动创建对应的object。像Python中其他大多数情况一样,我们也可以手动创建这个class object。这一操作可以通过type()实现。

通常情况下我们可以调用type来得到一个object的类型是什么。如下面的例子所示:

>>> print(type(1))
<type 'int'>

>>> print(type('str'))
<type 'str'>

>>> print(type(example()))
<class '__main__.example'>

>>> print(type(example))
<type 'type'>

在这里我们看到我们所创建example类的type是'type'。这实际上也就是接下来要讨论的内容。既type的完全不同的功能——type可以动态创建class。type()函数可以接收class的描述来作为参数并返回所生成的class object。type同时具有这两个迥异的功能是由于Python兼容性问题导致的。在此我们不做深究。

当使用type创建class时,其用法如下:

type(class_name, tuple_of_parent_class, dict_of_attribute_names_and_values)

其中第二个参数tuple_of_parent_class用来表示继承关系,可以为空。第三个参数用来描述我们所要创建的类所应该具有的attribute。如下面的例子所示:

>>>class class_example(object):
...     pass

上面定义的这个类可以由如下type函数创建:

>>>class_example = type('class_example', (), {}) # create a class on the fly
>>>print(class_example)
<class '__main__.class_example'>
>>> print(class_example()) # get a instance of the class
<__main__.class_example object at 0x10e414b10>

在这个例子中,type所接收的第一个参数'class_example'是该类的类名,同时我们使用了class_example作为存储该class object引用的变量。这二者可以不同。但一般我们没有理由采用不同的名字从而使得代码更加复杂。

我们也可以使用一个字典来定义所创建的class的attribute:

>>> class_example = type('class_example', (), {'attr': 1})
>>> print(class_example)
<class '__main__.class_example'>
>>> print(class_example.attr)
1
>>> print(class_example())
<__main__.class_example object at 0x10e414a90>
>>> print(class_example().attr)
1

上面的例子中type返回的class等同于下面这个class:

>>> class class_example(object):
...     attr = 1

当然,我们也可以用type返回一个继承class_example的类:

>>> child_example = type('child_example', (class_example,), {})
>>> print(child_example)
<class '__main__.child_example'>
>>> print(child_example.attr)
1

上面这个例子中type返回的class等同于如下class:

>>> class child_example(class_example):
...     pass

我们甚至可以动态创建包括方法的类。只要我们创建好方法并将其赋给相应的attribute即可:

>>> def echo(self):
...     print(self.attr)
...
>>> child_example = type('child_example', (class_example,), {'echo': echo})
>>> hasattr(class_example, 'echo')
False
>>> hasattr(child_example, 'echo')
True
>>> child_example().echo()
1

同样,我们也可以先动态创建一个class,然后再赋给其新的方法:

>>> child_example = type('child_example', (class_example,), {})
>>> def another_method(self):
...     print('another method')
...
>>> child_example.another_method = another_method
>>> hasattr(child_example, 'another_method')
True
>>> child_example().another_method()
another method

综上所述,Python中的class其实是一个object,并且我们可以动态创建class。事实上这也是我们在使用class关键字的时候Python所做的事情。Python通过使用metacalss来实现这一过程。

究竟什么是metaclass?

metaclass就是Python中用来创建class object的class。我们可以将其看做能够产生class的类工厂。我们可以通过如下例子理解这个关系:

class = metaclass()
object = class()

从上文中我们知道了type()可以被用来动态创建class,这是因为实际上type是一个metaclass。而且type实际上是Python用在在幕后创建所有class的metaclass。

包括int, string, function, class在内,Python中所有的东西都是object,而所有的object都是被相应的class创造的。我们可以通过__class__的值得知这一点。

>>> age = 24
>>> age.__class__
<type 'int'>

>>> name = 'bob'
>>> name.__class__
<type 'str'>

>>> def foo(): pass
>>> foo.__class__
<type 'function'>

>>> class Bar(object): pass
>>> bar = Bar()
>>> bar.__class__
<class '__main__.Bar'>

那么,这些__class____class__又是什么呢?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> bar.__class__.__class__
<type 'type'>

可以看出,所有的class都来自于typetype,作为metaclass,创建了以上所有的class object。

type是Python定义好的metaclass。当然,我们也可以自定义metaclass。

类的__metaclass__ attribute

当定义class的时候,我们可以使用__metaclass__ attribute来指定用来初始化当前class的metaclass。如下面的例子所示:

class Foo(object):
    __metaclass__ = something
    [other statements...]

如果我们指定了__metaclass__,Python就是使用这个metaclass来生成class Foo

当Python试图创建class Foo的时候,Python会首先在class的定义中寻找__metaclass__ attribute。如果存在__metaclass__,Python将会使用指定的__metaclass__来创建class Foo。如果没有指定的话,Python就会使用默认的type作为metaclas创建Foo

所以,对于下面这个例子:

class Foo(Bar):
    pass

Python首先在Foo中寻找是否存在__metaclass__ attribute。

如果存在的话,Python将使用这个metaclass在内存中创建一个名字为Foo的class object。如果Python

如果class定义中不存在__metaclass__的话,Python将会寻找MODULE级别的__metaclass__。如果存在的话鸠进行与前述相同的操作。但是只有我们定义的class没有继承任何类的情况下,Python才会在MODULE级别寻找__metaclass__。或者说,只有当该类是一个旧类的情况下,Python才会在MODULE级别寻找__metaclass__。(关于新类和旧类的区别,请看这篇文章).

当Python仍然没有找到__metaclass__时,Python将会使用当前类的母类的metaclass来创建当前类。在我们上面这个例子中,Python会使用Foo的母类Bar的metaclass来创建Foo的class object。

同时需要注意的是,在class中定义的__metaclass__ attribute并不会被子类继承。被子类继承的是母类的metaclass,也就是母类的.__class__ attribute。比如上面的例子中,Bar.__class__将会被Foo继承。也就是说,如果Bar定义了一个__metaclass__ attribute来使用type()创建Bar的class object(而非使用type.__new__()),那么Bar的子类,也就是Foo,并不会继承这一行为。

那么问题来了:我们究竟应该在__metaclass__ attribute中定义什么?

答案是:能够创建class的东西。

那么什么能够创建class呢?type,或者任何type的子类。

自定义metaclass

metaclass的主要目的是在class被创建的时候对生成的class进行自动的动态修改。

一般来说,这一点主要应用于API,例如我们想要根据当前的内容创建相匹配的class。

举一个简单的例子如下:我们决定让当前module下所有的class的attribute的名字都是大写。要实现这个功能有很多种方法。使用__metaclass__就是其中之一。

设置了__metaclass__的话,class的创建就会由指定的metaclass处理,那么我们只需要让这个metaclass将所有attribute的名字改成大写即可。

__metaclass__可以是任何Python的callable,不必一定是一个正式的class。

下面我们首先给出一个使用function作为__metaclass__的例子。

# the metaclass will automatically get passed the same argument 
# that is passed to `type()`
def upper_attr(class_name, class_parents, class_attr):
    '''Return a class object, with the list of its attribute turned into 
    uppercase.
    '''
    # pick up any attribute that doesn't start with '__' and turn it into uppercase.
    uppercase_attr = {}
    for name, val in class_attr.items():
        if name.startswith('__'):
            uppercase_attr[name] = val
        else:
            uppercase_attr[name.upper()] = val
    
    # let `type` do the class creation
    return type(class_name, class_parents, uppercase_attr)


class Foo(object):
    # this __metaclass__ will affect the creation of this new style class
    __metaclass__ = upper_attr
    bar = 'bar'


print(hasattr(Foo), 'bar')
# False

print(hasattr(Foo), 'BAR')
# True

f = Foo()
print(f.BAR)
# 'bar'

接下来我们通过继承type的方式实现一个真正的class形式的metaclass。注意如果尚不清楚__new____init__的作用和区别的,请看这篇文章.

# remember that `type` is actually a just a class like int or str
# so we can inherit from it.

class UpperAttrMetaclass(type):
    '''
    __new__ is the method called before __init__
    It's the function that actually creates the object and returns it.
    __init__ only initialize the object passed as a parameter.
    We rarely use __new__, except when we want to control how the object
    is created.
    For a metaclass, the object created is a class. And since we want to 
    customize it, we need to override __new__.
    We can also do something by overriding __init__ to get customized initialization
    process as well.
    Advanced usage involves override __call__, but we won't talk about this here.
    '''
    def __new__(upperattr_metaclass, class_name, class_parents, class_attr):
        uppercase_attr = {}
        for name, val in class_attr.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return type(class_name, class_parents, uppercase_attr)

但这不是很OOP。我们直接调用了type而非调用type.__new__。那么OOP的做法如下。

class UpperAttrMetaclass(type):
    def __new__(upperattr_metaclass, class_name, class_parents, class_attr):
        uppercase_attr = {}
        for name, val in class_attr.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        # basic OOP. Reuse the parent's `__new__()`
        return type.__new__(upperattr_metaclass, class_name, class_parents, uppercase_attr)

我们注意到,__new__所接收的参数中有一个额外的upperattr_metaclass。这没有什么特别的。如同__init__总是接收调用它的object作为第一个参数一样(惯例上用self来命名__init__所接收的第一个参数),__new__总是接收其被定义在内的class作为第一个参数,就像类方法总是接收其被定义的class作为第一个参数一样(惯例上用cls命名类方法所接收的第一个参数)。

清楚起见,这里给出的例子的变量和方法名都很长。但在实际的应用中,类似于使用selfcls代替第一个参数,我们可以将这些名字替换为更加简洁的形式:

class UpperAttrMetaclass(type):
    def __new__(cls, cls_name, bases, attr_dict):
        uppercase_attr = {}
        for name, val in attr_dict.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return type.__new__(cls, cls_name, bases, uppercase_attr)

通过应用super,我们可以使得上面这段代码更加干净简洁,也使得继承更加容易(我们可能有metaclass继承别的一些metaclass,而这些metaclass又继承type):

class UpperAttrMetaclass(type):
    def __new__(cls, cls_name, bases, attr_dict):
        uppercase_attr = {}
        for name, val in attr_dict.items():
            if name.startswith('__'):
                uppercase_attr[name] = val
            else:
                uppercase_attr[name.upper()] = val
        return super(UpperAttrMetaclass, cls).__new__(cls, cls_name, bases, uppercase_attr)

Voilà!上述基本就是关于metaclass的一切了。

使用metaclass之所以复杂,不是因为其代码实现复杂,而是因为我们一般使用metaclass来做一些逻辑上很复杂的操作,例如自省,修改继承以及改变类的默认attribute如__dict__等。

metaclass的确可以被用来实现一些奇妙的功能,也因此可以用来进行极其复杂的逻辑操作。但是metaclass本身是很简单的:

  • 影响class初始化的过程
  • 修改class的内容
  • 返回修改过的class

为什么我们要使用metaclass,而不是使用一些函数来实现类似的功能?

就像前文所说,__metaclass__实际上可以是任何callable,那么为什么我们还要使用metaclass而不是直接调用这些函数呢?

使用class作为metaclass有如下几个理由:

  • 使用class作为metaclass能够使得我们代码的动机更加明确。比如当我们读到上面所定义的UpperAttrMetaclass(type)代码时,我们清楚地知道接下来这段代码想要干什么(自定义class object初始化的过程)。
  • 我们能够使用OOP的思想进行处理。class作为metaclass可以继承其他的metaclass,重载母类的方法,甚至可以使用别的metaclass。
  • 如果我们使用class作为metaclass,某一使用该metaclass的class的子类将仍是是其metaclass的实例。但这一功能无法通过使用函数作为metaclass实现。
  • 使用metaclass可以使得代码结构更加优美。实际应用中我们很少使用metaclass来实现上面那样简单的功能。使用metaclass往往是为了实现非常复杂的操作。如果使用class作为metaclass,我们就可以把相应的方法封装到这一个metaclass中,使得代码更加易懂。
  • 使用class作为metaclass可以在class中容易的定义__new____init____call__方法。虽然我们在将所有的逻辑都放入__new__中,但有的时候根据需要使用其他几个方法会使得逻辑更加清晰。
  • 额贼!人家名字就叫metaclass。这不是带着个class吗?

为什么我们要使用metaclass呢?

那么究竟为什么我们要使用metaclass这样一个难以理解且容易出错的实现方式呢?

答案是通常情况下我们不需要使用metaclass。

引用Python大师Tim Peters的话来说,就是:

Metaclasses are deeper magic that 99% of users should never worry about. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

metaclass主要的使用情况就是用来创建API。使用metaclass的一个典型的例子是Django ORM。

它是的我们可以使用如下当时定义一个model:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

同时,如果我们调用这个model:

guy = Person(name='bob', age='35')
print(guy.age)

其并不会返回一个IntegerField对象,而是会返回一个int,甚至可以直接从数据库中调用这个值。

正是因为models.Model定义了__metaclass__,并使用了一些操作来将我们使用简单的语句定义的Person转化成了与数据库相应的域相联系的类,这种逻辑才成为可能。

Django使得很多复杂的逻辑仅暴露一个简单的API接口就可以调用,这正是通过metaclass实现的。metaclass会根据需要重新实现这些复杂操作所需要的真正的代码。

再说两句

首先我们知道了Python中的class实际上是object,同时class仍具有创建对应的实例的能力。

实际上class本身也是metaclass的实例。

>>> class Foo(object):
...     pass
...
>>> id(Foo)
4299321816

Python中的任何东西都是object,这些object不是class的实例就是metaclass的实例。

当然,type除外。

type事实上是其自身的metaclass。我们使用Python是无法重复这种实现的。这一逻辑是在Python代码实现的层面定义的。引用一下道德经中的说法,我们可以说Python中type生metaclass,metaclass生class,class生万物

另外,metaclass的应用一般颇为复杂,大多数情况下我们可以使用别的方法实现相同的功能。比如我们可以通过一下两种技术修改class:

  • monkey patching
  • class decorators

99%我们需要改变class的情况下,我们使用上述两种技术可以解决。

但事实是,99%的情况下我们根本不需要改变class。

原文链接:https://stackoverflow.com/a/6581949/6037083

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容