Python -- 元类metaclass详解

学习契机

项目中使用Elasticsearch(ES)存储海量业务数据,基于ES向外提供的API进一层封装,按需处理原始数据提供更精确、更多样化的结果。在研究这一层的代码时接触到@six.add_metaclass(abc.ABCMeta),故而学习一下Python的元类。不过,虽然@six.add_metaclass(abc.ABCMeta)实现上与元类有关,但实际应用只需要调用其接口,并不需要接触后幕后的元类操作。
翻译这篇答案是为了方便自己记忆理解,其实原文中一些地方我自己不是很明白,所以这个翻译会根据自己理解的程度持续更新。

原链接

stackoverflow-What are metaclasses in Python?

Python中的元类是什么

类也是对象

在理解元类之前,需要掌握Python中类概念。Python的类概念稍有奇特,其借鉴于Smalltalk。
在大部分语言中,类用于描述如何生成一个对象,在Python中也是如此:

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

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

但是,在Python中类的意义更多,类同时也是对象。当你使用关键字class时,Python解释器执行代码时会生成一个对象。以下代码会在内存中创建一个名为“ObjectCreator”的对象:

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

这个对象(类)自身可以创建对象(实例),这是为什么它是类的原因。
不过它仍然是一个对象,你可以:

  • 可以将它赋值给变量
  • 可以复制
  • 可以添加属性 TODO 添加属性只是对象的特性?
  • 可以将其当作函数参数传递

举例:

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

动态创建类

既然类也是对象,那就可像其他对象那样,动态创建类。
首先,可以使用关键字class,在函数中创建类:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

但是这还不够动态,因为还是需要自己编写整个类的代码。所以,想一想,这些类既然是对象,就必然是某种东西生成的。当使用class关键字时,Python自动创建了类这个对象,但是像Python中大部分事情一样,Python中也可以手动创建类。
还记type方法吗?一个可以让你知道一个对象是什么类型的方法:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

除此之外,type还有一个完全不同的功能:动态创建类。将类的描述作为参数传递给type,会返回一个类。(我知道,同一个函数根据传参的不同而展示出两种完全不同的功能很不合理,不过这是为了Python的向后兼容。)
type如何创建类:

type(类名,
     父类元祖 (可为空),
     包含键值对属性的字典)

举例:

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

上面这个MyShinyClass类可以用以下方法手动创建:

>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一个类
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # 创建一个类对象
<__main__.MyShinyClass object at 0x8997cec>

应该注意到了,使用”MyShinyClass”作为类名,也将其作为一个变量名,赋值为类的引用。类名和变量名是可以不同的,但是没必要把事情搞复杂。
type可以接受一个定义类属性的字典作为参数:

>>> class Foo(object):
...       bar = True

以上定义等同于:

>>> Foo = type('Foo', (), {'bar':True})

使用起来跟一个普通类一样:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)  # 利用实例打印类属性
True

当然,也可以作为基类,给其他类继承:

>>>   class FooChild(Foo):
...         pass

以上代码等同于:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar属性继承自类Foo
True

你肯定还想为类添加方法。只需要定义一个名称合理的函数,并将这个函数名作为属性传递给type就行:

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

在动态创建一个类之后,为这个类添加更多的方法,就像为一个正常创建的类添加方法一样:

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

现在已经看到:在Python中,类也是对象,可以动态地创建类。当使用关键字class时,Python使用元类像这样创建类的。

什么是元类(终于讲到了)

元类就是创建类的“东西”。我们定义类是为了创建对象,是吧?但是我们认识到在Python中类也是对象,而元类就是创建类这种对象(类)的,它们是类的类,你可以这样理解:

MyClass = MetaClass()
MyObject = MyClass()

你已经看到type可以让你做如下操作:

MyClass = type('MyClass', (), {})

这是因为type方法实际上是一个元类。事实上,type是Python中用于创建所有类的元类。不过现在你一定很奇怪为什么这个类名首字母是小写,而不是Type?我猜这是同str保持一致,str是用来创建string对象的类,int是用来创建integer对象的类,type则是创建类对象的类。
在Python中,一切,对就是一切,都是对象。包括整数、字符串、函数和类。所有东西都是对象,它们都是创建自某个类。
通过__class__属性可以验证这一点:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

那么,一个__class__的__class__属性是什么呢?

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

由此可见:元类确实就是创建类对象的东西。如果你觉得合适你就可以称之为“类工厂”。type是Python使用的内置元类,当然,你也可以自己创建元类。

__metaclass__属性

编写一个类时添加上__metaclass__属性:

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

如果你像上面这样做,Python就会使用元类创建一个Foo类。
要当心了,这里有些小圈套。你先写下了“class Foo(object)”,但此时内存中还没有创建Foo类对象。Python会在类的声明中寻找属性__metaclass_,如果找到了就会使用其创建Foo类;如果没有,会使用type创建这个类。下面这段文字要多读几遍。
当你编写以下代码时:

class Foo(Bar):
    pass

Python做了这些事情:

在类Foo中有定义__metaclass__属性吗?
如果有,则继续;
如果没有,Python会在模块层寻找__metaclass__属性(这只针对没有继承任何其他类的情况);
如果模块层也没有,则会在Bar(第一个父类)中寻找(这就有可能是内置的type)。
这样找到__metaclass__后,使用它在内存中创建名称为Foo的类对象(这边跟上,一个类对象)

