2

This has been asked, but all solutions seems to be from around 2011 for early python 2.x, unusable for recent (3.6+) versions.

Having

def function_decorator(fn):
    def wrapper(*args, **kwargs):
        print('decorated')
        return fn(*args, **kwargs)
    return wrapper

and

class C:
    @function_decorator
    def f1(self):
        pass

    # ... many other functions

    def f10(self):
        pass

what should I do so that the class C methods f1, ..., f10 will be decorated with function_decorator? (Obviously without typing it all in manually.)

My actual use case is having a generic parent class with generic methods, while looking to create several children, with each child having slightly different behavior of inherited methods.

E.g. Child1 applies decorator1 to all parent methods, Child2 applies decorator2 etc.

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41
stam
  • 175
  • 9
  • 2
    Python 2 solutions for this should work as well on Python 3 as they ever did on Python 2. If you tried one and failed, either you screwed it up, or some additional complication entered the picture. Either way, we need more information, ideally a [mcve] of a Python 2 solution failing when you tried it. – user2357112 Jan 16 '23 at 13:47
  • Given what you say about "child1 apply decorator1 to all parent methods, child2 apply decorator2", I suspect the problem is that you used something that would only decorate methods *defined* in a class, when you really wanted to decorate *inherited* methods, and then you wrongly blamed the failure on version differences. – user2357112 Jan 16 '23 at 13:50
  • The functionality in the accepted answer of the suggested duplicate is the same in Python 2 and Python 3, as far as I can tell. – 9769953 Jan 16 '23 at 14:02
  • I've tried handful and most has not been runnable ( I don't mean silly things like print 'x' to print('x') ), though I admit some solutions I avoided (e.g. 'inspect' since it 1. seemed too hacky 2. my IDE (pycharms) keeps flagging the code as having some issue 3. I was looking for 'full decorator', e.g. 'all encapsulated' solution, unlike the accepted answer from the link above, where you set decoration in a for loop via standalone code. So I thought I'd ask for working p3 rather than debugging old p2 code. Let me edit the question – stam Jan 16 '23 at 15:38

1 Answers1

0

Metaclasses still seem like the most elegant solution for this.

We can leverage the fact that we can pass arbitrary keyword arguments to the metaclass' __new__ constructor to provide some flexibility in terms of how exactly the decoration should occur.

For instance, we can add an optional func argument during class creation to allow specifying a new decorator for every child class. But wee could also decide to make it an option whether or not double-underscore methods are included, whether inherited methods should be decorated, and which methods specifically to exclude from decoration.

Here is a working example implementation: (tested with Python 3.7 to 3.11)

from __future__ import annotations
from inspect import getmembers
from typing import Any, Callable, Container, TypeVar
from typing_extensions import ParamSpec

P = ParamSpec("P")
T = TypeVar("T")

def _identity(x: T) -> T:
    return x

class DecoratingType(type):
    def __new__(
        mcs,
        name: str,
        bases: tuple[type],
        namespace: dict[str, Any],
        func: Callable[[Callable[P, T]], Callable[P, T]] = _identity,
        incl_dunder: bool = False,
        incl_parent: bool = True,
        exclude: Container[str] = (),
        **kwargs: Any,
    ) -> DecoratingType:
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        items = getmembers(cls) if incl_parent else namespace.items()
        for name, obj in items:
            if not callable(obj):
                continue
            if not incl_dunder and name.startswith("__"):
                continue
            if name not in exclude:
                namespace[name] = func(obj)
        return cls

Here is a little demo:

...

def dec1(f: Callable[P, T]) -> Callable[P, T]:
    print(f"applied dec1 to {f.__name__}")
    return f


def dec2(f: Callable[P, T]) -> Callable[P, T]:
    print(f"applied dec2 to {f.__name__}")
    return f


class Base(metaclass=DecoratingType):
    spam = "abc"

    def foo(self) -> None: ...

    def bar(self) -> None: ...


class Child1(Base, func=dec1, incl_parent=False, incl_dunder=True):
    def __str__(self) -> str:
        return "1"


class Child2(Base, func=dec2, exclude={"foo"}):
    def __str__(self) -> str:
        return "2"

    def baz(self) -> None: ...

Loading this gives the following output:

applied dec1 to __str__
applied dec2 to bar
applied dec2 to baz

In this case during creation of the Child1 class, we chose to not decorate methods inherited from Base, but we do decorate our own double-underscore method __str__ with the specified dec1.

For Child2 the specified dec2 decorator was applied to all methods, including those it inherited from Base, but excluding the double-underscore method __str__ as well as specifically foo (inherited).

As you can see, you can basically implement whatever functionality you want to this effect using the type.__new__ approach. This is just a showcase. You just have to keep the first four parameters in place, but you can add as many other ones as you want.

And since the metaclass is inherited, you only need to specify it once in your Base class. All other keyword-arguments are optional, including the func argument, meaning you can define a child class that essentially skips decoration altogether by just doing Child(Base): ....

Daniil Fajnberg
  • 12,753
  • 2
  • 10
  • 41