4

I am looking at a function in a flask backend with a decorator on it and thinking of importing it into another script and decorating it in a different way. Does anyone know what happens when you import it, whether the decorator goes with it or not?

I had a look at this but it's discussing more what happens in the same script.

cardamom
  • 6,873
  • 11
  • 48
  • 102
  • 3
    The decorator does not, but the *result* of the decorator does. Decoration is just a syntactic shortcut for `def funcname(...): ...; funcname = decorator(funcname)`. You don't have access to the original, undecorated function, unless the decorator explicitly exposes it via the new function. – chepner Mar 12 '19 at 14:47
  • https://stackoverflow.com/questions/25829364/applying-a-decorator-to-an-imported-function – NotSoShabby Mar 12 '19 at 14:48
  • @chepner what do you mean by _unless the decorator explicitly exposes it via the new function_ ? – cardamom Mar 12 '19 at 14:50
  • @cardamom: functions can have attributes, the decorator can return a new function object with additional attributes, one of which could be a reference to the original. – Martijn Pieters Mar 12 '19 at 14:51
  • 2
    @cardamom: the [`@functools.wraps` decorator utility](https://docs.python.org/3/library/functools.html#functools.wraps) does exactly that, it sets `wrapper.__wrapped__` to the original function. – Martijn Pieters Mar 12 '19 at 14:52

2 Answers2

6

No, importing a decorated function will not remove the decorator.

Importing retrieves the current object from the global namespace of the source module, and decorating a function causes the decorator return value to be stored in the global namespace.

Importing a module is mostly syntactic sugar for modulename = sys.modules['modulename'] (for import modulename) and objectname = sys.modules['modulename'].objectname assignments (for from modulename import objectname, in either case after first ensuring that sys.modules has the desired module loaded), and globals in a module are the same thing as attributes on a module object. Decorating is just syntactic sugar for functionname = decorator(functionobject).

If you need to add a new decorator to the imported function, just call the decorator:

from somemodule import somedecoratedfunction

newname_or_originalname = decorator(somedecoratedfunction)

If the imported decorated function doesn't lend itself to being decorated again in a new layer, or you want access to the original undecorated function, see if the object has a __wrapped__ attribute:

from somemodule import somedecoratedfunction

unwrapped_function = somedecoratedfunction.__wrapped__

A well-written decorators uses the @functools.wraps() decorator, which sets that attribute to point to the original:

>>> from functools import wraps
>>> def demodecorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwargs):
...         print("Decorated!")
...         return f(*args, **kwargs)
...     return wrapper
...
>>> @demodecorator
... def foo(name):
...     print(f"Hello, {name or 'World'}!")
...
>>> foo('cardamom')
Decorated!
Hello, cardamom!
>>> foo.__wrapped__('cardamom')
Hello, cardamom!
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
5

Decorating a function

@some_decorator
def some_func(...):
    ...

is equivalent to applying a function to another object:

def some_func(...):
    ...

some_func = some_decorator(some_func)

When you import the module, all you have access to is the object currently bound to some_func, which is the return value of some_decorator applied to the original function. Unless the thing returned some_decorator includes a reference to the original, undecorated function, you have no access to it from the imported module.

An example of exposing the original:

def some_decorator(f):
    def _(*args, *kwargs):
        # Do some extra stuff, then call the original function
        # ...
        return f(*args, **kwargs)
    _.original = f
    return _

@some_decorator
def some_func(...):
    ...

When you import the module, some_module.some_func refers to the decorated function, but the original undecorated function is available via some_module.some_func.original, but only because the decorator was written to make it available. (As Martijn Peters points out, the wraps decorator does this--and some other nice things--for you, but the decorator still needs to use wraps.)

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Thanks both good answers, marking this one as it is simpler, does not involve an extra import and then, would not know what order to put them in if @wraps had to go above or below another decorator. There is also the point that `.__wrapped__` is counterintuitive when what it actually does is UNwrap. – cardamom Mar 12 '19 at 15:42
  • 1
    It doesn't unwrap anything; it refers to the thing that *gets* wrapped before it was, in fact, wrapped. – chepner Mar 12 '19 at 15:45
  • 2
    @cardamom just know that the [standard library provides additional functionality](https://docs.python.org/3/library/inspect.html#inspect.unwrap) when you use that attribute name, making your decorator much more useful in other contexts such as an IDE providing autocompletion. Plus, `@functools.wraps()` takes care of a lot more for you. – Martijn Pieters Mar 12 '19 at 17:05