Python元类:理解'with_metaclass()'

62
我想问在类的定义中,with_metaclass()调用的含义是什么。
例如:
class Foo(with_metaclass(Cls1, Cls2)):
  • 一个类从元类继承是一种特殊情况吗?
  • 新的类也是元类吗?
2个回答

89

with_metaclass()是由six提供的一个实用的类工厂函数,可使Python 2和3代码的开发更加容易。

它使用了一些巧妙的手法(见下文),通过一个临时元类将元类附加到常规类上,以一种与Python 2和Python 3都兼容的方式。

引用文档:

创建一个新类,其基类为base,元类为metaclass。这是设计用于类声明的,如下所示:

from six import with_metaclass
   
class Meta(type):
    pass

class Base(object):
    pass

class MyClass(with_metaclass(Meta, Base)):
    pass

这是必需的,因为Python 2和3之间附加元类的语法已更改:

Python 2:

class MyClass(object):
    __metaclass__ = Meta

Python 3:

class MyClass(metaclass=Meta):
    pass

with_metaclass() 函数利用元类可以 a) 被子类继承,b) 可以用于生成新类,c) 当您从具有元类的基类派生时,创建实际的子类对象被委托给元类。它有效地创建一个新的临时基类,带有一个临时的 metaclass 元类,当用于创建子类时,交换 临时基类和元类组合与您选择的元类。

def with_metaclass(meta, *bases):
    """Create a base class with a metaclass."""
    # This requires a bit of explanation: the basic idea is to make a dummy
    # metaclass for one level of class instantiation that replaces itself with
    # the actual metaclass.
    class metaclass(type):

        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)

        @classmethod
        def __prepare__(cls, name, this_bases):
            return meta.__prepare__(name, bases)
    return type.__new__(metaclass, 'temporary_class', (), {})

分解上面的内容:

  • type.__new__(metaclass, 'temporary_class', (), {})使用metaclass元类创建一个名为temporary_class的新类对象,除此之外该类对象为空。使用type.__new__(metaclass,...)而不是metaclass(...)来避免使用特殊的metaclass.__new__()实现,这种实现在下一步的操作中需要巧妙地运用。
  • 仅适用于Python 3,当temporary_class作为基类时,Python首先调用metaclass.__prepare__()(将派生类名称(temporary_class,)作为this_bases参数传入)。然后使用既定的元类meta调用meta.__prepare__(),忽略this_bases并传入bases参数。
  • 接下来,在使用metaclass.__prepare__()的返回值作为类属性的基本命名空间之后(或在Python 2上只是使用普通字典),Python调用metaclass.__new__()来创建实际的类。再次将(temporary_class,)作为this_bases元组传递给它,但上面的代码会忽略它并使用bases代替,调用meta(name, bases, d)来创建新的派生类。

因此,使用with_metaclass()会给你一个新的类对象,没有额外的基类

>>> class FooMeta(type): pass
...
>>> with_metaclass(FooMeta)  # returns a temporary_class object
<class '__main__.temporary_class'>
>>> type(with_metaclass(FooMeta))  # which has a custom metaclass
<class '__main__.metaclass'>
>>> class Foo(with_metaclass(FooMeta)): pass
...
>>> Foo.__mro__  # no extra base classes
(<class '__main__.Foo'>, <type 'object'>)
>>> type(Foo) # correct metaclass
<class '__main__.FooMeta'>

2
只是为了澄清,在six中,语法(以匹配Python 2和3的行为)如下:class MyClass(with_metaclass(Meta, object)): pass(其中object是可选的)。 - Andy Hayden
如果您不想在继承中使用Base类,那么在声明MyClass后,只需执行MyClass = with_metaclass(Meta, MyClass),对吗?我认为这与下面的add_metaclass解决方案具有相同的注意事项(双重类创建)。 - DylanYoung
1
@DylanYoung:您不需要担心。with_metaclass()在我写这篇答案大约一年后进行了更新,以避免在类继承层次结构中有一个额外的类。仍然会创建一个临时类,但只是为了拦截类工厂过程。 - Martijn Pieters

28

谢谢!这种方法与 isinstance 没有问题,而其他所有 Py2/3 兼容的元类方式都有问题。 - Jérémie
根据我的经验,装饰器只在某些情况下起作用,因为它是在创建后修改/复制类,而元类的整个目的是确定类的创建方式。我忘记了确切的错误是什么,但那真是一件痛苦的事情;我会避免使用它。 - DylanYoung
@pylang 谢谢你的提示!我希望我能记得我遇到的问题。如果我再次遇到它,我会提交一个错误报告。 - DylanYoung
@pylang 总的来说,我认为这些问题实际上无法解决。事实是,类装饰器在创建类之后运行。这意味着任何具有创建副作用的类都将触发这些副作用两次。如果您认为可以使用类装饰器方法解决问题,我很想听听您的想法。如果有帮助的话,我认为我在创建 Django 中的 ProxyModelBase 元类时遇到了错误。 - DylanYoung
@pylang 实际上,这在补丁说明中已经提到了:“这很不错。它的稍微不符合直觉的效果是,在创建类时首先没有创建元类。如果父类具有一个作为修饰器使用的元类的超类,则可能会导致问题。但这种情况非常罕见,所以可以忽略不计。” - DylanYoung
考虑到这个错误(Python语言设计的一部分):“元类冲突:派生类的元类必须是其所有基类的元类的(非严格)子类”,我不确定为什么评审人认为这应该是“相当罕见”的。 - DylanYoung

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接