Parameterizing a decorator

Sometimes, we need to provide parameters to a decorator. The idea is that we are going to customize the wrapping function. When we do this, decoration becomes a two-step process.

Here's a snippet showing how we provide a parameterized decorator to a function definition:

@decorator(arg) 
def func( ): 
    pass 

The implementation is as follows:

def func( ): 
    pass 
func = decorator(arg)(func) 

We've done the following three things:

  • Defined a function, func
  • Applied the abstract decorator to its arguments to create a concrete decorator, decorator(arg)
  • Applied the concrete decorator to the defined function to create the decorated version of the function, decorator(arg)(func)

It can help to think of func = decorate(arg)(func) as having the following implementation:

concrete = decorate(arg)
func = concrete(func)

This means that a decorator with arguments is implemented as indirect construction of the final function. Now, let's tweak our debugging decorator yet again. We'd like to do the following:

@debug("log_name") 
def some_function( args ): 
    pass 

This kind of code allows us to specify the name of the log that the debugging output will go to. This means we won't use the root logger or create a distinct logger for each function.

The outline of a parameterized decorator will be the following:

def decorator(config) -> Callable[[F], F]:
def concrete_decorator(function: F) -> F:
def wrapped(*args, **kw):
return function(*args, **kw)
return cast(F, wrapped)
return concrete_decorator

Let's peel back the layers of this onion before looking at the example. The decorator definition (def decorator(config)) shows the parameters we will provide to the decorator when we use it. The body of this is the concrete decorator, which is returned after the parameters are bound to it. The concrete decorator (def concrete_decorator(function):) will then be applied to the target function. The concrete decorator is like the simple function decorator shown in the previous section. It builds the wrapped function (def wrapped(*args, **kw):), which it returns.

The following is our named logger version of debug:

def debug_named(log_name: str) -> Callable[[F], F]:
log = logging.getLogger(log_name)

def concrete_decorator(function: F) -> F:

@functools.wraps(function)
def wrapped(*args, **kw):
log.debug("%s(%r, %r)", function.__name__, args, kw)
result = function(*args, **kw)
log.debug("%s = %r", function.__name__, result)
return result

return cast(F, wrapped)

return concrete_decorator

This @debug_named decorator accepts an argument that is the name of the log to use. It creates and returns a concrete decorator function with a logger of the given name bound into it. When this concrete decorator is applied to a function, the concrete decorator returns the wrapped version of the given function. When the function is used in the following manner, the decorator adds noisy debug lines.

Here's an example of creating a logged named recursion with output from a given function:

@debug_named("recursion")
def ackermann3(m: int, n: int) -> int:
if m == 0:
return n + 1
elif m > 0 and n == 0:
return ackermann3(m - 1, 1)
elif m > 0 and n > 0:
return ackermann3(m - 1, ackermann3(m, n - 1))
else:
raise Exception(f"Design Error: {vars()}")

The decorator wraps the given ackermann3() function with logging output. Since the decorator accepts a parameter, we can provide a logger name. We can reuse the decorator to put any number of individual functions into a single logger, providing more control over the debug output from an application.

Now, let's see how to create a method function decorator.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset