在元类的`__new__`方法中调用类方法时使用`super`关键字

8

我有一个情况,我的类具有自定义元类,创建时会调用该类的一个类方法,大概像这样:

class Metaclass(type):
    def __new__(cls, name, bases, attrs):
        ...
        new_class = super(Metaclass, cls).__new__(cls, name, bases, attrs)
        ...
        new_class.get_fields() # do something
        ...
        return new_class

class FooBar(object):
    __metaclass__ = Metaclass

    @classmethod
    def get_fields(cls):
        ...

(这样的代码示例在Tastypie中有。)

问题是,如果我想要执行:

class NewBar(FooBar):
    @classmethod
    def get_fields(cls):
        super(NewBar, cls).get_fields()
        ...

这个不工作是因为在调用 super 时还没有创建 NewBar(程序流仍在元类中)。那么,有没有什么解决办法呢?

我知道可能 get_fields 方法可以成为元类的方法,但这将使继承变得更加困难(你将不得不定义新的元类和类本身,对于想要扩展这些类的开发人员来说并不好)。

(Python 2.7。)

3个回答

2
如果在调用get_fieldsNewBar不可用,你仍然可以在cls的MRO中找到它:
@classmethod
def get_fields(cls):
    # we can get invoked before NewBar is available in globals,
    # so get NewBar from cls.__mro__
    NewBar = next(c for c in cls.__mro__
                  if c.__module__ == __name__ and c.__name__ == 'NewBar')
    super(NewBar, cls).get_fields()
    ...

尽管这段代码看起来有点滑稽,但它能正常运行且比在问题中提出的其他替代方案要简单得多。虽然大多数对于super的调用都需要一个非常量的第一个参数(例如未经限定的super(cls, cls))是不正确的并会破坏继承关系,但这个调用是安全的,因为生成器表达式只是一种获取NewBar的非传统方式。
当在MRO中查找类时,我们检查类和模块名称(可以作为__name__使用,如Mitar所指出的),以避免出现误判,如果othermodule.NewBarthismodule.NewBar继承,则会出现误判。

try: super_class = NewBar; except NameError: super_class = cls; super(super_class, cls).get_fields() 这段代码怎么样? - Maciej Gol
@kroolik 这样做是可行的,但它以不同的方式进行了混淆。答案中的代码清楚地表明,我们只是试图找到 NewBar 以便以最常规的方式调用 super。在您的变体中,super 调用显得比必要的更加神奇。 - user4815162342
第三种变体,例如 try: NewBar; except NameError: NewBar = cls; ... 将保留 NameError 技巧,但明确表明我们真正寻找的是 NewBar。它仍然让我感到不安,因为它并不能 确保 它实际上找到了 NewBar。也许在那里加上 assert cls.__name__ == 'NewBar' 会更好。 - user4815162342
我们确实无法确定except:中的clsNewBar,但我们有两种情况:从我们的元类(NewBar.__metaclass__)调用 - 在这里,NewBar尚未完成构建,因此不可用作全局名称,而clsNewBar类对象。第二个是NewBarDeriving.__metaclass__,在这里,我们正在构造一个从NewBar派生的类,因此NewBar是一个有效的标识符,除非有人在NewBar.__metaclass__内部创建了一个新类等。但那是巫术,不是编程。assert很好,因为它通知程序员出现了问题。 - Maciej Gol
说实话,“next()”变量不能保证找到所需的类(考虑“NewBar.get_fields.im_func(<my_evil_class>, fields, excludes)”),但是使用“try: except: assert”则不会出现意外行为,这一点不言自明。 - Maciej Gol
显示剩余3条评论

0

根据@user4815162342的答案,我发现了更简单的解决方案:

try:
    super(NewBar, cls).get_fields()
except NameError, e:
    if 'NewBar' in str(e):
        super(cls, cls).get_fields()
    else:
        raise

简单易懂,但牺牲了正确性。特别是,'NewBar' in str(e) 这一行代码看起来像是一个等待发生的错误,尤其是这种代码需要在每个重写 get_fields 的实现中都存在。为了未来维护该代码的方便性,我强烈建议采用更清晰的方法。 - user4815162342
嗯,是的,但我无法更改原始的 get_fields 方法。它来自外部库。 - Mitar
我明白了。从你的例子中并不清楚FooBar是第一个定义get_fields的,而且已经使用了你的元类,所以可以合理地假设FooBar和整个get_fields接口都是由你定义的。 - user4815162342
那么,元类在你的控制之下吗?如果是这样,我认为我可以修改我的答案,使其在不改变“get_fields”函数签名的情况下工作。 - user4815162342
我已经更新了我的答案,使用第三种解决方案,即在cls.__mro__中查找NewBar。这个方法似乎最不丑陋——没有任何形式的全局变量,但仍需要通过基类搜索字符串。 - user4815162342
显示剩余2条评论

0

我知道这个问题是针对Python 2.7的,但是对于那些使用Python 3.6的人,你可以简单地调用super()

class NewBar(FooBar):
    @classmethod
    def get_fields(cls):
        super().get_fields()
        ...

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