0

I've got a class and would like to apply a decorator to all functions within that class without having to add a function decorator to every single function. I know that there are solutions like the ones explained here How to decorate all functions of a class without typing it over and over for each method? to add a decorator to the entire class. However, I need to have access to all class attributes within the decorator.

So using the decorator from the other solution like so, I'd need to be able to access all of cls's properties in the f

def function_decorator(orig_func):
    def decorator(*args, **kwargs):
        print("Decorating wrapper called for method %s" % orig_func.__name__)
        result = orig_func(*args, **kwargs)
        return result
    return decorator

def class_decorator(decorator):
    def decorate(cls):
        # this doesn't work
        # print(cls.name)
        for attr in cls.__dict__: 
            if callable(getattr(cls, attr)):
                setattr(cls, attr, decorator(getattr(cls, attr)))
        return cls
    return decorate

@class_decorator(function_decorator)
class PersonWithClassDecorator:
    def __init__(self, name):
        self.name = name

    def print_name(self):
        print(self.name) 

me = PersonWithClassDecorator("Me")
me.print_name()

The cls variable is of type <class 'main.PersonWithClassDecorator'>.

Does anyone have an idea on how to achieve this? I also looked into metaclasses but ran into the same issue of not being able to access the attributes of my class. Any help is much appreciated :)

  • you could probably leverage the `__getattribute__` of the class itself to do what you want, then you will have access to the `self` of the class – Nullman Aug 28 '23 at 11:35
  • The way it is defined the `name` property is not initialized at the stage where you want to access/print it. It's rather an instance than a class property. The class is decorated before you create `PersonWithClassDecorator` so as it is the value of `"Me"` just isn't set yet. Or am I reading it wrong ? – Nikolay Hüttenberend Aug 28 '23 at 13:23

1 Answers1

1

Alternative 1

As @Nullman suggested, you can move the decorating into the __getattribute__-hook. This method is called whenever any attibute (including methods) are accessed from an instance of your class.

You can implement __getattribute__ directly in your class or create a mixin that only contains the decorating.

from typing import Any

class Decorate:
    def __getattribute__(self, item: str) -> Any:
        val = super().__getattribute__(item)
        
        if callable(val):
            print("Name", self.name)  # you can access the instance name here
            return function_decorator(val)
        return val

And then inherit from Decorator.

class PersonWithClassDecorator(Decorate): ...

This simple approach has two downsides:

  • the function_decorator is hardcoded into the Decorate class
  • dunder-methods like __init__, __str__ or all the operator hooks are not decorated (internally they are not accessed from the instance, but from the class)

Note: if you use mypy (you should ;-) ) the __getattribute__ hook disables the detection of non-existing attributes. To solve this, wrap the __getattribute__ definition in a if not typing.TYPE_CHECKING block.

Alternative 2

The function_decorator actually has access to instance attributes. You can change it accordingly and use your original approach.

from collections.abc import Callable
from typing import Any

def function_decorator(orig_function: Callable[..., Any]) -> Callable[..., Any]:
    def decorator(self, *args: Any, **kwargs: Any) -> Any:
        print(f"Decorating wrapper called for method {orig_func.__name__}: {self.name}")
        return orig_function(self, *args, **kwargs)
    return decorator

Note: i added type hints and replaced the old %-based string formatting with f-strings.

This has a small problem: self.name is only defined after __init__ was called. You can handle this in different ways:

  • getattr(self, 'name', None) - self.name is then None before init
  • Don't decorate __init__: if orig_func.__name__ == '__init__': return orig_func
  • Use a different decorator for __init__

Downsides:

  • You will also have to handle @classmethod and @staticmethod
  • the function_decorator is now coupled to your class
Wombatz
  • 4,958
  • 1
  • 26
  • 35