简体   繁体   中英

Read function arguments in decorator

I have written this decorator that saves intermediate results in a json file

import json
import os

def json_file(fname):
    def decorator(function):
        def wrapper(*args, **kwargs):
            if os.path.isfile(fname):
                with open(fname, 'r') as f:
                    ret = json.load(f)
            else:
                with open(fname,'w') as f:
                    ret = function(*args, **kwargs)
                    json.dump(ret, f)
            return ret
        return wrapper
    return decorator

And usage is

@json_file("cached.json")
def some_calculation(n):
   return {"result": 2**n}

And I want to augment in to use the function parameters like this:

@json_file("cached_{n}.json")
def calculation(n):
    return {"result": 2**n}

Such that {n} is replaced with the value of n when calling the function.

I tried replacing fname with fname.format(**kwargs) , without any success.

How can this be achieved ?

EDIT:

As Per @jonrshape's comment, this is the error I am getting after adding the .format(**kwargs)

 <ipython-input-4-12b0c894b345> in wrapper(*args, **kwargs)
       5     def decorator(function):
       6         def wrapper(*args, **kwargs):
 ----> 7             fname = fname.format(**kwargs)
       8             if os.path.isfile(fname):
       9                 with open(fname, 'r') as f:

 UnboundLocalError: local variable 'fname' referenced before assignment

If n will sometimes be a positional and sometimes be a keyword argument, you can first search the keyword arguments kwargs , and if that fails, "fallback" to getting the value from args :

def json_file(fname):
    def decorator(function):
        def wrapper(*args, **kwargs):
            try:
                value = kwargs['n']
            except KeyError:
                value = args[0]
            fname.format(value)
            if os.path.isfile(fname):
                with open(fname, 'r') as f:
                    ret = json.load(f)
            else:
                with open(fname,'w') as f:
                    ret = function(*args, **kwargs)
                    json.dump(ret, f)
            return ret
        return wrapper
    return decorator

Just for sake of completeness, I'll address your error.

The reason your getting an UnboundLocalError error is because of how Python sees your variable definition.

If a variable is already defined in the current scope, assiging the variable to a new value will simply rebind it to the new value. However, if the variable has not yet been defined, Python treats it as a variable definition , not a rebinding.

That is why your code fails. Python expects fname to have been defined in the current scope, not a parent scope. But since fname was never defined, it raised an error.

You can fix this error by using the nonlocal statement. From the docs:

The nonlocal statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals. This is important because the default behavior for binding is to search the local namespace first. The statement allows encapsulated code to rebind variables outside of the local scope besides the global (module) scope.

Here is an example of the usage:

>>> def foo(arg):
    def bar():
        arg += 1
        return arg
    return bar

>>> 
>>> foo(0)() # this will raise an error
Traceback (most recent call last):
  File "<pyshell#76>", line 1, in <module>
    foo(0)() # this will raise an error
  File "<pyshell#74>", line 3, in bar
    arg += 1
UnboundLocalError: local variable 'arg' referenced before assignment
>>> 
>>> def foo(arg):
    def bar():
        nonlocal arg
        arg += 1
        return arg
    return bar

>>> foo(0)() # this will work
1
>>> 

You can use the inspect module.

First we extract the wrapped function's signature:

signature= inspect.signature(function)

Then we bind the *args and **kwargs:

bound_args= signature.bind(*args, **kwargs)

Now bound_args.parameters is a dict of parameter_name:parameter_value which we can use to format our file name:

file_name= fname.format(**bound_args.arguments)

Everything put together:

import json
import os
import inspect

def json_file(fname):
    def decorator(function):
        signature= inspect.signature(function)

        def wrapper(*args, **kwargs):
            bound_args= signature.bind(*args, **kwargs)
            file_name= fname.format(**bound_args.arguments)

            #~ print(file_name)
            if os.path.isfile(file_name):
                with open(file_name, 'r') as f:
                    ret = json.load(f)
            else:
                with open(file_name,'w') as f:
                    ret = function(*args, **kwargs)
                    json.dump(ret, f)
            return ret
        return wrapper
    return decorator

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM