broomrider
broomrider

Reputation: 656

Decorating class with class wrapper and __new__

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

The only way I found is inspect.isbuiltin check on super().__new__ and branching, but this is dirty.

Upvotes: 2

Views: 1201

Answers (1)

jsbueno
jsbueno

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

Related Questions