Search code examples
pythonpython-3.xinheritancepython-decorators

Capturing arguments passed to a constructor using class based decorator without breaking inheritance


Running the code below raises the following exception:

ERROR!
Traceback (most recent call last):
  File "<string>", line 22, in <module>
TypeError: MyDecorator.__init__() takes 2 positional arguments but 4 were given
> 

When an object derived from class Subclass is instanciated, I want to access all the arguments given to the constructor, from my decorator's code. I had to stop before because my current code breaks the inheritance. Could someone steer me in the right direction?

class MyDecorator:
    def __init__(self, original_class):
        self.original_class = original_class

    def __call__(self, *args, **kwargs):
        instance = self.original_class(*args, **kwargs)
        print(f"Decorator: Creating instance with arguments: {args}, {kwargs}")
        return instance

    def __getattr__(self, name):
        # Pass through attribute access to the original class
        return getattr(self.original_class, name)

@MyDecorator
class BaseClass:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

class SubClass(BaseClass):
    def additional_method(self):
        print("Additional method in SubClass")

# Creating an instance of the decorated base class
base_instance = BaseClass("foo", arg2="bar")
print(f"arg1: {base_instance.arg1}, arg2: {base_instance.arg2}")

# Creating an instance of the decorated subclass
sub_instance = SubClass("baz", arg2="qux")
print(f"arg1: {sub_instance.arg1}, arg2: {sub_instance.arg2}")

# Accessing additional method in the subclass
sub_instance.additional_method()

Solution

  • After discussion in the comment, I realize that a custom metaclass would be a simple solution. When you instantiate a class, you're actually calling the class' class' __call__ method which is the metaclass' __call__.

    You can do the interception there:

    class MyMeta(type):
        def __call__(self, *args, **kwargs):
            print(f"Creating instance with arguments: {args}, {kwargs}")
            return super().__call__(*args, **kwargs)
    
    
    class BaseClass(metaclass=MyMeta):
        def __init__(self, arg1, arg2):
            self.arg1 = arg1
            self.arg2 = arg2
    
    
    class SubClass(BaseClass):
        def additional_method(self):
            print("Additional method in SubClass")
    
    
    # Creating an instance of the decorated base class
    base_instance = BaseClass("foo", arg2="bar")
    print(f"arg1: {base_instance.arg1}, arg2: {base_instance.arg2}")
    
    # Creating an instance of the decorated subclass
    sub_instance = SubClass("baz", arg2="qux")
    print(f"arg1: {sub_instance.arg1}, arg2: {sub_instance.arg2}")
    
    # Accessing additional method in the subclass
    sub_instance.additional_method()
    

    output:

    Creating instance with arguments: ('foo',), {'arg2': 'bar'}
    arg1: foo, arg2: bar
    Creating instance with arguments: ('baz',), {'arg2': 'qux'}
    arg1: baz, arg2: qux
    Additional method in SubClass
    

    The point is that the metaclass applies to the whole inheritance chain.

    It is now much better because the isinstance() works normally. In your solution the type of the BaseClass would become MyDecorator which is wrong.