Python 究竟是如何找到“__new__”并选择它的参数的?

Kar*_*tel 4 python metaprogramming

在尝试实现一些我不想进入这里的深层魔法时(如果我得到答案,我应该能够弄清楚),我突然想到,__new__对于定义的类来说,它的工作方式不一样它,至于没有的类。具体来说:当您定义__new__自己时,它将传递镜像 的参数__init__,但默认实现不接受任何参数。这是有道理的,因为它object是一个内置类型,本身不需要这些参数。

然而,它会导致以下行为,我觉得这很令人烦恼:

>>> class example:
...     def __init__(self, x): # a parameter other than `self` is necessary to reproduce
...         pass
>>> example(1) # no problem, we can create instances.
<__main__.example object at 0x...>
>>> example.__new__ # it does exist:
<built-in method __new__ of type object at 0x...>
>>> old_new = example.__new__ # let's store it for later, and try something evil:
>>> example.__new__ = 'broken'
>>> example(1) # Okay, of course that will break it...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object is not callable
>>> example.__new__ = old_new # but we CAN'T FIX IT AGAIN
>>> example(1) # the argument isn't accepted any more:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object.__new__() takes exactly one argument (the type to instantiate)
>>> example() # But we can't omit it either due to __init__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'x'
Run Code Online (Sandbox Code Playgroud)

好的,但这只是因为我们仍然有一些明确附加到 的东西example,所以它隐藏了默认值,这破坏了一些描述符......对吧?除非

