如何从Python 3.x中的类定义传递参数到元类?

How to pass arguments to the metaclass from the class definition in Python 3.x?

这是如何从类定义向元类传递参数的python 3.x版本?问题,根据请求单独列出,因为答案与python 2.x明显不同。

在python 3.x中,如何将参数传递给元类的__prepare____new____init__函数,以便类作者可以向元类提供有关如何创建类的输入?

作为我的用例,我使用元类来实现类及其子类自动注册到pyyaml中,以便加载/保存yaml文件。这涉及到Pyyaml的股票YAMLObjectMetaClass中没有的一些额外的运行时逻辑。此外,我希望允许类作者可选地指定pyyaml用于表示类和/或用于构造和表示的函数对象的标记/标记格式模板。我已经知道我不能用pyyaml的YAMLObjectMetaClass的子类来完成这个任务,因为根据我的代码注释,我们没有访问__new__中的实际类对象的权限,所以我正在编写自己的元类来包装pyyaml的注册函数。

最后,我想做的事情是:

1
2
3
4
5
from myutil import MyYAMLObjectMetaClass

class MyClass(metaclass=MyYAMLObjectMetaClass):
    __metaclassArgs__ = ()
    __metaclassKargs__ = {"tag":"!MyClass"}

…当MyClass类对象被创建时,__metaclassArgs____metaclassKargs__将成为__prepare____new____init__方法的论据。

当然,我可以使用这个问题的python 2.x版本中列出的"保留属性名"方法,但我知道有一种更优雅的方法可用。


在深入研究了python的官方文档之后,我发现python 3.x提供了一种向元类传递参数的本地方法,尽管它并非没有缺陷。

只需在类声明中添加其他关键字参数:

1
2
class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass

…它们会像这样传递到您的元类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyMetaClass(type):

  @classmethod
  def __prepare__(metacls, name, bases, **kargs):
    #kargs = {"myArg1": 1,"myArg2": 2}
    return super().__prepare__(name, bases, **kargs)

  def __new__(metacls, name, bases, namespace, **kargs):
    #kargs = {"myArg1": 1,"myArg2": 2}
    return super().__new__(metacls, name, bases, namespace)
    #DO NOT send"**kargs" to"type.__new__".  It won't catch them and
    #you'll get a"TypeError: type() takes 1 or 3 arguments" exception.

  def __init__(cls, name, bases, namespace, myArg1=7, **kargs):
    #myArg1 = 1  #Included as an example of capturing metaclass args as positional args.
    #kargs = {"myArg2": 2}
    super().__init__(name, bases, namespace)
    #DO NOT send"**kargs" to"type.__init__" in Python 3.5 and older.  You'll get a
    #"TypeError: type.__init__() takes no keyword arguments" exception.

您必须让kargs退出对type.__new__type.__init__的调用(python 3.5及更高版本;请参阅下面的"更新"),否则由于传递的参数太多,将获得TypeError异常。这意味着,当以这种方式传递元类参数时,我们总是必须实现MyMetaClass.__new__MyMetaClass.__init__,以防止自定义关键字参数到达基类type.__new__type.__init__方法。type.__prepare__似乎能很好地处理额外的关键字参数(因此,在本例中,我为什么要传递这些参数,只是为了防止我不知道的某些功能依赖于**kargs,所以定义type.__prepare__是可选的。

更新

在python 3.6中,似乎已经调整了typetype.__init__现在可以优雅地处理额外的关键字参数。您仍然需要定义type.__new__(抛出TypeError: __init_subclass__() takes no keyword arguments异常)。

击穿

在Python3中,通过关键字参数而不是类属性指定元类:

1
2
class MyClass(metaclass=MyMetaClass):
  pass

这句话大致翻译为:

1
MyClass = metaclass(name, bases, **kargs)

…其中metaclass是您传入的"元类"参数的值,name是您的类的字符串名称('MyClass'bases是您传入的任何基类(本例中为零长度的tuple ()),kargs是任何未捕获的关键字参数(本例中为空的dict{})。案例)。

进一步细分,该声明大致翻译为:

1
2
3
namespace = metaclass.__prepare__(name, bases, **kargs)  #`metaclass` passed implicitly since it's a class method.
MyClass = metaclass.__new__(metaclass, name, bases, namespace, **kargs)
metaclass.__init__(MyClass, name, bases, namespace, **kargs)

…其中kargs始终是未捕获关键字参数的dict,我们将其传递给类定义。

分解我上面给出的示例:

1
2
class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass

…大致翻译为:

1
2
3
4
namespace = MyMetaClass.__prepare__('C', (), myArg1=1, myArg2=2)
#namespace={'__module__': '__main__', '__qualname__': 'C'}
C = MyMetaClass.__new__(MyMetaClass, 'C', (), namespace, myArg1=1, myArg2=2)
MyMetaClass.__init__(C, 'C', (), namespace, myArg1=1, myArg2=2)

大部分信息来自于Python关于"定制类创建"的文档。


下面是我对另一个关于元类参数的问题的答案中的一个版本的代码,这个问题已经被更新,以便它在Python2和3中都能工作。它本质上与Benjamin Peterson的六个模块的with_metaclass()函数所做的相同——即在需要时使用所需的元类显式地创建一个新的基类,从而避免由于两个版本的python之间的元类语法差异而导致的错误(因为这样做的方法不会改变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from __future__ import print_function
from pprint import pprint

class MyMetaClass(type):
    def __new__(cls, class_name, parents, attrs):
        if 'meta_args' in attrs:
            meta_args = attrs['meta_args']
            attrs['args'] = meta_args[0]
            attrs['to'] = meta_args[1]
            attrs['eggs'] = meta_args[2]
            del attrs['meta_args'] # clean up
        return type.__new__(cls, class_name, parents, attrs)

# Creates base class on-the-fly using syntax which is valid in both
# Python 2 and 3.
class MyClass(MyMetaClass("NewBaseClass", (object,), {})):
    meta_args = ['spam', 'and', 'eggs']

myobject = MyClass()

pprint(vars(MyClass))
print(myobject.args, myobject.to, myobject.eggs)

输出:

1
2
3
dict_proxy({'to': 'and', '__module__': '__main__', 'args': 'spam',
            'eggs': 'eggs', '__doc__': None})
spam and eggs


下面是在python 3中向元类传递参数的最简单方法:

Python 3 x

1
2
3
4
5
6
7
8
9
10
11
12
class MyMetaclass(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace, custom_arg='default'):
        super().__init__(name, bases, namespace)

        print('Argument is:', custom_arg)


class ExampleClass(metaclass=MyMetaclass, custom_arg='something'):
    pass

您还可以为只使用__init__和其他参数的元类创建一个基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ArgMetaclass(type):
    def __new__(mcs, name, bases, namespace, **kwargs):
        return super().__new__(mcs, name, bases, namespace)


class MyMetaclass(ArgMetaclass):
    def __init__(cls, name, bases, namespace, custom_arg='default'):
        super().__init__(name, bases, namespace)

        print('Argument:', custom_arg)


class ExampleClass(metaclass=MyMetaclass, custom_arg='something'):
    pass

In Python 3, you specify a metaclass via keyword argument rather than class attribute:

值得一提的是,这种样式与Python2不向后兼容。如果要同时支持python 2和3,则应使用:

1
2
3
4
5
6
from six import with_metaclass
# or
from future.utils import with_metaclass

class Form(with_metaclass(MyMetaClass, object)):
    pass