如何从类定义传递参数到元类?

yar*_*elk 18 python metaprogramming metaclass python-2.x

我试图在python 2.7中动态生成类,我想知道你是否可以轻松地从类对象传递参数到元类.

我读过这篇文章很棒,但并没有完全回答这个问题.目前我正在做的事情:

def class_factory(args, to, meta_class):
    Class MyMetaClass(type):
        def __new__(cls, class_name, parents, attrs):
            attrs['args'] = args
            attrs['to'] = to
            attrs['eggs'] = meta_class

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

但这要求我做以下事情

MyClassClass = class_factory('spam', 'and', 'eggs')
my_instance = MyClassClass()
Run Code Online (Sandbox Code Playgroud)

这样做有更干净的方法吗?

Joh*_*ord 28

虽然问题是针对Python 2.7并且已经有了一个很好的答案,但我对Python 3.3有同样的问题,这个帖子最接近我能找到的答案.我通过挖掘Python文档找到了一个更好的Python 3.x解决方案,并且我正在为寻找Python 3.x版本的其他人分享我的发现.

在Python 3.x中将参数传递给元类

在深入研究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及更早版本;参见"UPDATE")或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关于"自定义类创建"的文档.

  • 这非常有帮助.我希望你能发布一个关于Python 3的新问题,然后自己回答 - 我很难找到它. (3认同)
  • @JohnCrawford,这是一项伟大的工作。几个月来,即使彻底阅读文档,我也无法解决这个问题。您的示例需要添加到官方文档中。 (2认同)

mar*_*eau 13

是的,这是一种简单的方法.在元类的__new__()方法中,只需检入作为最后一个参数传递的类字典.class声明中定义的任何内容都将存在.例如:

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)

class MyClass(object):
    __metaclass__ = MyMetaClass
    meta_args = ['spam', 'and', 'eggs']

myobject = MyClass()

from pprint import pprint
pprint(dir(myobject))
print myobject.args, myobject.to, myobject.eggs
Run Code Online (Sandbox Code Playgroud)

输出:

['__class__',
 '__delattr__',
 '__dict__',
 '__doc__',
 '__format__',
 '__getattribute__',
 '__hash__',
 '__init__',
 '__metaclass__',
 '__module__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'args',
 'eggs',
 'to']
spam and eggs
Run Code Online (Sandbox Code Playgroud)

更新

上面的代码只能在Python 2中使用,因为在Python 3中以不兼容的方式更改了指定元类的语法.

要使它在Python 3中运行(但不再在Python 2中),这非常简单,只需MyClass要将定义更改为:

class MyClass(metaclass=MyMetaClass):
    meta_args = ['spam', 'and', 'eggs']
Run Code Online (Sandbox Code Playgroud)

通过显式调用元类并使用创建的类作为基类,通过"即时"创建基类,也可以解决语法差异并生成适用于Python 2和3的代码.

class MyClass(MyMetaClass("NewBaseClass", (object,), {})):
    meta_args = ['spam', 'and', 'eggs']
Run Code Online (Sandbox Code Playgroud)

类构造Python 3也已经过修改,并且添加了支持,允许其他方式传递参数,在某些情况下使用它们可能比这里显示的技术更容易.这一切都取决于你想要完成的事情.

有关新版本Python中流程的描述,请参阅@John Crawford的详细解答.