Reputation: 656
Code:
import functools
class MyInt1(int):
def __new__(cls, x, value):
print("MyInt1.__new__", cls, x, value)
return super().__new__(cls, x, base=2)
def __init__(self, x, value):
print("MyInt1.__init__", self, x, value)
self.value = value
super().__init__()
class MyInt2:
def __init__(self, x, value):
print("MyInt2.__init__", self, x, value)
self.value = value
def decorator(class_):
class Wrapper(class_):
def __new__(cls, *args, **kwargs):
print("Wrapper.__new__", cls, args, kwargs)
obj = super().__new__(cls, *args, **kwargs)
...
return obj
def __init__(self, *args, **kwargs):
print("Wrapper.__init__", self, args, kwargs)
functools.update_wrapper(self, class_)
super().__init__(*args, **kwargs)
return Wrapper
c = decorator(MyInt1)("101", 42)
print(c, c.value)
c = decorator(MyInt2)("101", 42)
print(c, c.value)
Output:
Wrapper.__new__ <class '__main__.decorator.<locals>.Wrapper'> ('101', 42) {}
MyInt1.__new__ <class '__main__.decorator.<locals>.Wrapper'> 101 42
Wrapper.__init__ 5 ('101', 42) {}
MyInt1.__init__ 5 101 42
5 42
Wrapper.__new__ <class '__main__.decorator.<locals>.Wrapper'> ('101', 42) {}
Traceback (most recent call last):
File "tmp2.py", line 42, in <module>
c = decorator(MyInt2)("101", 42)
File "tmp2.py", line 28, in __new__
obj = super().__new__(cls, *args, **kwargs)
TypeError: object() takes no parameters
__new__
does not accept __init__
arguments?The only way I found is inspect.isbuiltin
check on super().__new__
and branching, but this is dirty.
Upvotes: 2
Views: 1201
Reputation: 110438
It is a hard to find detail on Python's base class (object) behavior that is implemented in order to be practical to implement just __init__
when creating new classes:
object
's own __init__
and __new__
methods take no argument besides self
and cls
. However - if they are called from a subclass (which happens to be all other classes defined in Python) - each method of these methods check if the subclass has defined one and not the other of them (i.e., object's __init__
check if the class being instantiated also defined __new__
or not).
If either method finds out the converse method has been overriden and itself had not, it just swallows any extra arguments: therefore an user's class __init__
can have arguments, withouht worrying that the same arguments - which will be passed to object.__new__
will cause an error,
So, the problem you are having is that during this check, for example, object's __new__
finds out your wrapper had __init__
defined - therefore it does not swallow any of the arguments - and errors because there re extra arguments.
The only way to fix it, if you are keeping this pattern, is to reimplement the same logic in your decorator:
def decorator(class_):
def has_method(cls, meth):
# (FIXME:the check bellow does not take in account other applications of this decorator)
return any(meth in ancestor.__dict__ for ancestor in cls.__mro__[:-1]):
def has_new(cls):
return has_method(cls, "__new__")
def has_init(cls):
return has_method(cls, "__init__")
class Wrapper(class_):
def __new__(cls, *args, **kwargs):
print("Wrapper.__new__", cls, args, kwargs)
if (args or kwargs) and not has_new(cls) and has_init(cls):
args, kwargs = (), {}
obj = super().__new__(cls, *args, **kwargs)
...
return obj
def __init__(self, *args, **kwargs):
print("Wrapper.__init__", self, args, kwargs)
functools.update_wrapper(self, class_)
cls = self.__class__
if (args or kwargs) and not has_init(cls) and has_new(cls):
args, kwargs = (), {}
super().__init__(*args, **kwargs)
return Wrapper
https://mail.python.org/pipermail/python-list/2016-March/704027.html is a hint of this behavior existing - I had since seem it in official documentation, but in some place I don't recall which.
Upvotes: 3