Object decorators are objects that extend and customize functions in Python, and since they are objects themselves, they can store information that can be modified independently of the decorated function. This article explains the basics of Python decorators and how custom objects can be used as decorators.
Decorators are functions (or objects) that wrap other functions, which simply means that they execute commands before and after the function that is wrapped. Decorators are useful because they extend the functionality of existing functions by factoring out pieces of code that do not pertain to the purpose of the function.
In Python, decorator functions have the following syntax:
The decorator (or wrapper) function, in this case, is named
my_decorator, and the decorated (or wrapped) function is named
sum function’s purpose is simply to add two
numbers, and the
my_decorator simply prints a statement before
and after execution, presumably for debugging purposes. Keeping the
decorator function separate removes the clutter of print statements in
the sum function, and it allows
my_decorator to be used for
Executing the code above gives the following:
This is executed before sum This is executed after sum 5
As you might have noticed, the behavior of the above decorator cannot be directly customized–nor would it need to be for this simple decorator. In the following sections, I will demonstrate two different methods of customizing decorator behavior using a memoizer as an example.
Object Decorators: The Memoizer
A memoizer is a function (decorator) that stores the results of the wrapped function. It is a form of function optimization that caches computationally-expensive results into memory, returning these on future invocations rather than re-evaluating the answer every time. Non-trivial uses might include database lookup functions or expensive mathematical calculations with values that depend only on the specified arguments (holonomic, state functions).
Object decorators are particularly useful as memoizers because they can manage their own resources, and they can incorporate special methods for cleaning or deleting the cached data. A maintenance function could be used, for example, to remove values stored by the memoizer for which the result is known to have changed, as would be the case for a function that returns the number of records in a ever-changing database.
Let me illustrate with an example.
The above Memoizer stores the memoized function (
and a dict containing the cached results
__call__ method implements the
behavior of this object when it is called like a function, including
arguments and keyword arguments. When a Memoizer object wraps a
__call__ method is executed, and it is tasked with
executing the wrapped function then storing the result. A clear method
has been included which can either be used to remove all cached
values, when invoked without parameters, or to remove specific cached
values, when invoked with parameters.
A technical note on arguments: efficient storage of memoized results requires that a given set of arguments corresponds to only one result. The combination of parameters used in invoking a function is known as the signature of a function. Due to the Python’s flexibility with arguments, a function can be executed with the same parameters but with different signatures.
These three statements give the same arguments to the
function and return the same result, but they all have different (but
valid!) signatures. This is a problem when using
**kwargs to abstract the function arguments: given the
flexibility of writing arguments with keywords (sum(a=3, b=1)),
without keywords (sum(3,1)), or a mixture of the two cases
(sum(3, b=1)), the given parameters will jump between the args
tuple (when invoked without a keyword) and the kwargs dict (when
invoked with a keyword). This makes it difficult to store one result
for all three of these cases. The trade-off I’ve chosen is to assert
that arguments without keywords are not used, and to transform the
keyword arguments into a unique identifier (
that can be used as a key to the
self.cached_values dict. This
approach arguably makes code clearer by requiring keywords, and it
ensures that only one result is stored for a given set of parameters.
The Memoizer decorates functions like so:
The result of the of sum function is stored in the Memoizer’s
self.cached_values, and the stored values are deleted using the
Decorator arguments customize the behavior of decorators by allowing initialization parameters to change the behavior of the decorator when the decorator is created. In this example, the Memoizer is extended to measure the execution time of the wrapped function.
A key difference in this implementation from the former is the use of
an outer decorator function. The Memoizer is now a function that
MemoizerObject instance to be used in wrapping the
target function. The outer Memoizer function customizes the creation
of the ```MemoizerObject`` with the decorator arguments, and it
returns a decorator that wraps the function with this object.
The extra level of abstraction with the outer decorator is needed
because the wrapped function is not given as a parameter to the
MemoizerObject constructor function (
__init__), and therefore it
cannot be used directly to wrap the function.
This new decorator object collects statistics on the execution time and the number of calls to the wrapped function, and these are reported when the verbose option is set to True.
The generated output gives statistics when running the sum function.
In the next article on Object Decorators, descriptors are used as Method Object Decorators, opening new possibilities for membership testing and class-level customization of object decorators.