Home > Software engineering >  Dynamically override all parent class methods
Dynamically override all parent class methods

Time:09-17

I want to be able to dynamically override all calls sent to methods on the parent class. So far I've only been able to do it for methods not defined in a parent class.

I.e. Given a parent class:

class A:
    def foo(self):
        print("foo")

    def bar(self):
        print("bar")

I want to create a Spy class that inherits from A will print "This is a spy" before calling any method on A, including foo.

class Spy(A):
    pass # What do I do here?

s = Spy()

>>> s.foo()
This is a spy
foo

Current implementation

My current implementation of Spy is this:

class Spy(A):
    def __getattr__(self, method):
        print("This is a spy")
        return getattr(super(A, self), method)

However, this only works for methods not defined in the parent class:

>>> s.baz()
This is a spy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __getattr__
AttributeError: 'super' object has no attribute 'baz'

When I call a method that exists already, it doesn't work:

>>> s.foo()
foo

>>> s.bar()
bar

CodePudding user response:

The following code snippet should do what you want. I have excluded methods starting with __ because these can be problematic (for example, overriding __class__ with a function will cause an error).

class A:
    def foo(self):
        print("foo")

    def bar(self, x):
        print(x)


class Spy(A):

    def functionwrapper(self, functionname):
        originalfunc = getattr(super(), functionname)

        def wrap(*args, **kwargs):
            print("This is a spy: ", end="")
            originalfunc(*args, **kwargs)

        return wrap

    def __init__(self):
        for methodname in [method for method in dir(A) if (method[0:2] != "__")]:
            setattr(self, methodname, self.functionwrapper(methodname))


s = Spy()
s.foo()
s.bar("variable")

Output

This is a spy: foo
This is a spy: variable

CodePudding user response:

When I call a method that exists already, it doesn't work

That's because __getattr__ is only gonna get called when the default attribute access fails. So if the parent has that method, it gets found and your __getattr__ won't get called.

You need to intercept the __getattribute__ instead:

from types import MethodType
from inspect import isfunction


class A:
    def foo(self):
        print("foo")


class Spy(A):
    def bar(self):
        print("barrrrrrrrr")

    def __getattribute__(self, name):
        # check it instance's namespace
        instance_dict = object.__getattribute__(self, "__dict__")
        if name in instance_dict:
            return instance_dict[name]

        # check its class' namespace
        if name in type(self).__dict__:
            return object.__getattribute__(self, name)

        # check parents
        for cls in type(self).mro()[1:]:
            if name in cls.__dict__:
                member = cls.__dict__[name]
                if isfunction(member):
                    # your code here
                    print("This is a spy")
                    return MethodType(cls.__dict__[name], self)
                return member

        raise AttributeError(f"{type(self)} object has no attribute '{name}'")


s = Spy()
s.foo()
s.bar()
print("-----------------------------------")
s.boo()

output:

This is a spy
foo
barrrrrrrrr
-----------------------------------
Traceback (most recent call last):
  File "...", line 32, in <module>
    s.boo()
  File "...", line 25, in __getattribute__
    raise AttributeError(f"{type(self)} object has no attribute '{name}'")
AttributeError: <class '__main__.Spy'> object has no attribute 'boo'

So this is one way of doing it and I agree that it might be overkill.

You do that in three steps:

  • Check to see it is in the instance's namespace: like the data attributes you usually set in __init__.

  • Check to see it is in its class' namespace: You don't want yout print statement here because it's not sent to the parent.

  • Check its parents

CodePudding user response:

test.py:

import functools
import inspect

TXT = "This is a spy"


def deco(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(TXT)

        return func(*args, **kwargs)

    return wrapper


class A:
    def foo(self):
        print("foo")

    def bar(self):
        print("bar")


class Spy(A):
    def __init__(self):
        self.patch()

    @classmethod
    def patch(cls):
        superclass = cls.__bases__[0]
        for name, value in inspect.getmembers(superclass, inspect.isfunction):
            setattr(cls, name, deco(value))


def main():
    s = Spy()
    s.foo()
    s.bar()


if __name__ == "__main__":
    main()

Test:

$ python test.py 
This is a spy
foo
This is a spy
bar
  • Related