A Look into Python Decorators
A Look into Python Decorators
Join the DZone community and get the full member experience.Join For Free
Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.
Recently I had occasion to re-read that post. It wasn’t a pleasant experience — it was pretty clear to me that the attempt had failed.
That failure — and two other things — have prompted me to try again.
- Matt Harrison has published an excellent e-book Guide to: Learning Python Decorators.
- I now have a theory about why most explanations of decorators (mine included) fail, and some ideas about how better to structure an introduction to decorators.
There is an old saying to the effect that “Every stick has two ends, one by which it may be picked up, and one by which it may not.” I believe that most explanations of decorators fail because they pick up the stick by the wrong end.
In this post I will show you what the wrong end of the stick looks like, and point out why I think it is wrong. And I will show you what I think the right end of the stick looks like.
The wrong way to explain decorators
Most explanations of Python decorators start with an example of a function to be decorated, like this:
def aFunction(): print("inside aFunction")
and then add a decorator line, which starts with an @ sign:
@myDecorator def aFunction(): print("inside aFunction")
At this point, the author of the introduction often defines a decorator as the line of code that begins with the “@”. (In my older post, I called such lines “annotation” lines.)
For instance, in 2008 Bruce Eckel wrote on his Artima blog
A function decorator is applied to a function definition by placing it on the line before that function definition begins.
and in 2004, Phillip Eby wrote in an article in Dr. Dobb’s Journal
Decorators may appear before any function definition…. You can even stack multiple decorators on the same function definition, one per line.
Now there are two things wrong with this approach to explaining decorators. The first is that the explanation begins in the wrong place. It starts with an example of a function to be decorated and an annotation line, when it should begin with the decorator itself. The explanation should end, not start, with the decorated function and the annotation line. The annotation line is, after all, merely syntactic sugar — is not at all an essential element in the concept of a decorator.
The second is that the term “decorator” is used incorrectly (or ambiguously) to refer both to the decorator and to the annotation line. For example, in his Dr. Dobb’s Journal article, after using the term “decorator” to refer to the annotation line, Phillip Eby goes on to define a “decorator” as a callable object.
But before you can do that, you first need to have some decorators to stack. A decorator is a callable object (like a function) that accepts one argument—the function being decorated.
So… it would seem that a decorator is both a callable object (like a function) and a single line of code that can appear before the line of code that begins a function definition. This is sort of like saying that an “address” is both a building (or apartment) at a specific location and a set of lines (written in pencil or ink) on the front of a mailing envelope. The ambiguity may be almost invisible to someone familiar with decorators, but it is very confusing for a reader who is trying to learn about decorators from the ground up.
The right way to explain decorators
So how should we explain decorators?
Well, we start with the decorator, not the function to be decorated.
We start with the basic notion of a function — a function is something that generates a value based on the values of its arguments.
We note that in Python, functions are first-class objects, so they can be passed around like other values (strings, integers, objects, etc.).
We note that because functions are first-class objects in Python, we can write functions that both (a) accept function objects as argument values, and (b) return function objects as return values. For example, here is a function foobar that accepts a function object original_function as an argument and returns a function object new_function as a result.
def foobar(original_function): # make a new function def new_function(): # some code return new_function
We define “decorator”.
A decorator is a function (such as foobar in the above example) that takes a function object as an argument, and returns a function object as a return value.
So there we have it — the definition of a decorator. Anything else that we say about decorators is a refinement of, or an expansion of, or an addition to, this definition of a decorator.
We show what the internals of a decorator look like. Specifically, we show different ways that a decorator can use the original_function in the creation of the new_function. Here is a simple example.
def verbose(original_function): # make a new function that prints a message when original_function starts and finishes def new_function(*args, **kwargs): print("Entering", original_function.__name__) original_function(*args, **kwargs) print("Exiting ", original_function.__name__) return new_function
We show how to invoke a decorator — how we can pass into a decorator one function object (its input) and get back from it a different function object (its output). In the following example, we pass the widget_func function object to the verbose decorator, and we get back a new function object to which we assign the name talkative_widget_func.
def widget_func(): # some code talkative_widget_func = verbose(widget_func)
We point out that decorators are often used to add features to the original_function. Or more precisely, decorators are often used to create a new_function that does roughly what original_function does, but also does things in addition to what original_function does.
And we note that the output of a decorator is typically used to replace the original function that we passed in to the decorator as an argument. A typical use of decorators looks like this. (Note the change to line 4 from the previous example.)
def widget_func(): # some code widget_func = verbose(widget_func)
So for all practical purposes, in a typical use of a decorator we pass a function (widget_func) through a decorator (verbose) and get back an enhanced (or souped-up, or “decorated”) version of the function.
We introduce Python’s “decorator syntax” that uses the “@” to create annotation lines. This feature is basically syntactic sugar that makes it possible to re-write our last example this way:
@verbose def widget_func(): # some code
The result of this example is exactly the same as the previous example — after it executes, we have a widget_func that has all of the functionality of the original widget_func, plus the functionality that was added by the verbose decorator.
Note that in this way of explaining decorators, the “@” and annotation syntax is one of the last things that we introduce, not one of the first.
And we absolutely do not refer to line 1 as a “decorator”. We might refer to line 1 as, say, a “decorator line” or as a “decorator invocation line” or as an “annotation line” … whatever. But line 1 is not a “decorator”. Line 1 is a line of code. A decorator is a function — a different animal altogether.
Once we’ve nailed down these basics, there are a few advanced features to be covered.
- We explain that a decorator need not be a function (it can be any sort of callable, e.g. a class).
- We explain how decorators can be nested within other decorators.
- We explain how
decoratorsdecorator lines can be “stacked”. A better way to put it would be: we explain how decorators can be “chained”.
- We explain how additional arguments can be passed to decorators, and how decorators can use them.
Ten — A decorators cookbook
The material that we’ve covered up to this point is what any basic introduction to Python decorators would cover. But a Python programmer needs something more in order to be productive with decorators. He (or she) needs a catalog of recipes, patterns, examples, and commentary that describes / shows / explains when and how decorators can be used to accomplish specific tasks. (Ideally, such a catalog would also include examples and warnings about decorator gotchas and anti-patterns.) Such a catalog might be called “Python Decorator Cookbook” or perhaps “Python Decorator Patterns”.
As far as I know, no such decorator cookbook currently exists.
The Python Decorator Library on the Python wiki is a collection of decorator examples. It has its uses, but it does not have the systematic organization and explanatory material of a true cookbook.
Something similar to a descriptor cookbook, although still not systematically organized, can be generated by a search of the ActiveState Python Cookbook, filtering on “descriptor”.
So that’s it. I’ve described what I think is wrong (well, let’s say suboptimal) about most introductions to decorators. And I’ve sketched out what I think is a better way to structure an introduction to decorators.
Now I can explain why I like Matt Harrison’s e-book Guide to: Learning Python Decorators. Matt’s introduction is structured in the way that I think an introduction to decorators should be structured. It picks up the stick by the proper end.
The first two-thirds of the Guide hardly talk about decorators at all. Instead, Matt begins with a thorough discussion of how Python functions work. By the time the discussion gets to decorators, we have been given a strong understanding of the internal mechanics of functions. And since most decorators are functions (remember our definition of decorator), at that point it is relatively easy for Matt to explain the internal mechanics of decorators.
Which is just as it should be.
Published at DZone with permission of Steve Ferg . See the original article here.
Opinions expressed by DZone contributors are their own.