Chrysos
Chrysos

Reputation: 17

How access to 'self' attribut in decorator function added dynamically to an instance

My goal is to be able to adding decorator dynamically to all method of class, before and after instantiation.

I need to use self inner my decoractor added after class is instanciate in variable.

It is working when I add decoractor to class method directly

but not for instantiate class method.

In my example, I have 2 test class, one wrapper apply_trigger, which apply the active_trigger wrapper to all methods of class or instantiate class :


 def active_trigger(func):
     @wraps(func)
     def wrapper(*args, **kwargs):
         def afterexec(obj: object, func,*args, **kwargs):
             a = func(obj,*args, **kwargs)
             # process on obj => SELF
             print(obj)
             return a
         return afterexec(args[0], func, *args[1:], **kwargs)
     return wrapper

def apply_trigger(cls):

    for name, m in inspect.getmembers(cls, lambda x: inspect.isfunction(x) or inspect.ismethod(x)):
        setattr(cls , name, active_trigger(m))
    return cls


class test1():
    def method_test1(self, a:int):
        print(self)
        print(a)

@apply_trigger
class test2():
    

    def method_test2(self,b:int, *args, **kwargs):
        print(b)

    def method_t2ex(self,b:int):
        print(b,'_EXEC_')

I need to add decorator to all methods dynamically . In the case of test2, it is working

t2 = test2()
t2.method_test2(2)

but in the case of decorator added after instanciation, there are no 'self' pass in the decorator active_trigger

t1 = apply_trigger(test1())
t1.method_test1(11)

My debugger show at the wrapper step wrapper(*args, **kwargs) :

for the test2 case. args : (<__main__.test2 object at 0x7feae10bdf90>,2)

for the test1 case. args : (11)

How get the self attribut in test1 while decorator execution ?


UPDATE

Thanks to the explanation of @juanpa.arrivillaga

I had find a way to make with 2 distinct decorator when I have a class or an instance.

I use a decorator with parameter in the case of instance, where I pass the instance as parameter.

 def active_trigger(func):
     @wraps(func)
     def wrapper(*args, **kwargs):
         def afterexec(obj: object, func,*args, **kwargs):
             a = func(obj,*args, **kwargs)
             # process on obj => SELF
             print(obj)
             return a
         return afterexec(args[0], func, *args[1:], **kwargs)
     return wrapper

def active_trigger_instance(self):
    def active_trigger(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            def afterexec(func,*args, **kwargs):
                a = func(*args, **kwargs)
                print(self)
                return a
            return afterexec(func, *args, **kwargs)
        return wrapper
    return active_trigger

def apply_trigger(cls):

    import inspect
    if inspect.isclass(cls):
        for name, m in inspect.getmembers(cls, lambda x: inspect.isfunction(x) or inspect.ismethod(x)):
            setattr(cls , name, active_trigger(m))
    else:
        for name, m in inspect.getmembers(type(cls), lambda x: inspect.isfunction(x) or inspect.ismethod(x)):
            setattr(cls , name, active_trigger_instance(cls)(getattr(cls, name)))
    return cls

now it is working but maybe there a more elegant way to make it

Upvotes: 0

Views: 92

Answers (1)

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 96236

In Python, a method is simply a function that lives in the class namespace, that when called on an instance, receives the instance it is called on as the first positional argument

In other words,

some_instance.some_method(arg, x=11)

Is roughly equivalent to:

type(some_instance).some_method(some_instance, arg, x=11)

For the above magic1 to work then some_method must exist in the class namespace (or in the namespace of a class in the method resolution order -- that is inheritance).

It doesn't matter where the function is defined, it matters in what namespace it is in:

>>> class MyClass:
...     pass
...
>>> my_instance = MyClass()
>>>
>>> def a_method(self):
...     return 42
...
>>> MyClass.a_method = a_method
>>>
>>> my_instance.a_method()
42

Note the namespaces:

>>> from pprint import pprint
>>> pprint(MyClass.__dict__)
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
              'a_method': <function a_method at 0x105a301f0>})
>>> pprint(my_instance.__dict__)
{}

Now, the following will demonstrate that the instance will not be passed to the function if the function exists in the instance namespace instead of the class namespace

>>> def another_method(self):
...     return 88
...
>>> my_instance.another_method = another_method
>>> my_instance.another_method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: another_method() missing 1 required positional argument: 'self'
>>>

And note the namespaces again:

>>> pprint(MyClass.__dict__)
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
              '__doc__': None,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
              'a_method': <function a_method at 0x105a301f0>})
>>> pprint(my_instance.__dict__)
{'another_method': <function another_method at 0x105a30280>}

The above is essentially what you are doing when you do:

t1 = apply_trigger(test1())

Just that *args, **kwargs in your def wrapper definition is preventing a TypeError.

Now, if you want to do this on an instance, my first suggestions is simply not to, and reconsider your design. But if you really want, you can just create an auxilliary function which does the binding of the first argument manually (you could use functools.partial),

def apply_trigger_to_instance(obj):
    ...
    for name, m in inspect.getmembers(cls, lambda x: inspect.isfunction(x) or inspect.ismethod(x)):    
        bound_m = functools.partial(m, obj)
        setattr(cls , name, active_trigger(m))

1 If you want to understand it so it isn't magic, you have to understand the descriptor protocol

Upvotes: 0

Related Questions