>>> del example.__new__ # if we get rid of it, the problem persists
>>> example(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object.__new__() takes exactly one argument (the type to instantiate)
>>> assert example.__new__ is old_new # even though the lookup gives us the same object!
Run Code Online (Sandbox Code Playgroud)

如果我们直接添加和删除属性,而不在中间替换它,同样的事情仍然会发生。简单地分配和删除一个属性就会破坏类,这显然是不可撤销的,并且使得实例化变得不可能。就好像该类有一些隐藏属性告诉它如何调用__new__,而该属性已被悄悄损坏。


当我们example在开始实例化时,Python 实际上是如何找到基数的__new__(它显然找到了object.__new__,但它是直接查找吗object?通过 间接到达那里type?其他什么东西?),以及它如何决定__new__应该在没有参数的情况下调用它,甚至如果我们__new__在类中编写一个方法,它会传递一个参数?为什么如果我们暂时弄乱类' __new__,即使我们恢复一切以使得没有可观察到的净变化,这个逻辑也会被破坏?

use*_*ica 5

您看到的问题与 Python 如何查找或选择其参数无关__new____new__接收您传递的每个参数。您观察到的效果来自 中的特定代码object.__new__,以及更新 C 级插槽的逻辑中的错误tp_new


Python 如何将参数传递给__new__. 特殊之处在于object.__new__这些参数的作用。

object.__new__object.__init__期望有一个参数,即要实例化的类__new__和要初始化的对象__init__。如果它们收到任何额外的参数,它们将忽略额外的参数或抛出异常,具体取决于已覆盖的方法:

  1. 如果一个类恰好重写了 或 之一__new____init__则未重写的object方法应该忽略额外的参数,因此人们不会被迫重写两者。
  2. 如果子类__new____init__显式地将额外参数传递给object.__new__object.__init__,则该object方法应引发异常。
  3. 如果 和__new__都没有__init__被重写,则这两个object方法都应该抛出额外参数的异常。

源代码中有一个很大的注释讨论了这一点。


在 C 级别,__new____init__对应于类内存布局中的函数指针槽tp_newtp_init通常情况下,如果这些方法之一是用 C 实现的,则槽将直接指向 C 级实现,并且将生成包装 C 函数的 Python 方法对象。如果该方法是用Python实现的,则槽将指向该slot_tp_new函数,该函数在MRO中搜索__new__方法对象并调用它。当实例化一个对象时,Python将通过调用and函数指针来调用__new__and 。__init__tp_newtp_init

object.__new__object_new由C级函数实现,object.__init__object_init. objecttp_newtp_init被设置为指向这些函数。

object_new并通过检查类和插槽来object_init 检查它们是否被覆盖。如果指向 以外的其他内容,则已被覆盖,与和类似。tp_newtp_inittp_newobject_new__new__tp_init__init__

static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    if (excess_args(args, kwds)) {
        if (type->tp_new != object_new) {
            PyErr_SetString(PyExc_TypeError,
                            "object.__new__() takes exactly one argument (the type to instantiate)");
            return NULL;
        }
        ...
Run Code Online (Sandbox Code Playgroud)

现在,当您分配或删除 时__new__,Python 必须更新tp_new槽以反映这一点。当您__new__对类进行赋值时,Python 会将类的tp_new槽设置为通用slot_tp_new函数,该函数会搜索__new__方法并调用它。当您删除 __new__时,该类应该tp_new从超类重新继承,但代码有一个错误:

else if (Py_TYPE(descr) == &PyCFunction_Type &&
         PyCFunction_GET_FUNCTION(descr) ==
         (PyCFunction)(void(*)(void))tp_new_wrapper &&
         ptr == (void**)&type->tp_new)
{
    /* The __new__ wrapper is not a wrapper descriptor,
       so must be special-cased differently.
       If we don't do this, creating an instance will
       always use slot_tp_new which will look up
       __new__ in the MRO which will call tp_new_wrapper
       which will look through the base classes looking
       for a static base and call its tp_new (usually
       PyType_GenericNew), after performing various
       sanity checks and constructing a new argument
       list.  Cut all that nonsense short -- this speeds
       up instance creation tremendously. */
    specific = (void *)type->tp_new;
    /* XXX I'm not 100% sure that there isn't a hole
       in this reasoning that requires additional
       sanity checks.  I'll buy the first person to
       point out a bug in this reasoning a beer. */
}
Run Code Online (Sandbox Code Playgroud)

在该specific = (void *)type->tp_new;行中,type是错误的类型 - 这是我们试图更新其插槽的类,而不是我们应该继承的类tp_new

当此代码找到__new__用 C 编写的方法时,它不会更新tp_new为指向相应的 C 函数,而是设置tp_new为它已有的任何值!tp_new一点都没有改变!


因此,最初,您的example类已tp_new设置为object_new,并object_new忽略额外的参数,因为它认为__init__已被覆盖,但__new__并未被覆盖。

当您设置时example.__new__ = 'broken',Python 将example's设置tp_newslot_tp_new。在那之后你所做的任何事情都不会改变tp_new任何其他事情,即使del example.__new__确实应该改变。

object_new发现examples tp_newisslot_tp_new而不是 时object_new,它会拒绝额外的参数并引发异常。


该错误还表现在其他一些方面。例如,

>>> class Example: pass
... 
>>> Example.__new__ = tuple.__new__
>>> Example()
<__main__.Example object at 0x7f9d0a38f400>
Run Code Online (Sandbox Code Playgroud)

分配之前__new__Exampletp_new设置为object_new。当示例执行此操作时Example.__new__ = tuple.__new__,Python 发现tuple.__new__是用 C 实现的,因此无法更新tp_new,并将其设置为object_new。然后,在Example(1, 2, 3),中tuple.__new__ 应该引发异常,因为tuple.__new__不适用于Example

>>> tuple.__new__(Example)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tuple.__new__(Example): Example is not a subtype of tuple
Run Code Online (Sandbox Code Playgroud)

但因为tp_new仍然设置为object_newobject_new所以被调用而不是tuple.__new__


开发人员多次 尝试修复有缺陷的代码但每次修复本身都有缺陷并被恢复。第二次尝试更接近了,但打破了多重继承 - 请参阅错误跟踪器中的对话。