Search code examples
pythonpython-decoratorskeyword-argument

How can I decorate a Python function without changing the names of the arguments?


The following Python code defines a logger and a factorial function, then calls the factorial function with a keyword argument:

def logger(f):
  def f_(a):
    print("Call", a)
    return f(a)
  return f_

# @logger # uncomment this line to see the problem
def factorial(n):
  return 1 if n == 0 else n * factorial(n-1)

print(factorial(n=5))

resulting in the following output (as expected): 120.

Now if I uncomment the logger decorator, I get an error, because the name of the argument has become a instead of n:

How can I decorate a function (like factorial) without changing the names of the arguments?


Solution

  • Ignoring the decorator syntax, here's what you're doing:

    def logger(f):
        def f_(a):
            print("Call", a)
            return f(a)
        return f_
    
    def factorial(n):
        return 1 if n == 0 else n * factorial(n-1)
    
    # `factorial` is `def f_(a):` now
    factorial = logger(factorial)
    

    So to simply remedy this, use the same declaration, or don't use keyword arguments (n=5)


    The better way to correct this is to use unpacking in your inner function:

    import functools
    
    def logger(f):
        @functools.wraps(f)
        def f_(*args, **kwargs):
            print("Call", f.__name__, *args, *[f"{k}={v!r}" for k, v in kwargs.items()])
            return f(*args, **kwargs)
        return f_
    

    Here's a useful article on the topic.