XY

没有任何借口!!!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

【转】Python metaclass

Posted on 2017-10-19 22:12  路缘  阅读(147)  评论(0编辑  收藏  举报

转自: http://ju.outofmemory.cn/entry/32434 

 

在回答了 yield 关键字和 decorator 的问题之后,我更明白了,我决定非常详细地回答这个问题。

读前警告:这个回答非常的长。

类也是对象

在弄明白 metaclass 之前,你应该先清楚地知道什么是 Python 中的类(Class)。Python 中的这种从 Smalltalk 语言中借鉴而来的类十分奇怪。

在大部分的编程语言中,类就是一段用来描述怎样产生对象(Object)的代码。Python 也不例外:

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

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

但是 Python 中的类可不止是这些。类本身也是对象。

没错,就是对象。

你一使用 class 关键字,Python 就会执行它并创建一个 对象 。代码:

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

会在内存中创建一个名为 ObjectCreator 的对象。

这个对象(也就是这个类)本身拥有能力来创建对象(它的实例),这就是它之所以是类的原因。

但仍然,它还是一个对象,因此:

  • 你可以将它赋值给一个变量
  • 你可以复制(copy)它
  • 你可以对它增加属性
  • 你可以把它当做函数的参数

例如:

>>> print ObjectCreator # 你可以打印一个 class,因为它是一个对象
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print o
...
>>> echo(ObjectCreator) # 你可以把类当做函数参数传递
<class '__main__.ObjectCreator'>
>>> print hasattr(ObjectCreator, 'new_attribute')
False
>>> ObjectCreator.new_attribute = 'foo' # 你可以对一个类增加属性
>>> print hasattr(ObjectCreator, 'new_attribute')
True
>>> print ObjectCreator.new_attribute
foo
>>> ObjectCreatorMirror = ObjectCreator # 你可以把一个类赋值给一个变量
>>> print ObjectCreatorMirror.new_attribute
foo
>>> print ObjectCreatorMirror()
<__main__.ObjectCreator object at 0x8997b4c>

动态地创建类

既然类就是对象,那么你就可以像创建对象一样,动态地创建类。

首先,你可以在一个函数(function)中使用 class 创建类:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # 返回类,而不是实例
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print MyClass # 这个函数返回的是类,不是实例
<class '__main__.Foo'>
>>> print MyClass() # 你可以用这个类来创建对象
<__main__.Foo object at 0x89c6d4c>

但这还不够动态,因为你还是写了定义类的全部代码。

既然类就是对象,那它一定是由什么东西生成的。

当你使用 class 关键字时,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(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

例如:

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

可以手动地写成这样:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print MyShinyClass
<class '__main__.MyShinyClass'>
>>> print MyShinyClass() # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

你可能注意到了,我们使用“MyShinyClass”作为了类名并且用它作为了一个变量名来引用这个类。它们是可以不同的,但是没有理由把事情搞得更复杂。

type 接受一个字典(dictionary)来定义类的属性。所以:

>>> 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 is inherited from Foo
True

最后,你还可以给你的类定义方法。只需要正确地创建函数并且将它赋值成类的参数就可以了。

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

至此你应该明白了:在 Python 中,类本身也是对象,你可以动态地创建它。

这就是你在使用 class 关键字时 Python 所做的事情,你也可以用 metaclass 来完成。

什么是 metaclass(终于)

Metaclass 就是一种创建类的东西。

你定义类是为了创建对象,对不对?

但我们已经知道,Python 类也是对象了。

那么,metaclass 就是创建这些对象的东西。它是“类的类”,你可以把它们想象成这样:

MyClass = MetaClass()
MyObject = MyClass()

你已经知道了, type 可以让你做类似这样的事情:

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

这是因为 type 函数本质上就是一个 metaclass。 type 就是 Python 在底层用来创建所有类的 metaclass。

现在你也许在想,尼玛为什么要把它写成全小写呢,写成 Type 不行吗?

好吧,我猜这是为了和那个创建字符串对象的类 str 保持一致的风格,创建所有整数对象的 int 类也是如此。 type 就是那个创建类对象的类。

你可以用 __class__ 属性来验证。

在 Python 中,所有的东西,我是说一切的东西,都是一个对象。包括整数、字符串、函数和类。它们全都是对象,并且它们全都是被一个类来创建的:

>>> 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__ 又是什么呢?

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

所以,metaclass 就是创建类对象的东西。

如果你乐意你可以称它为“类工厂”。

内置的 type 就是 Python 使用的 metaclass,当然,你也可以创建你自己的 metaclass。

__metaclass__ 属性

你可以在编写类的时候增加一个 __metaclass__ 属性:

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

如果你这样写,Python 将会使用这个 metaclass 来创建 Foo 类。

小心点,这是一种奇技淫巧。

你先写下了 class Foo(object) ,但是类对象 Foo 还并不会在内存中创建。

Python 会检查类中有没有 __metaclass__ 声明,如果有的话,就用它来创建类 Foo 。如果没有,就还是用 type 来创建这个类。

熟记上面这段话。

当你这样做的时候:

class Foo(Bar):
  pass

Python 会做下面这几件事:

Foo 中有 __metaclass__ 属性吗?

如果有的话,就用这个 __metaclass__ 来在内存中创建一个名为 Foo 的类对象(我是说的类对象,仔细点)。

如果 Python 找不到 __metaclass__ 属性,它就会检查 Bar(父类)有没有 __metaclass__ ,然后重复同样的动作。

如果 Python 在所有父类中都找不到 __metaclass__ ,它就会在模块(Module)级别来找,然后重复同样的动作。

如果还是找不到 __metaclass__ ,它就用 type 来创建类对象。

现在的重点是,你可以把什么写成 __metaclass__ 呢?

答案就是:可以创建类的东西。

那什么可以创建类呢? type ,使用过它的,或者它的子类。

自定义 metaclass

使用 metaclass 的主要目的就是在创建类的时候自动地改变它。

你通常可以为 API 做这些,因为你需要创建符合上下文环境的类。

这里有个比较蠢的例子,就是你决定把你某个模块里的所有类的属性都写成大写的。有很多种方法可以做到,其中一个就是在模块级别声明一个 __metaclass__ 

这样,这个模块中的所有类都是用这个 __metaclass__ 创建的,我们只需要告诉这个 metaclass 把所有的属性转成大写就可以了。

幸运的是, __metaclass__ 其实可以是任意能被执行(callable)的东西,它并不要求一定要是一个常规的类(我知道,那些名字中有“类”的东西并不一定要是一个类,猜猜看……这对你有些帮助)。

所以,我们用一个函数来写一个简单的例子作为开头:

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
  """
    Return a class object, with the list of its attribute turned 
    into uppercase.
  """

  # pick up any attribute that doesn't start with '__'
  attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
  # turn them into uppercase
  uppercase_attr = dict((name.upper(), value) for name, value in attrs)

  # let `type` do the class creation
  return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # this will affect all classes in the module

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" childrend
  bar = 'bip'

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

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

现在,用一个真正的类作为 metaclass 来做同样的事情:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return type(future_class_name, future_class_parents, uppercase_attr)

但是这还不够面向对象,我们直接调用了 type 而不是重写了父类的 __new__ 。我们来写一个:

class UpperAttrMetaclass(type):

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

        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

你可能注意到了那个 upperattr_metaclass 。这玩意儿一点都不特别:一个方法总是把当前实例当做第一个参数传进来。就像普通方法的 self 参数一样。

当然,我用的这个名字可能太长了点,不像 self 这种约定俗成的名字。所以真实产品中的 metaclass 会像这个样子:

class UpperAttrMetaclass(type):

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

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

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

我们可以使用 super 让它变得更清晰一点,这样就能更容易地继承(是的,你可以定义 metaclass,继承自 metaclass 或者 type):

class UpperAttrMetaclass(type):

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

        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

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

就是这样。关于 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

现在到了考虑这个重要问题的时候了,为什么你要使用这个晦涩难懂又容易出错的特性呢?

好吧,通常来说你不应该用。

Metaclass 是 99% 的用户都可以不必考虑的深层技巧。如果你在想你是不是要用 metaclass 的时候,你就真的不要使用(真正需要使用它的人会确切地知道需要它的原因,并且不需要解释为什么需要它)。

Python 大师 Tim Peters

metaclass 最主要的使用场景就是创建 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 对象。它会返回一个 int ,甚至可以直接从数据库中取出它。

这样之所以可行就是因为 models.Model 定义了 __metaclass__ ,它会像有魔法似的返回 Person ,而你仅仅只是在复杂的数据库字段钩子函数里定义了一个简单的语句。

Django 使用 metaclass 让一些很复杂的东西变成了看起来很简单的 API。这些 API 重造出来的幕后代码才是真正工作的代码。

最后的话

首先,你知道了类是一些可以创造实例的对象。

好吧,实际上,类本身就是实例,metaclass 的实例。

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

Python 里所有的东西都是对象,所有的东西都是类的实例或者 metaclass 的实例。

除了 type 

type 实际上是它自身的 metaclass。这点是你用纯 Python 代码重现不出来的,它是因为在实现层面做了点带欺骗性质的技巧而产生的结果。

其次,metaclass 是复杂的东西。你可能在做一些简单的修改类的工作时并不是真的需要它。你可以使用其他两种不同的技术来修改类:

  • 猴子补丁(monkey patching)
  • 类装饰器(class decorators)

99% 你需要修改类的时候,你最好使用这两种技术。

但是 99% 的这种时候,你其实根本就不需要修改类 :-)。