在继承Django模型的类中使用__new__无效

7 投票
1 回答
3320 浏览
提问于 2025-04-17 12:08

这让我感到困惑,但我找不到明确的答案。这里说的是在从Django模型派生的类中使用__new__方法(更准确地说,是静态方法)。

理想情况下,__new__应该这样使用(因为我们在用Django,可以假设使用的是Python 2.x版本):

class A(object):
  def __new__(self, *args, **kwargs):
    print ("This is A's new function")
    return super(A, self).__new__(self, *args, **kwargs)

  def __init__(self):
    print ("This is A's init function")

从上面的类实例化一个对象是按预期工作的。但是,当在从Django模型派生的类上尝试类似的操作时,就会发生一些意想不到的事情:

class Test(models.Model):
  def __new__(self, *args, **kwargs):
    return super(Test, self).__new__(self, *args, **kwargs)

从上面的类实例化一个对象时会出现这个错误:TypeError: unbound method __new__() must be called with Test instance as first argument (got ModelBase instance instead)

我不明白为什么会发生这种情况(虽然我知道由于Django框架的原因,背后发生了一些类的魔法)。

任何答案都将不胜感激。

1 个回答

11

__new__ 方法的第一个参数并不是实例。因为(a)它是一个静态方法,正如你所提到的,(b) 它的任务是创建一个实例并返回它!__new__ 的第一个参数通常叫做 cls,因为它代表的是类。

这就让你引用的错误信息显得很奇怪;通常这是你在调用一个未绑定的方法(也就是通过 ClassName.methodName 访问到的)时,传入了一个不是该类实例的 self 参数时会出现的错误。然而,静态方法(包括 __new__)并不会变成未绑定的方法,它们只是类的简单函数:

>>> class Foo(object):
    def __new__(cls, *args, **kwargs):
        return object.__new__(cls)
    def method(self):
        pass

>>> class Bar(object):
    pass

>>> Foo.method
<unbound method Foo.method>

>>> Foo.__new__
<function __new__ at 0x0000000002DB1C88>

>>> Foo.method(Bar())
Traceback (most recent call last):
  File "<pyshell#36>", line 1, in <module>
    Foo.method(Bar())
TypeError: unbound method method() must be called with Foo instance as first argument (got Bar instance instead)

>>> Foo.__new__(Bar)
<__main__.Bar object at 0x0000000002DB4F28>

从这里可以看出,__new__ 不应该是未绑定的方法。此外(与普通方法不同),它并不在乎你传入的参数是否一致;我实际上通过调用 Foo.__new__ 成功构造了一个 Bar 的实例,因为 Foo.__new__Bar.__new__ 最终都是以相同的方式实现的(将所有实际工作推迟到 object.__new__)。

不过,这让我简要查看了 Django 的源代码。Django 的 Model 类有一个元类 ModelBase。这个元类相当复杂,我并没有完全搞明白它在做什么,但我注意到了一些很有趣的事情。

ModelBase.__new__(这个元类的 __new__ 方法,负责在你的类定义结束时创建类)在调用它的父类 __new__并没有传入你的类字典。它只传入了一个只有 __module__ 属性的字典。然后,在进行了一系列处理后,它执行了以下操作:

    # Add all attributes to the class.
    for obj_name, obj in attrs.items():
        new_class.add_to_class(obj_name, obj)

(attrs 是包含你在类定义中所有定义的字典,包括你的 __new__ 方法;add_to_class 是一个元类方法,基本上就是 setattr 的功能)。

我现在99%确定问题出在这里,因为 __new__ 是一个奇怪的隐式静态方法。所以与其他静态方法不同的是,你并没有给它加上 staticmethod 装饰器。Python(在某种程度上)只是识别了 __new__ 方法,并将其作为静态方法处理,而不是普通方法[1]。但我敢打赌,这种情况只发生在你在类定义中定义 __new__ 时,而不是通过 setattr 设置时。

所以你的 __new__ 本该是一个静态方法,但没有被 staticmethod 装饰器处理,结果变成了普通的实例方法。当 Python 调用它并传入 Test 时,按照正常的实例创建协议,它就会抱怨说没有得到 Test 的实例。

如果以上都正确,那么:

  1. 这个问题的出现是因为 Django 有点问题,但仅仅是因为它没有考虑到 Python 在 __new__ 作为静态方法时的不一致性。
  2. 你可能可以通过给你的 __new__ 方法加上 @staticmethod 来解决这个问题,尽管你本不应该这样做。

[1] 我认为这是 Python 的一个历史遗留问题,因为 __new__staticmethod 装饰器出现之前就已经引入了,但 __new__ 不能 接受一个实例,因为没有实例可以调用它。

撰写回答