7

I was wondering if there was a way for a decorated function to refer to an object created by the wrapper of a decorator. My question arose when I was thinking to use a decorator to :

  1. make a wrapper that creates a figure with subplots
  2. inside the wrapper execute the decorated function which would add some plots
  3. finally save the figure in the wrapper

However, the decorated function would need to refer the figure created by the wrapper. How can the decorated function refer to that object ? Do we necessarily have to resort to global variables ?

Here is a short example where I reference in the decorated function a variable created in the wrapper (but I did not manage to do this without tweaking with globals):

def my_decorator(func):
    def my_decorator_wrapper(*args, **kwargs):
        global x
        x = 0
        print("x in wrapper:", x)
        return func(*args, **kwargs)
    return my_decorator_wrapper

@my_decorator
def decorated_func():
    global x
    x += 1
    print("x in decorated_func:", x)

decorated_func()
# prints:
# x in wrapper: 0
# x in decorated_func: 1

I know this would be easily done in a class, but I am asking this question out of curiosity.

Tanguy
  • 3,124
  • 4
  • 21
  • 29
  • 3
    The decorator could supply an additional parameter to the wrapped function when it is called. Normally that would be a bad idea, as decoration is something you can *optionally* do to a function, but you're talking about a scenario with vastly tighter coupling between decorated & decorator than normal. – jasonharper Jan 22 '19 at 20:59
  • In Python 3, you could use `nonlocal` to refer to objects that are known to be created by the decorator. – chepner Jan 22 '19 at 20:59
  • @chepner: No, `nonlocal` doesn't work like that. `nonlocal` is for closure variables. – user2357112 Jan 22 '19 at 21:02
  • 2
    @user2357112 OK, right. I was getting closures and dynamic scoping confused. – chepner Jan 22 '19 at 21:10
  • 1
    Can you add a code snippet to make your request clearer? – DocDriven Jan 22 '19 at 21:14
  • 4
    There are various way this could potentially be achieved, but none seems particularly good, and really, if your decorated function needs to understand something about the internals of the decorator, it's probably not a good use-case for a decorator to begin with – juanpa.arrivillaga Jan 22 '19 at 21:43
  • agreed with @juanpa.arrivillaga, decorated functions should not be aware of their decorator. It defeats the whole purpose – yorodm Jan 22 '19 at 22:22

3 Answers3

1

Yes, the function can refer to it by looking at itself.

the decorator end. it just takes attributes and sets them on the function

if it looks a bit complicated, that's because decorators that take parameters need this particular structure to work. see Decorators with parameters?

def declare_view(**kwds):
    """declaratively assocatiate a Django View function with resources
    """

    def actual_decorator(func):
        for k, v in kwds.items():
            setattr(func, k, v)

        return func

    return actual_decorator

calling the decorator

@declare_view(
    x=2
)
def decorated_func():
    #the function can look at its own name, because the function exists 
    #by the time it gets called.
    print("x in decorated_func:", decorated_func.x)

decorated_func()

output

x in decorated_func: 2 

In practice I've used this quite a bit. The idea for me is to associate Django view functions with particular backend data classes and templates they have to collaborate with. Because it is declarative, I can introspect through all the Django views and track their associated URLs as well as custom data objects and templates. Works very well, but yes, the function does expect certain attributes to be existing on itself. It doesn't know that a decorator set them.

Oh, and there's no good reason, in my case, for these to be passed as parameters in my use cases, these variables hold basically hardcoded values which never change, from the POV of the function.

Odd at first, but quite powerful and no runtime or maintenance drawbacks.

Here's some live example that puts that in context.

@declare_view(
    viewmanager_cls=backend.VueManagerDetailPSCLASSDEFN,
    template_name="pssecurity/detail.html",
    objecttype=constants.OBJECTTYPE_PERMISSION_LIST[0],
    bundle_name="pssecurity/detail.psclassdefn",
)
def psclassdefn_detail(request, CLASSID, dbr=None, PORTAL_NAME="EMPLOYEE"):
    """

    """
    f_view = psclassdefn_detail
    viewmanager = f_view.viewmanager_cls(request, mdb, f_view=f_view)
    ...do things based on the parameters...
    return viewmanager.HttpResponse(f_view.template_name)
JL Peyret
  • 10,917
  • 2
  • 54
  • 73
  • 2
    you can set attributes on functions!? – John de Largentaye Jan 22 '19 at 23:00
  • 2
    In Python everything is an object. So, yes, since views are Python objects and don't have `__slots__`, you can set arbitrary attributes on them. Between Python 2 and 3 their own internal attributes have moved a bit. For example the name might be `func.__name__` in 3, but something slightly more complicated in 2. – JL Peyret Jan 22 '19 at 23:05
  • edit: read ‘functions’ instead of ‘views’ in my preceding comment. – JL Peyret Jan 22 '19 at 23:39
1

Try to avoid using global variables.

Use arguments to pass objects to functions

There is one canonical way to pass a value to a function: arguments.

Pass the object as argument to the decorated function when the wrapper is called.

from functools import wraps

def decorator(f):
    obj = 1

    @wraps(f)
    def wrapper(*args):
        return f(obj, *args)

    return wrapper

@decorator
def func(x)
    print(x)

func() # prints 1

Use a default argument for passing the same object

If you need to pass the same object to all functions, storing it as default argument of your decorator is an alternative.

from functools import wraps

def decorator(f, obj={}):
    @wraps(f)
    def wrapper(*args):
        return f(obj, *args)

    return wrapper

@decorator
def func(params)
    params['foo'] = True

@decorator
def gunc(params)
    print(params)

func()

# proof that gunc receives the same object
gunc() # prints {'foo': True}

The above creates a common private dict which can only be accessed by decorated functions. Since a dict is mutable, changes will be reflected across function calls.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
0

Classes as Decorators

This article points to classes as decorators, which seems a more elegant way to point to the state defined in the decorator. It relies on function attributes and uses the special .__call__() method in the decorating class.

Here is my example revisited using a class instead of a function as a decorator:

class my_class_decorator:
    def __init__(self, func):
        self.func = func
        self.x =  0

    def __call__(self, *args, **kwargs):
        print("x in wrapper:", self.x)
        return self.func(*args, **kwargs)

@my_class_decorator
def decorated_func():
    decorated_func.x += 1
    print("x in decorated_func:", decorated_func.x)

decorated_func()
# prints:
# x in wrapper: 0
# x in decorated_func: 1
Tanguy
  • 3,124
  • 4
  • 21
  • 29