需要注意的是,__metaclass__属性不会被继承,但是父类的元类(Bar.__class__)可以被继承:如果Bar的__metaclass__属性定义了使用type()(不是type.__new())创建Bar类,其子类不会继承这个行为。(Be careful here that the metaclass attribute will not be inherited, the metaclass of the parent (Bar.__class__) will be. If Bar used a metaclass attribute that created Bar with type() (and not type.__new__()), the subclasses will not inherit that behavior.)TODO 这边不太理解
现在有个新问题,你可以赋什么值给__metaclass__?
答案是:可以创建一个类的东西。
那什么可以创建类?type、子类化type或者使用type的东西。

自定义元类

元类的主要目的,是在创建类的时候动态地改变类。通常你会想创建符合当前上下文的类供API使用。举一个简单的例子,当你希望一个模块中所有的类属性都是小写时,有几种方法可以实现,其中有一种方法就是在模块层设置__metaclass__。使用这种方法,这个模块中所有的类都将使用此元类创建,我们只需要使元类将所有类属性置为小写。
幸运的是,__metaclass__可以被任意调用,不一定非要是一个正式的类(我知道,名称包含“class”不一定非要是一个类,搞清楚了…这很有用)。
现在先用一个函数举一个简单的例子:

# 元类会自动获取通常传给`type`的参数
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      返回一个类对象,将其属性置为大写
    """

    # 过滤出所有开头不为'__'的属性,置为大写
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # 利用'type'创建类
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr  # 这会影响此模块中所有的类

class Foo():  # global __metaclass__ won't work with "object" though  == 没看懂
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

现在,完成同样的功能,但是为元类定义一个真实的类:

# 记住`type`实际上是一个像`str`和`int`的类,可以用于被继承
class UpperAttrMetaclass(type):
    # __new__放在__init__之前调用,此方法创建对象并反回
    # 而__init__则是初始化作为参数传递给此方法的对象
    # 除非你想控制如何创建一个对象,否则很少用到__new__
    # 在这里,被创建的对象是类,而我们想自定义这个类,所以重写了__new__
    # 如果需要的话你也可以在__init__中做一些操作
    # 一些高级用法会包括重写__call__,不过这里还不需要
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

这不是真正的面向对象(OOP),这里直接调用了type,没有重写或者调用父类的__new__。现在像这样处理:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # 复用type.__new__方法
        # 这是基本的OOP,没什么深奥的
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

你大概发现了传给type的额外的参数upperattr_metaclass。这没什么奇怪的:__new__的第一个参数总是其定义的类。就像类方法中第一个参数总是self。当然,为了清晰期间,这里我起的名字比较长,但是像self这样的参数通常有一个传统的名字。所以真正的产品代码中,元类是像这样的:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

使用super可以更清晰,which will ease inheritance (because yes, you can have metaclasses, inheriting from metaclasses, inheriting from type)TODO:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

以上,关于元类也没有更多了。使用元类的代码比较复杂的原因不在于元类,而在于你通常会依靠自省、操纵继承、__dict__变量等,使用元类做一些晦涩的事情。(it's because you usually use metaclasses to do twisted stuff relying on introspection, manipulating inheritance, vars such as __dict__, etc.)TODO
元类来用于黑魔法时的确特别有用,因为也会将事情搞得很复杂。但就其本身而言,是简单的:

  • 拦截一个类的创建
  • 修改类
  • 返回修改的类

为什么会用元类代替函数?

既然__metaclass__可以被任意调用,为什么要使用明显更复杂的类呢?有这样一些理由:

  • 意图明显。当你看到UpperAttrMetaclass(type),你知道接下来会发生什么。
  • 可以使用OOP。元类可以继承自元类,重写父类的方法,元类甚至可以使用元类。
  • 如果为一个类指定的元类是类而不是方法,这个类的子类将是元类的一个实例。Children of a class will be instances of its metaclass if you specified a metaclass-class, but not with a metaclass-function.TODO
  • 你可以将代码组织得更好。使用元类时肯定不会仅想像上面举的例子那样简单,通常是用于比较复杂的场景。将多个方法组织在一个类中有益于使代码更容易阅读。
  • 你可以使用__new__,__init__ 和 __call__,这些方法可以处理不用的事情。即使很多时候你可以在__new__中完成所有工作,当然,一些人会更习惯用__init__。
  • 这些东西叫 “metaclass”,小心了!这一定很难搞!

为什么使用元类

好了,现在的问题是:为什么要是用这样晦涩且容易出错的特性?其实,通常你不会用:

元类是99%的用户根本不必操心的深度魔法。如果你在考虑是否需要使用,那就不要用(真正需要的用户很清楚他们的需求,根本不需要解释为什么要使用元类)

Python领袖 Tim Peters

使用元类的一个主要场景是创建API。Django ORM是一个典型的例子,它允许你像下面这样定义:

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

如果你这样做:

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

它不会返回一个IntegerField的对象,而是返回一个整数,甚至可以从数据库中直接获取数据。TODO
这是因为,在models.Model中定义了__metaclass__,使用了一些魔法将你定义的简单的Person类转换为一个复杂的数据库挂钩。(turn the Person you just defined with simple statements into a complex hook to a database field.)TODO

Django使用元类对外提供简单的API,简化了一些复杂的东西,API中重建的代码会去完成幕后真正的工作。

结语

首先,类是可以创建实例的对象。类本身是元类的对象:

>>> class Foo(object): pass
>>> id(Foo)
142630324

在Python中,除了type,一切皆对象,一切都是类或者元类的对象。事实上type是自己的元类,这在纯Python中这是无法实现的,这里在实现层上做了一些手段。
其次,元类是复杂的。你可能不希望对非常简单的类使用元类,那么还有其他两种手段用来改变类:

  • monkey patching
  • 类装饰器

如果你需要改变类,99%的情况下使用这两种方法。
但其实98%的情况你根本不需要改变类。

    原文作者:Mounire
    原文地址: https://segmentfault.com/a/1190000013719754
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