Search code examples
pythongetattrgetattribute

Apply an additional function to direct instance methods


Suppose I have a couple of classes:

class MyBaseClass:
    def method1(self):
        print('Hello 1')

class MyClass2:
    def __init__(self, inp):
        self.old_class = inp

    def __getattr__(self, item):
        if hasattr(self, item):
            print('Hello!')
            return getattr(self,item)
        else:
            return getattr(self.old_class, item)  

    def method2(self):
        print('Hello 2')

What I want to do is print "Hello" when the method called is defined directly as part of that class. For example:

MyBaseClass().method1()
'Hello 1'
MyClass2(MyBaseClass()).method1()
'Hello 1'
MyClass2(MyBaseClass()).method2()
'Hello!'
'Hello 2'

I am also aware that my issue is that __getattr__ isn't called unless __getattribute__ fails. My question is either:

  1. How can I define __getattribute__ to achieve this without running into recursion errors.

or 2) Is there something I can use instead of __getattribute__ to achieve this.

I don't want to add print('Hello!') to the method defined in MyClass2 (e.g. I would like something that generalises to multiple methods without putting print('Hello!') inside each of them.


Solution

  • Sounds like your code design/architecture is not so smart as it may be.

    Usually inheritance and mixins are using to extend classes:

    class Base:
      def base_method(self):
        print("base")
    
    class Sub(Base)
      pass
    
    # will print `base`
    Sub().base_method()
    

    If you want to assign new methods to new classes dynamically you may use type:

    class A:
      pass
    
    def extra_method(self):
      print(self)
    
    B = type("B", (A,), {"extra_method": extra_method})
    
    # will print <__main__.B object at 0x7f0924521630>
    B().extra_method()
    

    But let's suppose that you are completelly understanding what you want to implement and design of code in your question is one possible correct way.

    In this case you have to implement __getattribute__ like:

    class MyBaseClass:
        def method1(self):
            print('Hello 1')
    
    class MyClass2:
        def __init__(self, inp):
            self.old_class = inp
    
        # `__getattr__` is using only when you want to call attribute that is not defined inside the class (`method1` in your case)
        # however you want to print `Hello!` on calling existant attribute (`method2`) so `__getattribute__` should be used
        # `__getattribute__` will be called in any case 
        def __getattribute__(self, item):
            # hasattr can't be used here
            # because hasattr is using `__getattribute__` under the hood
            # and `__getattribute__` is calling `hasattr` that is using `__getattribute__`
            # ...endless recursion
            # to solve this problem we may try to assign method to variable without checking is it exists or not
            # if it not exists internal method will be assigned
            # if not - AttributeError will be raised and method of another class will be called
            try:
                # why we call `super()` here?
                # if we call `__getattribute__` of current class that is calling `__getattribute__` that is calling `__getattribute__`
                # ... endless recursion again
                # So we may use default `__getattribute__` method of parent class (and parent class is `object`) to be able avoid recursion
                my_attr = super().__getattribute__(item)
            except AttributeError:
                # in other way default `__getattribute__` of `old_class`s instance will be called
                # that is simple
                return self.old_class().__getattribute__(item)
    
            # why we need this `if`?
            # when we calling `self.old_class()` on exception
            # `__getattribute__` is calling under the hood again because `old_class` is an attribute of `self`
            # we don't need to print `Hello!` every time when `old_class` is called, right?
            # and that is purpose of this `if`
            if item != 'old_class':
                print('Hello!')
            # finally return desired attribute if it exists
            return my_attr
    
        def method2(self):
            print('Hello 2')