Frage zu decorator classes

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
skirnir
User
Beiträge: 38
Registriert: Sonntag 25. Januar 2015, 10:59

Hallo!

Beim rumspielen mit Decorators ist mir gestern aufgefallen, dass es anscheinend nicht so einfach ist, Klassenmethoden mit Decorator classes zu dekorieren. Beispiel:

Code: Alles auswählen

from functools import wraps

def f_decorate(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{func=}")
        print(f"{args=}")
        print(f"{kwargs=}")
        return func(*args, **kwargs)

    return wrapper

class c_decorate:
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print(f"{self._func=}")
        print(f"{args=}")
        print(f"{kwargs=}")
        return self._func(*args, **kwargs)

@f_decorate
def f_add(a, b):
    print("this is a function decorated with a function")
    print(f"{a} + {b} = {a+b}")
    return a + b

@c_decorate
def c_add(a, b):
    print("this is a function decorated with a class")
    print(f"{a} + {b} = {a+b}")
    return a + b

class Dings:
    @f_decorate
    def f_add(self, a, b):
        print("this is a method decorated with a function")
        print(f"{a} + {b} = {a+b}")
        return a + b

    @c_decorate
    def c_add(self, a, b):
        print("this is a method decorated with a class")
        print(f"{a} + {b} = {a+b}")
        return a + b

def main():
    print("--------Functions--------------------")
    f_add(1, 2)
    print("-------------------------------------")
    c_add(1, 2)
    print("=====================================")
    print("--------Methods----------------------")
    d = Dings()
    d.f_add(1, 2)
    print("-------------------------------------")
    d.c_add(1, 2)

if __name__ == "__main__":
    main()
Wenn ich das Programm laufen lasse, passiert folgendes:

Code: Alles auswählen

$ python --version
Python 3.13.7
$ python decorator_example.py
--------Functions--------------------
func=<function f_add at 0x7e693dd8f6a0>
args=(1, 2)
kwargs={}
this is a function decorated with a function
1 + 2 = 3
-------------------------------------
self._func=<function c_add at 0x7e693ddebba0>
args=(1, 2)
kwargs={}
this is a function decorated with a class
1 + 2 = 3
=====================================
--------Methods----------------------
func=<function Dings.f_add at 0x7e693ddebce0>
args=(<__main__.Dings object at 0x7e693ef37a10>, 1, 2)
kwargs={}
this is a method decorated with a function
1 + 2 = 3
-------------------------------------
self._func=<function Dings.c_add at 0x7e693ddebe20>
args=(1, 2)
kwargs={}
Traceback (most recent call last):
  File "/home/skirnir/dabble/decorator_example.py", line 61, in <module>
    main()
    ~~~~^^
  File "/home/skirnir/dabble/decorator_example.py", line 58, in main
    d.c_add(1, 2)
    ~~~~~~~^^^^^^
  File "/home/skirnir/dabble/decorator_example.py", line 21, in __call__
    return self._func(*args, **kwargs)
           ~~~~~~~~~~^^^^^^^^^^^^^^^^^
TypeError: Dings.c_add() missing 1 required positional argument: 'b'
Warum geht hier die Klasseninstanz flöten, wenn ich die Methode mit der decorator class dekoriere?
Sirius3
User
Beiträge: 18299
Registriert: Sonntag 21. Oktober 2012, 17:20

Funktionsobjekte im Namensraum einer Klasse bekommen, wenn sie über ihre Instanz aufgerufen werden, automatisch als erstes Argument die Instanz.
f_decorate liefert eine Funktion zurück, also ist alles gut.
c_decorate liefert dagegen eine Instanz mit __call__-Methode; damit funktioniert das automatische Einfügen des ersten Arguments nicht.
imonbln
User
Beiträge: 193
Registriert: Freitag 3. Dezember 2021, 17:07

Im Großen und Ganzen hat @Sirius3 recht mit seiner Aussage und es ist meistens besser, Dekoratoren als Funktionen zu implementieren.
Wenn du aber unbedingt einen Dekorator als Klasse schreiben willst, der auch Klassenmethoden dekorieren kann, kommst du um einen Descriptor (https://docs.python.org/3/howto/descriptor.html) und etwas Boilerplate-Code nicht herum. Du greifst dann in die Objekterzeugung ein und musst selbst dafür sorgen, dass die Methode als erstes Argument self erhält. So ist es jedoch möglich, dass ein Klassendekorator eine Methode einer anderen Klasse dekoriert. Auch wenn ich davon in der Praxis eher abraten würde.

Code: Alles auswählen

class CDecorate:

    def __init__(self, orig_func):
        self._func = orig_func

    def __call__(self, *args, **kwargs):
        print(f"{self._func=}")
        print(f"{args=}")
        print(f"{kwargs=}")
        return self._func(*args, **kwargs)

    def __get__(self, wrapped_instance, owner):
        # pass decorator instance and decorated object instance
        return WrapperHelper(self, wrapped_instance)


class WrapperHelper:

    def __init__(self, decorator_instance, wrapped_instance):
        self.decorator_instance = decorator_instance
        self.wrapped_instance = wrapped_instance

    def __call__(self, *args, **kwargs):
        return self.decorator_instance(self.wrapped_instance, *args, **kwargs)


@CDecorate
def c_add(a, b): 
    print("this is a function decorated with a class")
    print(f"{a} + {b} = {a+b}")
    return a + b 


class Dings:

    @CDecorate
    def c_add(self, a, b): 
        print("this is a method decorated with a class")
        print(f"{a} + {b} = {a+b}")
        return a + b 


def main():
    print("function:")
    c_add(1, 2)
    print("class:")
    Dings.c_add(1, 2)
    
if __name__ == "__main__":
    main()


Wie du siehst, kann der Klassendekorator auf diese Weise Methoden und Funktionen dekorieren.
skirnir
User
Beiträge: 38
Registriert: Sonntag 25. Januar 2015, 10:59

"If it looks like a duck and quacks like a duck, pay close attention to the sound of the quack, it might be a special breed of duck."

Danke euch für die Erklärungen.
Antworten