How to apply decorators that are classes in other class instance methods?

Asked

Viewed 153 times

6

I’m trying to implement a class that’s a method Decorator python. The idea is to apply it to the methods of other classes. However, it is getting lost in the reference self of the annotated class method.

I did the following MCVE to demonstrate the problem:

import traceback

class Decorator:
    def __init__(self, wrapped):
        self.__wrapped = wrapped

    def __call__(self, *args, **kwargs):
        print(f"PARÂMETROS: args = {args}, kwargs = {kwargs}")
        return self.__wrapped(*args, **kwargs)

class Teste:
    def __init__(self):
        self.__x = 123

    @Decorator
    def foo(self):
        return self.__x

x = Teste()

print("\nInvocando método de instância como se fosse estático.")
try:
    print(Teste.foo(x))
    print("Funcionou!")
except:
    traceback.print_exc()

print("\nInvocando método de instância diretamente.")
try:
    print(x.foo())
    print("Funcionou!")
except:
    traceback.print_exc()

In the code above, I created the class Decorator the instances of which must be applied to methods of other classes such as method decorators. The class Teste is the method containing the foo decorated. The subsequent code attempts to invoke the method foo of Teste in two different ways (as if it were a static method and in the usual way as an instance method).

That is the result:

Invocando método de instância como se fosse estático.
PARÂMETROS: args = (<__main__.Teste object at 0x02BE0E90>,), kwargs = {}
123
Funcionou!

Invocando método de instância diretamente.
PARÂMETROS: args = (), kwargs = {}
Traceback (most recent call last):
  File "mcve.py", line 30, in <module>
    print(x.foo())
  File "mcve.py", line 9, in __call__
    return self.__wrapped(*args, **kwargs)
TypeError: foo() missing 1 required positional argument: 'self'

The result demonstrates that invoking Teste.foo(x) works, but invoke x.foo() does not work. I would like instance methods to be called as such, regardless of the fact that I have applied a method Decorator arbitrary in it. Note that when the method is called such as an instance method, the instance of Teste is not even in the args nor in the kwargs.

I also tried it from here, but again it didn’t work:

class Decorator:
    def __init__(self, wrapped):
        self.__wrapped = wrapped

    def __call__(*args, **kwargs):
        print(f"PARÂMETROS: args = {args}, kwargs = {kwargs}")
        return args[0].__wrapped(*args, **kwargs)

So I ask:

  • How can I make a method Decorator that is a form class that a call x.foo() work?

  • Or otherwise, how can I "fish" the reference to instance of the decorated method (the self of foo) instead of taking the self of own Decorator?

Addendum

Imagining that the class Decorator be it so:

class Decorator:
    def __init__(self, wrapped):
        self.__wrapped = wrapped

    def __call__(self, *args, **kwargs):
        print(f"PARÂMETROS: args = {args}, kwargs = {kwargs}")
        return self.__wrapped(*args, **kwargs)

    def bar(self):
        return 456

The idea is that print(x.foo.bar) also prints 456.

2 answers

4

Contextualization of the error

The problem is that when making the decorated method, the attribute Teste.foo becomes an instance of Decorator, 'Cause memorizing it like that is the equivalent of doing:

class Teste:
    def __init__(self):
        self.__x = 123

    def _foo(self):
        return self.__x

    foo = Decorator(_foo)

And therefore when you do Teste.foo(x), being x your instance, you will actually be doing the equivalent of:

Decorator.__call__(Teste.foo, x)

Being executed, thus, self.__wrapped(*args, **kwargs), in which self will be Teste.foo, __wrapped will be the method Teste._foo and the value of x will go into *args. In this way, the result produced will be the equivalent to call Teste._foo(x), which would be the same as x._foo(), thus producing the expected output.

While, when you do x.foo(), the parameters change because it will be the same as do Decorator.__call__(x.foo). As you can see in the implementation, the method __call__ will call the decorated method by passing only *args and **kwargs and, as in the call is passed nothing but the instance of the decorator, these values will be an empty list and an empty dictionary respectively. As a consequence, by invoking the method decorated in self.__wrapped(*args, **kwargs), no parameter will be passed, which generates the error quoted in the question, because the parameter self is mandatory since it is a method of instance.

Possible solution

What you can do is use a closure in your decorator so that instead of making the attribute Teste.foo be an instance of Decorator you make him a closure defined by this class. This implies that when you call the decorated method, instead of calling the decorated method __call__ of the decorator, you will be invoking a function. Being a function, it does not expect the instance as first parameter, circumventing the problem presented, without losing the reference to the instance of the decorator because, as is a closure, it will keep references to the scope in which it was defined, including the self your decorator’s.

For this, you make the method itself __call__ be the decorator of your methods:

class Decorator:
    def __call__(self, method):
        def wrapper(obj, *args, **kwargs):
            # Aqui você ainda pode utilizar self para acessar a instância do decorador
            return method(obj, *args, **kwargs)
        return wrapper

This way, when you do x.foo() you will be invoking the closure wrapper defined by the class Decorator, passing as first parameter, obj, a reference to the body itself x, generating the expected result.

class Teste:
    def __init__(self):
        self.__x = 123

    @Decorator()
    def foo(self):
        return self.__x

x = Teste()

print(Teste.foo(x))  # 123
print(x.foo())  # 123
  • You may be able to implement different solutions using the method __new__ or descriptors, but need to analyze more calmly in a time of less fatigue.

  • I will test this tomorrow calmly. Anyway, your answer seems very good, already has my +1.

  • Well, I ran some tests, and your solution solves what I put into the mess. However, when I was reducing my original problem to a MCVE, I cut the other methods of the Decorator and with the proposed solution, they become inaccessible. The reason Decorator was returned instead of a closure was so that such methods would be available. Do you know any solution to this case?

  • @Victorstafusa accessible from where? If you go from inside the decorator, even inside the closure Voce can access them. I even put as comment in the code this. Gave some error when trying to play?

  • Externally accessible as x.foo.bar. I edited the question.

  • His answer helped me to solve the problem, but did not completely solve it. After breaking my head for a few days, I arrived at the solution of the answer I posted here. I thank you very much for the help.

Show 1 more comment

1


I thank Anderson Carlos Woss. Despite the his answer have indicated me some ways, did not allow me to access things like x.foo.bar, and so it still wasn’t all I needed.

Researching further on the subject, I also considered this response of Martijn Pieters in SO.en, that despite being for a very different question, there are several factors similar to this from here.

After breaking my head pretty hard for a few days, I came to this:

import traceback
from functools import wraps

class InnerDecorator:
    def __init__(self, real_self, wrapped, other):
        self.__real_self = real_self
        self.__wrapped = wrapped
        self.__other = other

    def __call__(self, *args, **kwargs):
        if self.__real_self is None:
            print(f"PARÂMETROS (ESTÁTICO): args = {args}, kwargs = {kwargs}")
            return self.__wrapped(*args, **kwargs)
        else:
            print(f"PARÂMETROS (INSTÂNCIA): self = {self.__real_self} args = {args}, kwargs = {kwargs}")
            return self.__wrapped(self.__real_self, *args, **kwargs)

    def other(self):
        return self.__other

class Decorator:
    def __init__(self, wrapped):
        self.__wrapped = wrapped

    def __get__(self, obj, objtype = None):
        if self.__wrapped is None:
            raise AttributeError("unreadable attribute")

        @wraps(self.__wrapped)
        def interno():
            return InnerDecorator(obj, self.__wrapped, 77)
        return interno()

    def other(self):
        return self.__get__(None, None).other()

    def __call__(self, *args, **kwargs):
        return self.__get__(None, None)(*args, **kwargs)

@Decorator
def xoom(bar):
    return 500 + bar

class Teste:
    def __init__(self, x):
        self.__x = x

    @Decorator
    def foo(self, bar):
        return self.__x + bar

print("\nInvocando método de instância como se fosse estático.")
try:
    x = Teste(3)
    print(Teste.foo(x, 30))
    print(Teste.foo.other())
    print("Funcionou!")
except:
    traceback.print_exc()

print("\nInvocando método de instância diretamente.")
try:
    x = Teste(5)
    print(x.foo(10))
    print(x.foo.other())
    print("Funcionou!")
except:
    traceback.print_exc()

print("\nInvocando funções.")
try:
    print(xoom(55))
    print(xoom.other())
    print("Funcionou!")
except:
    traceback.print_exc()

Here’s the way out:

Invocando método de instância como se fosse estático.
PARÂMETROS (ESTÁTICO): args = (<__main__.Teste object at 0x02FD5990>, 30), kwargs = {}
33
77
Funcionou!

Invocando método de instância diretamente.
PARÂMETROS (INSTÂNCIA): self = <__main__.Teste object at 0x02FC0F90> args = (10,), kwargs = {}
15
77
Funcionou!

Invocando funções.
PARÂMETROS (ESTÁTICO): args = (55,), kwargs = {}
555
77
Funcionou!

Anyway, it shows that the @Decorator can be applied in a method as if it were static, can be applied in a normal instance method, and can also be applied in any function other than any class method. In addition, it adds a property other to the decorated method.

The main point of improvement is that it took two classes to do this, the Decorator and the InnerDecorator and there is some duplication of code between them. But I am satisfied and I reached the goal that I sought to achieve.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.