Checking whether a function is decorated
Asked Answered
B

4

7

I am trying to build a control structure in a class method that takes a function as input and has different behaviors if a function is decorated or not. Any ideas on how you would go about building a function is_decorated that behaves as follows:

def dec(fun):
    # do decoration


def func(data):
    # do stuff


@dec
def func2(data):
    # do other stuff


def is_decorated(func):
    # return True if func has decorator, otherwise False


is_decorated(func)  # False
is_decorated(func2) # True
Barbour answered 29/7, 2021 at 23:40 Comment(7)
Decorators are just syntactic sugar for wrapper functions.Necrose
@Necrose Okay, then a potential rephrase of 'how could you go about writing a is_decorated function that detects if a function has a wrapper function?'Barbour
This seems like an XY problem. Why do you need to treat decorated functions differently?Necrose
You could check if func.__closure__ is not None. A wrapped function is a closure.Necrose
@Necrose my decorator causes side effects that I want to manageBarbour
So you just want to detect your decorator, not all decorators in general?Necrose
@Necrose Yes, but there may be more than oneBarbour
X
7

Yes, it's relatively easy because functions can have arbitrary attributes added to them, so the decorator function can add one when it does its thing:

def dec(fun):
    def wrapped(*args, **kwargs):
        pass

    wrapped.i_am_wrapped = True
    return wrapped

def func(data):
    ... # do stuff

@dec
def func2(data):
    ... # do other stuff

def is_decorated(func):
    return getattr(func, 'i_am_wrapped', False)


print(is_decorated(func))   # -> False
print(is_decorated(func2))  # -> True
Xylophone answered 29/7, 2021 at 23:52 Comment(2)
This just also wraps manually the function with an additional property. You have to do this in all of your wrappers. My problem that this feels a tinkering instead of using any in-built potential of the language. If there is any of course. But eventually this is what functools.wraps provide isn't it? It just wraps this logic further...lol.Substandard
Fantastic approach - thank you!Belisle
U
3

There are several ways to identify if a function is wrapped/decorated.

TLDR Solution

def is_decorated(func):
    return hasattr(func, '__wrapped__') or func.__name__ not in globals()

Scenario A

If we assume the decorator uses the helper decorator functools.wraps, then it is pretty straight forward, since our decorated function is going to have the attribute __wrapped__ after.

from functools import wraps


def decorator(function):
    @wraps(function)
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator
def foo(x, y): ...


def is_decorated(function):
    return hasattr(function, '__wrapped__')

Scenario B

In case we don't use wraps during the decorator's definition, then it's a bit different.

def decorator(function):
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator
def foo(x, y): ...


def bar(x, y): ...


print(bar.__name__)  # prints 'bar'
print(foo.__name__)  # prints '_wrapper' instead of 'foo'


def is_decorated(function):
    """
    globals() returns a dictionary which includes
    defined functions, classes and many other stuff.
    Assuming we haven't defined the inner/wrapper name
    as an outer function elsewhere, then it will not be
    in globals()
    """
    return function.__name__ not in globals()

Conclusion

There are other - more sophisticated - ways to check this, however readability is as important as completeness is, and this solution is as simple it can be. As mentioned in the previous code block, the only "hole" of this solution is the following situation:

from functools import wraps

def is_decorated(func):
    return hasattr(func, '__wrapped__') or func.__name__ not in globals()


def decorator_wraps(function):
    @wraps(function)
    def _wrapper(*a, **kw): ...
    return _wrapper


def decorator_no_wraps(function):
    def _wrapper(*a, **kw): ...
    return _wrapper


@decorator_wraps
def foo(x, y): ...


@decorator_no_wraps
def bar(x, y): ...


def baz(x, y): ...


for function in (foo, bar, baz):
    print(is_decorated(function))  # True, True, False


def _wrapper(): ...


for function in (foo, bar, baz):
    print(is_decorated(function))  # True, False, False
Unyielding answered 19/9, 2022 at 6:18 Comment(0)
B
1

What if leveraging class-based decorators? Use __call__ method to make the class a callable one like it was a function.

class dec:
    def __init__(self, func):
        self.wrapped_func = func

    def __call__(self, *args, **kwargs):
        return self.wrapped_func(*args, **kwargs)
   
def func(data):
    ...

@dec
def func2(data):
    ...

def is_decorated(func):
    return isinstance(func, dec)

assert is_decorated(func) is False
assert is_decorated(func2) is True
Bushman answered 13/10, 2023 at 9:12 Comment(0)
O
-1

In general, you can't. There's nothing special about a decorator that leaves a trace. Per the excellent Primer on Python Decorators tutorial, "Decorators provide a simple syntax for calling higher-order functions."

A quick illustrative example from the tutorial:

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice
from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")

You see how wrapper_do_twice inside do_twice is literally just calling the passed function twice? It in no way modifies the function, so it's impossible to tell (without inspection, which, don't do that) that the function was decorated.

However, if you are writing your own decorator, or otherwise have knowledge that you can exploit that the decorator you are using is modifying the decorated function (in contrast to the example above, which does not modify the function), then you can just check for whatever marker(s) are left directly. See this question for an example.

Osber answered 29/7, 2021 at 23:50 Comment(1)
Thank you for your links, they're helpful! :)Barbour

© 2022 - 2025 — McMap. All rights reserved.