pythonpython-decoratorscode-inspection

Saving the arguments of a function each time it is called, using a decorator defined in a parent class


I have a series of parent-children classes A(), B(A),... possibly in different modules. A user will import one of these classes and define its own child class Z(X) deriving either from X = A, B,.... Then he will initialize his instance with an initialize method defined in the parent class X, or in his overridden initialize in Z. I want the initialize function to save its arguments each time it is called and I have prepared in A() a save_last_arginfo() function that could be called automatically by a decorator of initialize.

I give an example that works with save_last_arginfo being explicitly called from Z.initialize(). How to replace this call by the decorator defined in A? I am not sure

   import inspect
   import functools

   class A():
        
      lastargsinfo = None
    
      #@decorator   
      def initialize(self, *args, **kwargs):
         pass

      def save_last_arginfo(self):
         args1, varargs1, varkw1, defaults1, = inspect.getfullargspec(self.initialize)[0:4]
         frame = inspect.getouterframes(inspect.currentframe())[1][0]       # will this work when using the decorator
         args2, varargs2, keywords2, locals2 = inspect.getargvalues(frame)
         self.lastarginfo = [args1, defaults1,  locals2] # I'll do a dictionary with args and kwargs later
    
      def decorator(self):                  # is this syntax correct?
         @functools.wraps(self.func)
         def wrapper(*args, **kwargs):
            self.save_last_arginfo()
            return self.func(*args, **kwargs)
         return wrapper

        
   class B(A):
      #@decorator   
      def initialize(self, a, b=1):
         pass
        
   class Z(B):
      #@decorator                       # how to correct this syntax? A.decorator? what to do with self?
      def initialize(self, a, b=1, c=2, **kwargs):
         self.save_last_arginfo()       # how to remove this line and use the decorator instead
         return (a+b)*c


   myZ = Z()
   myZ.initialize(0,b=3, c=8, d=12)
   print(myZ.lastarginfo)
   # returns [['self', 'a', 'b', 'c'], (1, 2), {'self': <Z object at 0x000001BBBF135340>, 'a': 0, 'b': 3, 'c': 8, 'kwargs': {'d': 12}}]

Solution

  • I try to reformulate Daniil answers, keeping it simple for me, and closer to my goals, in order to check whether I have understood and to continue the discussion.

    import inspect
    import functools
    
    class A:
        def __init__(self):
            self.last_call_info = []
           
        def save_last_call(func):
            @functools.wraps(func)
            def wrapper(self, *args, **kwargs):
                result = func(self, *args, **kwargs)
                self.last_call_info  = [inspect.signature(func), args, kwargs]
                return result
            return wrapper
        
        @save_last_call
        def initialize(self, *args, **kwargs):
            pass
            
        @classmethod
        def __init_subclass__(cls, **kwargs):
            setattr(cls, "initialize", cls.save_last_call(cls.initialize))
        
    class B(A):
        def initialize(self, a, x=5, y=8):
            # Do something here
            pass
    
    class Z(B):
        def initialize(self, a, b, x=5, y=8):
            # Do something here
            pass
            
    myZ = Z()
    myZ.initialize(1,2,x=4)
    print(myZ.last_call_info)
    

    outputs: [<Signature (self, a, b, x=5, y=8)>, (1, 2), {'x': 4}]

    Note that in the wrapper, I have to call func BEFORE storing its arguments to avoid a bug occuring when the last programmer who overrides initialize in Z choose to reuse X.initialise (by calling super for instance), so that save_last_call is called twice for two different initialize with different arguments. Example:

    class Z(B):
       def initialize(self, a, b, x=5, y=9):
          super().initialize(a, x=x ,y=y)
          self.b = b
       
    myZ = Z()
    myZ.initialize(1,2,x=4)
    print(myZ.last_call_info )
    

    outputs now the correct result [<Signature (self, a, b, x=5, y=9)>, (1, 2), {'x': 4}]