Using __new__ to override __init__ in a subclass

I am interested in using __new__ functionality to inject code into the __init__ function for subclasses. My understanding from the documentation is that python will call __init__ on the instance returned by __new__ . However, my attempts to change the __init__ value in the instance before returning it from __new__ do not seem to work.

 class Parent(object): def __new__(cls, *args, **kwargs): new_object = super(Parent, cls).__new__(cls) user_init = new_object.__init__ def __init__(self, *args, **kwargs): print("New __init__ called") user_init(self, *args, **kwargs) self.extra() print("Replacing __init__") setattr(new_object, '__init__', __init__) return new_object def extra(self): print("Extra called") class Child(Parent): def __init__(self): print("Original __init__ called") super(Child, self).__init__() c = Child() 

The above code prints:

 Replacing __init__ Original __init__ called 

but I expect him to print

 Replacing __init__ New __init__ called Original __init__ called Extra called 

Why not?

It seems to me that Python calls the original __init__ value, regardless of what I set to __new__ . Performing an introspection on c.__init__ shows that the new version is in place, but it was not called as part of creating the object.

+6
source share
2 answers

Well, it is expected that the new object will be empty until __init__ called. Probably, python, as an optimization, does not bother to search for an object and is sent to choose __init__ directly from the class.

Therefore, you will have to change the __init__ the subclasses themselves. Fortunately, Python has a tool for this, metaclasses.

In Python 2, you set up a metaclass by setting a special member:

 class Parent(object): __metaclass__ = Meta ... 

See Python2 Documentation

In Python 3, you set the metaclass through the keyword attribute in the parent list, so

 class Parent(metaclass=Meta): ... 

See Python3 Documentation

The metaclass is the base class for an instance of the class. It must be obtained from type and in it __new__ it can change the created class (I believe that the call __init__ should be called, but the examples override __new__ , so I will go with it), __new__ will look like what you have:

 class Meta(type): def __new__(mcs, name, bases, namespace, **kwargs): new_cls = super(Meta, mcs).__new__(mcs, name, bases, namespace, **kwargs) user_init = new_cls.__init__ def __init__(self, *args, **kwargs): print("New __init__ called") user_init(self, *args, **kwargs) self.extra() print("Replacing __init__") setattr(new_cls, '__init__', __init__) return new_cls 

(using the Python 3 example, but the signature in Python 2 seems the same, but not **kwargs , but adding them should not hurt, I have not tested it).

+1
source

I suspect that the answer is that __init__ is a special function, internally it is defined as a class method and as a result cannot be replaced by reassigning it to an object instance.

In Python, all objects are represented by PyObject in C, which has a pointer to PyTypeObject . It contains a member named tp_init , which I assume contains a pointer to the __init__ function.

Another solution works because we are modifying the class, not the instance of the object.

0
source

All Articles