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

41

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

有没有更简洁的方法来做这件事?


1
这里你想要实现什么还不太清楚,能否提供一些关于你的最终目标的信息呢?我不确定你是否需要一个__metaclass__ - Thomas Orozco
我不明白你为什么这样做。也许没有函数,只用元类会更好? - alexvassel
1
我和他们在一起;通常这种问题相对于这里众多的问题而言,归功于你,你实际上过度思考了问题,并设计了一个比可能必要的解决方案更为复杂的解决方案。说出来吧,坦白一点,你在想什么?但我确实喜欢你的问题。 - John Mee
你说,“但这需要我做...” 我不明白你想要实现什么代码。你想要一个带参数的类构建器,然后你会想要实例化这个类。这正是你展示的内容。请向我们展示你想要达到的代码,目前还不清楚你的方向。 - Ned Batchelder
2个回答

66
虽然这个问题是关于Python 2.7的,已经有了一个很好的答案,但我对Python 3.3也有同样的问题,而且在谷歌上找到的最接近答案的线程就是这个。通过查阅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.

您需要在调用type.__new__type.__init__时将kwargs排除在外(适用于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 是你传入的 "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)

...其中kwargs总是我们传递给类定义的未捕获关键字参数的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文档中的“自定义类创建”

5
非常有帮助。我希望你能发布一个新问题,专门涉及Python 3,并自己回答它 - 我很难找到相关内容。 - Rick
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - John Crawford
2
@JohnCrawford,这是非常棒的工作。我已经花了几个月的时间仔细阅读文档,但仍然无法理解。您的示例应该被添加到官方文档中。 - kennes
1
值得一提的是,即使您只是继承了一个已经指定了自定义元类的类,您仍然可以添加额外的kwargs。例如: class D(C, myArg2=2) - Ivan Klass
1
微妙之处。当覆盖__init__()并从中调用super().__init__()时,最好将**kwargs传递给后者,即使它通常是空的,因此每当它不为空时会引发TypeError: __init_subclass__() takes no keyword arguments。还要注意,这意味着任何由自定义__init__()处理的关键字参数都需要从kwargs中删除,以避免将它们传递下去。以这种方式完成后,即使type.__init__()稍后开始接受某些参数,代码也应该能够继续工作。 - martineau

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中,类的构建也进行了修改,并添加了支持其他传递参数方式的功能,在某些情况下,使用这些方法可能比本文介绍的技术更容易。一切取决于您想要实现什么目标。
请参阅@John Crawford的详细answer,了解Python新版本中该过程的描述。

请参考此答案,获取适用于Python 2和3的代码版本。 - martineau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接