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__,即使我们恢复一切以使得没有可观察到的净变化,这个逻辑也会被破坏?
您看到的问题与 Python 如何查找或选择其参数无关__new__。__new__接收您传递的每个参数。您观察到的效果来自 中的特定代码object.__new__,以及更新 C 级插槽的逻辑中的错误tp_new。
Python 如何将参数传递给__new__. 特殊之处在于object.__new__这些参数的作用。
object.__new__并object.__init__期望有一个参数,即要实例化的类__new__和要初始化的对象__init__。如果它们收到任何额外的参数,它们将忽略额外的参数或抛出异常,具体取决于已覆盖的方法:
__new__,__init__则未重写的object方法应该忽略额外的参数,因此人们不会被迫重写两者。__new__或__init__显式地将额外参数传递给object.__new__或object.__init__,则该object方法应引发异常。__new__都没有__init__被重写,则这两个object方法都应该抛出额外参数的异常。源代码中有一个很大的注释讨论了这一点。
在 C 级别,__new__和__init__对应于类内存布局中的函数指针槽tp_new。tp_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. object和tp_new槽tp_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_new为slot_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__,Example已tp_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_new,object_new所以被调用而不是tuple.__new__。
开发人员多次 尝试修复有缺陷的代码,但每次修复本身都有缺陷并被恢复。第二次尝试更接近了,但打破了多重继承 - 请参阅错误跟踪器中的对话。