-
-
Notifications
You must be signed in to change notification settings - Fork 29.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cannot set A.__new__
back to object.__new__
once it is set to something else.
#105888
Comments
Implementing that But it shows according to the view from with the Python code, both def __call__(cls, *args, **kwargs):
if cls.__new__ is object.__new__:
newCls = cls.__new__(cls)
else:
newCls = cls.__new__(cls, *args, **kwargs)
cls.__init__(newCls, *args, **kwargs)
return newCls
|
Update: Ignore this post since it is partially incorrect. The behavior of Once it is set during class creation, there is no easy way from Python side to change its value. The subsequent stores of Lines 5412 to 5446 in 4426279
I agree it is a bug.
|
To circumvent it, avoid setting |
I'll try and check what I can do. Maybe I can handover the modified or swapped methods to I'll also investigate another workaround by adding a wrapper for |
For your example: cls.__new__ = cls.__new__ Where can I find the C code for the assignment, which adds an entry to When Another problem: An assignment like the one above shouldn't break the code. I also would like to understand (still need to search it) how |
Just a curious side question: Core features:
I have unit tests (pytest) to check the behavior and performance of my code. I would be open in donating that to CPython so I'm not sure whom to contact by issues or email and speak to. |
@sunmy2019 I fixed your example 😃: The original exampled posted in the morning missed an Assigning something different then class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members):
newClass = type.__new__(cls, className, baseClasses, members)
oldnew = newClass.__new__
newClass.__new__ = 5
newClass.__new__ = oldnew
return newClass
class A(metaclass=M):
def __init__(self, arg):
pass
a = A(5) Error message:
It can be fixed like that: class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members):
newClass = type.__new__(cls, className, baseClasses, members)
oldnew = newClass.__new__
# newClass.__new__ = 5
newClass.__new__ = oldnew
return newClass So the assignment operator has a hidden magic feature if the assign method to |
My example is wrong so I deleted it. |
Update: Ignore this post since it is partially incorrect.
I want to clarify this. slot You can find the assignment here. Lines 6245 to 6284 in 3bb0994
It adds an entry? Yes. You can also verify that After you do class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members, abstract):
newClass = type.__new__(cls, className, baseClasses, members)
if abstract:
def newnew(*_, **__):
...
# keep original __new__ and exchange it with a dummy method throwing an error
newClass.__new_orig__ = newClass.__new__
newClass.__new__ = newnew
else:
# 1. replacing __new__ with original (preserved) method doesn't work
print("before __new__", newClass, newClass.__dict__)
newClass.__new__ = newClass.__new_orig__
print("after __new__", newClass, newClass.__dict__)
return newClass
class A(metaclass=M, abstract=True):
...
class B(A, abstract=False):
...
B()
|
I got your point about
not fail? Also following you explanations, |
I don't follow. Let's consider the following code again:
If what you say was true, then the assignment of 5 wouldn't matter. The class has already been created at that point by Also, when commenting out the assignment of Alternatively, if the creation of the class happens later, i.e., after the completion of We established that example fails if and only if the assignment of The evidence we presented strongly suggest that assignments to |
It is touched on here. Lines 4420 to 4435 in 3bb0994
|
@Paebbels @skoehler You both are correct. I am missing something. I dig a bit deeper. For this code: class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members):
newClass = type.__new__(cls, className, baseClasses, members)
oldnew = newClass.__new__
newClass.__new__ = oldnew
return newClass
class A(metaclass=M):
def __init__(self, arg):
pass
a = A(5) The following things happened.
Lines 6245 to 6284 in 3bb0994
You can see it here. The Lines 4927 to 4938 in 4d140e5
But it does not. It special cases Lines 9814 to 9835 in 4d140e5
So in this example. class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members):
newClass = type.__new__(cls, className, baseClasses, members)
# now tp_new = object_new
oldnew = newClass.__new__
newClass.__new__ = oldnew
# due to the special case in `update_slot`,
# `tp_new` is unchanged
# everything is fine.
return newClass class M(type):
# staticmethod
def __new__(cls, className, baseClasses, members):
newClass = type.__new__(cls, className, baseClasses, members)
# now tp_new = object_new
oldnew = newClass.__new__
newClass.__new__ = 5
# tp_new = slot_tp_new (indicating looking through `newClass.__dict__`)
newClass.__new__ = oldnew
# due to the special case in `update_slot`,
# `tp_new` is left unchanged (`= slot_tp_new`)
# and `newClass.__dict__` is updated. Still looking through `__dict__`.
return newClass
A()
# looking through `A.__dict__`
# found `object.__new__`
# object_new checks `tp_new` of A
# Finally fails here. |
Super weird bug. Looks like we should fix the |
I couldn't follow all the steps yet. I might need to read the C code a few more times to puzzle all parts together. But we know for sure, something is odd in that behavior. Btw. what's the sentence about the beer? Am I and @skoehler qualified for it? |
Unfortunately, the 23-yr old slot updating code was introduced in Python 2.3 721f62e. There are codes relying on this behavior. So at least this part is not changeable. The argument check code is added 12 yrs ago. 96384b9 @Paebbels Will you allow me to edit your post? I want to state the reason and the problem more clearly.
You should ask @gvanrossum. But I guess it was correct when that line was written. Somewhere else broke the things. |
@sunmy2019, yes you can modify the post. |
A.__new__
back to object.__new__
once it is set to something else.
I personally think it is the current design does not allow setting class A:
def __new__(cls, arg):
return super().__new__(cls)
def __init__(self, arg):
pass
A(123) # it passes
A.__new__ = object.__new__
A(123) # it cannot pass
# TypeError: object.__new__() takes exactly one argument (the type to instantiate) I prefer to make it clear in the doc: do not assign |
Ok, now we have understood it. What group of developers is deciding on such a question? I don't think so. |
I think core developers can decide if there are no controversies. Otherwise we require a PEP.
It's just my guess. It may never be never supported. https://godbolt.org/z/eE81aGPMo |
You cannot set
A.__new__
back toobject.__new__
once it is set to something else.The cause of this is analyzed here: #105888 (comment)
Edited for clarity.
Original Post
Summary
If the
__new__
method of a class was replaced by a dummy method and later replaced by it original__new__
, the method has a divergent behavior as before. Before and and after the swap, both methods point to the same physical address, so according to my debugging capabilities from within Python code, both methods are identical, but show a different behavior.As I understood the answer to my StackOverflow question, CPython implements a "forgiving" feature, if only one of the
__new__
or__init__
methods was modified by a user e.g. expecting additional arguments. So the other method ignores them, because__call__
of the meta-class passes args and kwargs to__new__
as well as__init__
.So far so good, but why does
__new__
behave differently in my case if the methods are still identical according the function pointer locations? I my opinion, some internal checks in CPython are not correct and the exception is raised inadvertently.Full reproducer file
Background
I'm implementing several features like abstract methods, abstract classes, must-override methods, singletone behavior, slotted classes (automatic inference of slots) and mixin classes (deferred slots) using a user-defined meta-class called
ExtendedType
. The following code can be found as a whole at pyTooling/pyTooling on the development branch.Depending on the internal algorithms of
ExtendedType
, it might decide a classA
is abstract. If so, the__new__
method is replaced by a dummy method raising an exception (AbstractClassError
). Later, when a classB(A)
inherits fromA
, the meta-class might come to the decision,B
isn't abstract anymore, thus we want to allow the object creation again and allow calling for the original__new__
method. Therefore, the original method is preserved as a field in the class (__orig_new__
).Replacing
__new__
was choosen, because it has no performance impact compared to overriding__call__
in the meta-class, as this is called on every object creation, also on non-abstract classes.(Performance loss is 1:2.47 on CPython 3.10.11 and 1:2.86 on CPython 3.11.3 measured by pytest-benchmark)
To simplify the internal algorithms for the abstractness decision, the meta-class implements a boolean named-parameter abstract.
Bug Report
This causes an error message:
Investigate if swapping methods worked correctly:
Results:
So, the preserved method in
__new_orig__
is identical toobject.__new__
and is again the same after swapping back the__new__
method in classB
.Behavior of Ordinary Classes
Here is a comparison with two ordinary classes
X
andY(X)
:Of cause this will work, but are the new methods different?
Also
X
andY
use the same__new__
method asB
orobject
.Comparing results:
Outputs:
Further Reading:
Your environment
/cc @skoehler
The text was updated successfully, but these errors were encountered: