31

我正在尝试在 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
        ...

但这需要我执行以下操作

MyClassClass = class_factory('spam', 'and', 'eggs')
my_instance = MyClassClass()

有更清洁的方法吗?

4

2 回答 2

52

虽然这个问题是针对 Python 2.7 并且已经有一个很好的答案,但我对 Python 3.3 也有同样的问题,而且这个线程是我在 Google 上找到的最接近答案的东西。通过深入研究 Python 文档,我为 Python 3.x 找到了更好的解决方案,并且我正在与其他来这里寻找 Python 3.x 版本的人分享我的发现。

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

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

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

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

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

class MyMetaClass(type):

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

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

您必须忽略kwargstype.__new__and的调用type.__init__(Python 3.5 及更早版本;请参阅“更新”),否则会TypeError由于传递太多参数而导致异常。这意味着——当以这种方式传入元类参数时——我们总是必须实现MyMetaClass.__new__MyMetaClass.__init__防止我们的自定义关键字参数到达基类type.__new__type.__init__方法。 type.__prepare__似乎可以优雅地处理额外的关键字参数(因此为什么我在示例中传递它们,以防万一有一些我不知道依赖的功能**kwargs),所以定义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, **kwargs)

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

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

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

... wherekwargs总是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 的“自定义类创建”文档

于 2014-08-07T19:57:18.730 回答
17

是的,有一个简单的方法可以做到这一点。在元类的__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

输出:

['__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

更新

上面的代码只能在 Python 2 中工作,因为指定元类的语法在 Python 3 中以不兼容的方式进行了更改。

让它在 Python 3 中工作(但不再在 Python 2 中)非常简单,只需将定义更改MyClass为:

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

还可以通过“即时”创建基类来解决语法差异并生成适用于 Python 2 和 3 的代码,这涉及显式调用元类并使用创建的类作为元类的基类被定义。

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

Python 3 中的类构造也进行了修改,并添加了支持以允许其他方式传递参数,并且在某些情况下使用它们可能比此处显示的技术更容易。这完全取决于您要完成的工作。

有关新版本 Python 中该过程的描述,请参阅@John Crawford 的详细答案。

于 2012-12-07T12:55:43.837 回答