Search code examples
pythonclasstypesdecorator

Inherit and modify methods from a parent class at run time in Python


I have a parent class of which I don't know the methods a priori. Its methods can do arbitrary things such as modifying attributes of the class.

Say:

class Parent():
    def __init__(self):
        self.a = 42
    def arbitrary_name1(self, foo, bar):
        self.a += 1
        return foo + bar
    def arbitrary_name2(self, foo, bar):
        self.a -= 1
        return foo ** 2 + bar
my_parent = Parent()

I want to dynamically create a child class where such methods exist but have been modified : for instance say each original methods should now be called twice and return the list of the results from the original parent method being called twice. Note that methods should modify the instance as the original methods did. Aka if I knew the parent class beforehand and its methods I would have done (in a static fashion):

class MyChild(Parent):
    def __init__(self):
        super().__init__()
    def arbitrary_name1(self, foo, bar):
        list_of_res = []
        for _ in range(2):
            list_of_res.append(super().arbitrary_name1(foo, bar))
        return list_of_res
    def arbitrary_name2(foo, bar):
        list_of_res = []
        for _ in range(2):
            list_of_res.append(super().arbitrary_name2(a))
        return list_of_res

Which would have given me the behavior I want:

my_child = MyChild()

assert my_child.arbitrary_name1(1, 2) == [3, 3]
assert my_child.a == 44
print("Success!")

However you only get a my_parent instance, which can have arbitrary methods and you don't know them in advance. Let's assume further to simplify that you can know the signature of the methods and that they all have the same (one could use inspect.signature if it was not the case or functools.wraps to catch arguments). The way I approached the problem was to use MethodTypes to bind the old methods to the new methods in various ways and did variations of the following:

import re
import types

# Get all methods of parent class dynamically
parent_methods_names = [method_name for method_name in dir(my_parent)
                  if callable(getattr(my_parent, method_name)) and not re.match(r"__.+__", method_name)]


def do_twice(function):
    def wrapped_function(self, foo, bar):
        res = []
        for _ in range(2):
            # Note that self is passed implicitly but can also be passed explicitly
            res.append(function(foo, bar))
        return res
    return wrapped_function



my_dyn_child = Parent()
for method_name in parent_methods_names:
    # following https://stackoverflow.com/questions/962962/python-changing-methods-and-attributes-at-runtime
    f = types.MethodType(do_twice(getattr(my_parent, method_name)), my_dyn_child)
    setattr(my_dyn_child, method_name, f) 

assert getattr(my_dyn_child, parent_methods_names[0])(1, 2) == [3, 3]
assert my_dyn_child.a == 44
print("Success!")
Success!
Traceback (most recent call last):
  File "test.py", line 52, in <module>
    assert my_dyn_child.a == 44
AssertionError

The method seems to work as intended as the first assess passes.

However to my horror my_dyn_child.a = 42 thus the second assert fails. I imagine it is because the self is not bound properly and thus is not the same but there is some dark magic going on.

Is there a way to accomplish the desired behavior ?


Solution

  • You're actually incrementing self.a on my_parent, so if you ran assert my_parent.a == 44 it won't raise the assertion error.

    I suppose this is because getattr(my_parent, method_name) gets a method that is already bound (to my_parent), so future calls to it reference my_parent and not my_dyn_child.

    You should be able to fix it by instead calling

    f = types.MethodType(do_twice(getattr(my_dyn_child, method_name)), my_dyn_child)
    

    This way all method calls are referencing the same self (as actual subclassing would do). This works because my_dyn_child begins life as an instance of Parent anyway, so has the same original methods (at least until we replace them. wrapped_function must have a reference to the pre-replaced methods as they are still being called).

    All that said, depending on what you're actually trying to achieve it feels like there must be a better way.