Writing a simple function decorator

A decorator is a function (or a callable object) that accepts a function as an argument and returns a new function. The result of decoration is a function that has been wrapped. Generally, the additional features of the wrapping surround the original functionality, either by transforming actual argument values or by transforming the result value. These are the two readily available join points in a function.

When we use a decorator, we want to be sure that the resulting decorated function has the original function's name and docstring. These details can be handled for us by a decorator to build our decorators. Using functools.wraps to write new decorators simplifies the work we need to do because the bookkeeping is handled for us.

Additionally, the type hints for decorators can be confusing because the parameter and return are both essentially of the Callable type. To be properly generic, we'll use an upper-bound type definition to define a type, F, which embraces any variation on callable objects or functions.

To illustrate the two places where functionality can be inserted, we can create a debug trace decorator that will log parameters and return values from a function. This puts functionality both before and after the called function. The following is a defined function, some_function, that we want to wrap. In effect, we want code that behaves like the following:

logging.debug("function(%r, %r)", args, kw)
result = some_function(*args, **kw)
logging.debug("result = %r", result) 

This snippet shows how we'll have new log-writing to wrap the original, some_function(), function.

The following is a debug decorator that inserts logging before and after function evaluation:

import logging, sys
import functools
from typing import Callable, TypeVar

FuncType = Callable[..., Any]
F = TypeVar('F', bound=FuncType)

def debug(function: F) -> F:

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

return cast(F, logged_function)

We've used the @functools.wraps decorator to ensure that the original function name and docstring are preserved as attributes of the result function. The logged_function() definition is the resulting function returned by the debug() decorator. The internal, logged_function() does some logging, then invokes the decorated function, function, and does some more logging before returning the result of the decorated function. In this example, no transformation of argument values or results was performed.

When working with the logger, f-strings are not the best idea. It can help to provide individual values so the logging filters can be used to redact or exclude entries from a sensitive log. 

Given this @debug decorator, we can use it to produce noisy, detailed debugging. For example, we can do this to apply the decorator to a function, ackermann(), as follows:

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

This definition wraps the ackermann() function with debugging information written via the logging module to the root logger. We've made no material changes to the function definition. The @debug decorator injects the logging details as a separate aspect. 

We configure the logger as follows:

logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) 

We'll revisit logging in detail in Chapter 16, The Logging and Warning Modules. We'll see this kind of result when we evaluate ackermann(2,4) as follows:

DEBUG:root:ackermann((2, 4), {}) 
DEBUG:root:ackermann((2, 3), {}) 
DEBUG:root:ackermann((2, 2), {}) 
. 
. 
. 
DEBUG:root:ackermann((0, 10), {}) 
DEBUG:root:ackermann = 11 
DEBUG:root:ackermann = 11 
DEBUG:root:ackermann = 11 

In the next section, we will see how to create separate loggers.

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

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