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

Posted

技术标签:

【中文标题】如何将参数从 Python 3.x 中的类定义传递给元类?【英文标题】:How to pass arguments to the metaclass from the class definition in Python 3.x? 【发布时间】:2015-01-31 05:44:09 【问题描述】:

这是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"

...当 MyClass 类对象被创建时,__metaclassArgs____metaclassKargs__ 将是 MyYAMLObjectMetaClass__prepare____new____init__ 方法的参数。 p>

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

【问题讨论】:

【参考方案1】:

这是我对 other question 的回答中关于元类参数的代码版本,该参数已更新,以便它可以在 both Python 2 3 中工作. 它本质上与 Benjamin Peterson 的 six 模块的 with_metaclass() 函数所做的相同——即在需要时使用所需的元类显式创建一个新的基类,从而避免由于元类导致的错误两个版本的 Python 之间的语法差异(因为这样做的方式没有改变)。

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)

输出:

dict_proxy('to': 'and', '__module__': '__main__', 'args': 'spam',
            'eggs': 'eggs', '__doc__': None)
spam and eggs

【讨论】:

【参考方案2】:

这是在 Python 3 中将参数传递给元类的最简单方法:

Python 3.x

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__ 的元类创建基类:

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

【讨论】:

【参考方案3】:

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

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

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 3 中,您通过关键字参数而不是类属性指定元类: 这种风格不向后兼容 python2,请参阅***.com/a/32713241/3285282 感谢您的编辑,@drhagen。这让我想起了为什么我写了神秘代码注释“因为我们无法访问__new__ 中的实际类对象”。我对这个问题的 Python 2 版本的原始答案也已更新。【参考方案4】:

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

值得一提的是,这种风格不向后兼容python 2。如果你想同时支持python 2和3,你应该使用:

from six import with_metaclass
# or
from future.utils import with_metaclass

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

【讨论】:

感谢分享,但这个答案只演示了如何以交叉兼容的方式传递元类。使用这种方法如何处理元类的参数?

以上是关于如何将参数从 Python 3.x 中的类定义传递给元类?的主要内容,如果未能解决你的问题,请参考以下文章

android如何为自定义的类传递参数?

如何在 Python 3.x 中传递命令行参数?

从 C++ 调用 python 函数时如何将 C++ 类作为参数传递?

python如何将可选参数或关键字参数从一个函数传递到另一个函数?

思考java中core属性的特殊值,如何传递到底是值ins中的类目,全解答过程

如何将值从活动传递到 Android Studio 中的类?