python - 如何将参数从 Python 3.x 中的类定义传递给元类?

标签 python python-3.x metaprogramming metaclass

这是 How to pass arguments to the metaclass from the class definition? 的 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"}

...哪里__metaclassArgs____metaclassKargs__将成为 __prepare__ 的论据, __new__ , 和 __init__ MyYAMLObjectMetaClass 的方法当MyClass正在创建类对象。

当然,我可以使用 the Python 2.x version of this question 中列出的“保留属性名称”方法,但我知道有更优雅的方法可用。

最佳答案

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

只需在您的类声明中添加额外的关键字参数:

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

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

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 中,type 似乎得到了调整,type.__init__ 现在可以优雅地处理额外的关键字参数。您仍然需要定义 type.__new__(抛出 TypeError: __init_subclass__() takes no keyword arguments 异常)。

分割

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

class MyClass(metaclass=MyMetaClass):
  pass

该语句大致翻译为:

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

...其中 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)

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

分解我上面给出的例子:

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

...大致翻译为:

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's Documentation on "Customizing Class Creation" .

关于python - 如何将参数从 Python 3.x 中的类定义传递给元类?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27258557/

相关文章:

python - HttpTrigger 使用python依次执行

python - 同时运行多个预训练的 Tensorflow 网络

python - 使用网格管理器添加小部件后的 tkinter 可滚动 Canvas

postgresql - Psycopg2 在 postgres 数据库中插入 python 字典

python - 通过代理连接发送带有 header 的 aiohttp post 请求

c++ - 遍历std::initializer_list

python - 将 JSON 页面列表转换为一个对象

python - 使用Opencv创建幻灯片

django-models - Pythonic 字典遍历

ruby - 定义类的不同方式如何影响包含作品的方式?