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

Joh*_*ord 13 python metaprogramming metaclass python-3.x

这是如何从类定义向元类传递参数的Python 3.x版本问题,由请求单独列出,因为答案与Python 2.x有很大不同.


在Python 3.x中,如何传递参数给元类的__prepare__,__new____init__功能,这样一类的作者可以对类应该如何被创建的元类给输入?

作为我的用例,我正在使用元类来自动将类及其子类注册到PyYAML中以加载/保存YAML文件.这涉及PyYAML库存中没有的额外运行时逻辑YAMLObjectMetaClass.另外,我想允许类作者有选择地指定PyYAML用来表示用于构造和表示的类和/或函数对象的标记/标记格式模板.我已经发现我不能使用PyYAML的子类YAMLObjectMetaClass来实现这一点 - "因为__new__根据我的代码注释,我们无法访问实际的类对象- 所以我正在写我自己的包含PyYAML注册函数的元类.

最终,我想做的事情是:

from myutil import MyYAMLObjectMetaClass

class MyClass(metaclass=MyYAMLObjectMetaClass):
    __metaclassArgs__ = ()
    __metaclassKargs__ = {"tag": "!MyClass"}
Run Code Online (Sandbox Code Playgroud)

...其中__metaclassArgs__,并__metaclassKargs__会争论去的__prepare__,__new____init__方法MyYAMLObjectMetaClass的时候MyClass是越来越创建的类的对象.

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

Joh*_*ord 13

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

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

class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass
Run Code Online (Sandbox Code Playgroud)

......它们会像这样传递到你的元类中:

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.
Run Code Online (Sandbox Code Playgroud)

你不得不放弃kargs调用type.__new__type.__init__(Python 3.5及更早版本;请参阅下面的"更新"),或者TypeError由于传递了太多参数而导致异常.这意味着 - 当以这种方式传递元类参数时 - 我们总是必须实现MyMetaClass.__new__MyMetaClass.__init__保持我们的自定义关键字参数不会到达基类type.__new__type.__init__方法. type.__prepare__似乎优雅地处理额外的关键字参数(因此,为什么我在示例中传递它们,以防万一有一些我不知道的功能依赖**kargs),所以定义type.__prepare__是可选的.

UPDATE

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

分解

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

class MyClass(metaclass=MyMetaClass):
  pass
Run Code Online (Sandbox Code Playgroud)

该声明大致转化为:

MyClass = metaclass(name, bases, **kargs)
Run Code Online (Sandbox Code Playgroud)

... metaclass传入的"元类"参数的值name是,您的类('MyClass')的字符串名称,bases是您传入的任何基类(()在这种情况下为零长度元组),并且kargs是任何未捕获的关键字参数(dict {}在这种情况下为空).

进一步打破这一点,该声明大致转化为:

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)
Run Code Online (Sandbox Code Playgroud)

... kargs总是dict我们传递给类定义的未捕获的关键字参数.

打破我上面给出的例子:

class C(metaclass=MyMetaClass, myArg1=1, myArg2=2):
  pass
Run Code Online (Sandbox Code Playgroud)

...大致翻译为:

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)
Run Code Online (Sandbox Code Playgroud)

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