Python Decorator

Understanding Python Decorators in 12 Easy Steps!

1 July 2012

Ok, perhaps I jest. As a Python instructor, understanding decorators is a topic I find studentsconsistently struggle with upon first exposure. That’s because decorators are hard tounderstand! Getting decorators requires understanding several functionalprogramming concepts as well as feeling comfortable with some unique features of Python’sfunction definition and function calling syntax. *Using* decorators is easy(see Section 10)! But writing them can be complicated.

I can’t make decorators easy - but maybe by walking through each piece of the puzzle one step at atime I can help you feel more confident in understanding decorators[1]. Because decorators arecomplex this is going to be a long article - but stick with it! I promise to make each piece assimple as possible - and if you understand each piece, you’ll understand how decorators work! I’mtrying to assume minimal Python knowledge but this will probably be most helpful to people who haveat least a casual working exposure to Python.

I should also note that I used Python’s doctest modules to run the Python code samples in thisarticle. The code looks like an interactive Python console session (>>> and indicate Pythonstatements while output has its own line). There are occasionally cryptic comments that begin with"doctest" - these are just directives to doctest and can safely be ignored.

1. Functions

Functions in Python are created with the def keyword and take a name and an optional list ofparameters. They can return values with the return keyword. Let’s make and call the simplestpossible function:

>>> def foo():
...     return 1
>>> foo()

The body of the function (as with all multi-line statements in Python) is mandatory and indicated byindentation. We can call functions by appending parentheses to the function name.

2. Scope

In Python functions create a new scope. Pythonistas might also say that functions have their ownnamespace. This means Python looks first in the namespace of the function to find variable nameswhen it encounters them in the function body. Python includes a couple of functions that let us lookat our namespaces. Let’s write a simple function to investigate the difference between localand global scope.

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals() # doctest: +ELLIPSIS
{..., 'a_string': 'This is a global variable'}
>>> foo() # 2

The builtin globals function returns a dictionary containing all the variable names Python knowsabout. (For the sake of clarity I’ve omitted in the output a few variables Python automaticallycreates.) At point #2 I called my function foo which prints the contents of the local namespaceinside the function. As we can see the function foo has its own separate namespace which iscurrently empty.

3. variable resolution rules

Of course this doesn’t mean that we can’t access global variables inside our function. Python’sscope rule is that variable creation always creates a new local variable but variable access(including modification) looks in the local scope and then searches all the enclosing scopes to finda match. So if we modify our function foo to print our global variable things work as we wouldexpect:

>>> a_string = "This is a global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

At point #1 Python looks for a local variable in our function and not finding one, looks for aglobal variable[2] of the same name.

On the other hand if we try to assign to the global variable inside our function it doesn’t do whatwe want:

>>> a_string = "This is a global variable"
>>> def foo():
...     a_string = "test" # 1
...     print locals()
>>> foo()
{'a_string': 'test'}
>>> a_string # 2
'This is a global variable'

As we can see, global variables can be accessed (even changed if they are mutable data types) butnot (by default) assigned to. At point #1 inside our function we are actually creating a new localvariable that "shadows" the global variable with the same name. We can see this be by printing thelocal namespace inside our function foo and notice it now has an entry. We can also see back outin the global namespace at point #2 that when we check the value of the variable a_string ithasn’t been changed at all.

4. Variable lifetime

It’s also important to note that not only do variables live inside a namespace, they also havelifetimes. Consider

>>> def foo():
...     x = 1
>>> foo()
>>> print x # 1
Traceback (most recent call last):
NameError: name 'x' is not defined

It isn’t just scope rules at point #1 that cause a problem (although that’s why we have a NameError)it also has to do with how function calls are implemented in Python and many other languages. Thereisn’t any syntax we can use to get the value of the variable x at this point - it literallydoesn’t exist! The namespace created for our function foo is created from scratch each time thefunction is called and it is destroyed when the function ends.

5. Function arguments and parameters

Python does allow us to pass arguments to functions. The parameter names become local variables inour function.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1}

Python has a variety of ways to define function parameters and passarguments to them. For the full skinny you’ll want tosee thePython documentation on defining functions. I’ll give youthe short version here: function parameters can beeither positional parameters thatare mandatory or named, defaultvalue parameters that are optional.

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
>>> foo(3) # 3
>>> foo() # 4
Traceback (most recent call last):
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5

At point #1 we are defining a function that has a single positional parameter x and asingle named parameter y. As we see at point #2 we can call this function passing argumentsnormally - the values are passed to the parameters of foo by position even though one isdefined in the function definition as a named parameter. We can also call the function withoutpassing any arguments at all for the named parameter as you can see at point #3 - Python uses thedefault value of 0 we declared if it doesn’t receive a value for the namedparameter y. Of course we can’t leave out values for the first (mandatory,positional) parameter - point #4 demonstrates that this results in an exception.

All clear and straightforward? Now it gets slightly confusing - Python supports named argumentsat function call time. Look at point #5 - here we are calling a function with two named argumentseven though it was defined with one named and one positional parameter. Since wehave names for our parameters the order we pass them in doesn’t matter.

The opposite case is true of course. One of the parameters for our function is defined as a namedparameter but we passed an argument to it by position - the call foo(3,1) at point #2 passes a 3 asthe argument to our ordered parameter x and passes the second (an integer 1) to the secondparameter even though it was defined as a named parameter.

Whoo! That feels like a lot of words to describe a pretty simple concept: function parameters canhave names or positions. This means slightly different things depending on whether we’re atfunction definition or function call time and we can use named arguments to functions defined onlywith positional parameters and vice-versa! Again - if that was all too rushed be sure to check outthe the docs.

6. Nested functions

Python allows the creation of nested functions. This means we can declare functions inside offunctions and all the scoping and lifetime rules still apply normally.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
>>> outer()

This looks a little more complicated, but it’s still behaving in a pretty sensible manner. Considerwhat happens at point #1 - Python looks for a local variable named x, failing it then looks in theenclosing scope which is another function! The variable x is a local variable to our functionouter but as before our function inner has access to the enclosing scope (read and modify accessat least). At point #2 we call our inner function. It’s important to remember that inner is alsojust a variable name that follows Python’s variable lookup rules - Python looks in the scope ofouter first and finds a local variable named inner.

7. Functions are first class objects in Python

This is simply the observation that in Python, functions are objects like everything else. Ah,function containing variable, you’re not so special!

>>> issubclass(int, object) # all objects in Python inherit from a common baseclass
>>> def foo():
...     pass
>>> foo.__class__ # 1
<type 'function'>
>>> issubclass(foo.__class__, object)

You may never have thought of your functions as having attributes - but functions are objects inPython, just like everything else. (If you find that confusing wait till you hear that classes areobjects in Python, just like everything else!) Perhaps this is making the point in an academic way -functions are just regular values like any other kind of value in Python. That means you can passfunctions to functions as arguments or return functions from functions as return values! If you’venever thought of this sort of thing consider the following perfectly legal Python:

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y): # 1
...     return func(x, y) # 2
>>> apply(add, 2, 1) # 3
>>> apply(sub, 2, 1)

This example might not seem too strange too you - add and sub are normal Python functions thatreceive two values and return a calculated value. At point #1 you can see that the variable intendedto receive a function is just a normal variable like any other. At point #2 we are calling thefunction passed into apply - parentheses in Python are the call operator and call the value thevariable name contains. And at point #3 you can see that passing functions as values doesn’t haveany special syntax in Python - function names are just variable labels like any other variable.

You might have seen this sort of behavior before - Python uses functions as arguments for frequentlyused operations like customizing the sorted builtin by providing a function to the keyparameter. But what about returning functions as values? Consider:

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
>>> foo = outer() #2
>>> foo # doctest:+ELLIPSIS
<function inner at 0x...>
>>> foo()
Inside inner

This may seem a little more bizarre. At point #1 I return the variable inner which happens to be afunction label. There’s no special syntax here - our function is returning the inner function whichotherwise couldn’t be called. Remember variable lifetime? The function inner is freshly redefinedeach time the function outer is called but if inner wasn’t returned from the function it wouldsimply cease to exist when it went out of scope.

At point #2 we can catch the return value which is our function inner and store it in a newvariable foo. We can see that if we evaluate foo it really does contain our function inner andwe can call it by using the call operator (parentheses, remember?) This may look a little weird, butnothing to hard to understand so far, right? Hold on, because things are about to take a turn forthe weird!

8. Closures

Let’s not start with a definition, let’s start with another code sample. What if we tweaked our lastexample slightly:

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)

From our last example we can see that inner is a function returned by outer, stored in avariable named foo and we could call it with foo(). But will it run? Let’s consider scopingrules first.

Everything works according to Python’s scoping rules - x is a local variable in our functionouter. When inner prints x at point #1 Python looks for a local variable to inner and notfinding it looks in the enclosing scope which is the function outer, finding it there.

But what about things from the point of view of variable lifetime? Our variable x is local to thefunction outer which means it only exists while the function outer is running. We aren’t able tocall inner till after the return of outer so according to our model of how Python works, xshouldn’t exist anymore by the time we call inner and perhaps a runtime error of some kind shouldoccur.

It turns out that, against our expectations, our returned inner function does work. Pythonsupports a feature called function closures which means that inner functions defined in non-globalscope remember what their enclosing namespaces looked like at definition time. This can be seen bylooking at the func_closure attribute of our inner function which contains the variables in theenclosing scopes.

Remember - the function inner is being newly defined each time the function outer is called. Rightnow the value of x doesn’t change so each inner function we get back does the same thing asanother inner function - but what if we tweaked it a little bit?

>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
>>> print2()

From this example you can see that closures - the fact that functions remember their enclosingscope - can be used to build custom functions that have, essentially, a hard coded argument. Wearen’t passing the numbers 1 or 2 to our inner function but are building custom versions of ourinner function that "remembers" what number it should print.

This alone is a powerful technique - you might even think of it as similar to object orientedtechniques in some ways: outer is a constructor for inner with x acting like a private membervariable. And the uses are numerous - if you are familiar with the key parameter in Python’ssorted function you have probably written a lambda function to sort a list of lists by the seconditem instead of the first. You might now be able to write an itemgetter function that accepts theindex to retrieve and returns a function that could suitably be passed to the key parameter.

But let’s not do anything so mundane with closures! Instead let’s stretch one more time and write adecorator!

9. Decorators!

A decorator is just a callable that takes a function as an argument and returns a replacementfunction. We’ll start simply and work our way up to useful decorators.

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() # 1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) # 2
>>> decorated()
before some_func

Look carefully through our decorator example. We defined a function named outer that hasa single parameter some_func. Inside outer we define an nested functionnamed inner. The inner function will print a string then call some_func,catching its return value at point #1. The value of some_func might be different eachtime outer is called, but whatever function it is we’ll callit. Finally inner returns the return value of some_func() + 1 - and we can see thatwhen we call our returned function stored in decorated at point #2 we get the results ofthe print and also a return value of 2 instead of the original return value 1 we would expect toget by calling foo.

We could say that the variable decorated is a decorated version of foo -it’s foo plus something. In fact if we wrote a useful decorator we might want toreplace foo with the decorated version altogether so we always got our "plus something"version of foo. We can do that without learning any new syntax simply by re-assigning the variablethat contains our function:

>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>

Now any calls to foo() won’t get the original foo, they’ll get ourdecorated version! Got the idea? Let’s write a more useful decorator.

Imagine we have a library that gives us coordinate objects. Perhaps they are primarily made up ofx and y coordinate pairs. Sadly the coordinate objects don’t supportmathematical operators and we can’t modify the source so we can’t add this supportourselves. We’re going to be doing a bunch of math, however, so we want to make addand sub functions that take two coordinate objects and do the appropriate mathematicalthing. These functions would be easy to write (I’ll provide a sample Coordinate class for thesake of illustration)

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
...     return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

But what if our add and subtract functions had to also have some bounds checking behavior? Perhapsyou can only sum or subtract based on positive coordinates and any result should be limited topositive coordinates as well. So currently

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

but we’d rather have have the difference of one and two be {x: 0, y: 0} and the sum of oneand three be {x: 100, y: 200} without modifying one, two, or three. Instead of addingbounds checking to the input arguments of each function and the return value of each function let’swrite a bounds checking decorator!

>>> def wrapper(func):
...     def checker(a, b): # 1
...         if a.x < 0 or a.y < 0:
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
...         if b.x < 0 or b.y < 0:
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
...         ret = func(a, b)
...         if ret.x < 0 or ret.y < 0:
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
...         return ret
...     return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

This decorator works just as before - returns a modified version of a function but in this caseit does something useful by checking and normalizing the input parameters and the return value,substituting 0 for any negative x or y values.

It’s a matter of opinion as to whether doing it this makes our code cleaner: isolating the boundschecking in its own function and applying it to all the functions we care to by wrapping them with adecorator. The alternative would be a function call on each input argument and on the resultingoutput before returning inside each math function and it is undeniable that using the decorator isat least less repetitious in terms of the amount of code needed to apply bounds checking to afunction. In fact - if its our own functions we’re decorating we could make the decoratorapplication a little more obvious.

10. The @ symbol applies a decorator to a function

Python 2.4 provided support to wrap a function in a decorator by pre-pending the function definitionwith a decorator name and the @ symbol. In the code samples above we decorated our function byreplacing the variable containing the function with a wrapped version.

>>> add = wrapper(add)

This pattern can be used at any time, to wrap any function. But if we are defining a function we can"decorate" it with the @ symbol like:

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

It’s important to recognize that this is no different than simply replacing the original variableadd with the return from the wrapper function - Python just adds some syntactic sugar to makewhat is going on very explicit.

Again - using decorators is easy! Even if writing useful decorators like staticmethod or classmethod would be difficult, using them is just a matter of prepending your function with @decoratorname!

11. *args and **kwargs

We’ve written a useful decorator but it’s hard coded to work only on a particular kind offunction - one which takes two arguments. Our inner function checker accepts two arguments andpasses the arguments on to the function captured in the closure. What if we wanted a decorator thatdid something for any possible function? Let’s write a decorator that increments a counter for everyfunction call of every decorated function without changing any of it’s decorated functions. Thismeans it would have to accept the calling signature of any of the functions that it decorates andalso call the functions it decorates passing on whatever arguments were passed to it.

It just so happens that Python has syntactic support for just this feature. Be sure to read the Python Tutorialfor more details but the * operator used when defining a function means that any extra positionalarguments passed to the function end up in the variable prefaced with a *. So:

>>> def one(*args):
...     print args # 1
>>> one()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args): # 2
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

The first function one simply prints whatever (if any) positional arguments are passed to it. Asyou can see at point #1 we simply refer to the variable args inside the function - *args is onlyused in the function definition to indicate that positional arguments should be stored in thevariable args. Python also allows us to specify some variables and catch any additional parametersin args as we can see at point #2.

The * operator can also be used when calling functions and here it means the analogous thing. Avariable prefaced by * when calling a function means that the variable contents should beextracted and used as positional arguments. Again by example:

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) # 1
>>> add(*lst) # 2

The code at point #1 does exactly the same thing as the code at point #2 - Python is doingautomatically for us at point #2 what we could do manually for ourselves. This isn’t too bad -*args means either extract positional variables from an iterable if calling a function or whendefining a function accept any extra positional variables.

Things get only slightly more complicated when we introduce ** which does for dictionaries &key/value pairs exactly what * does for iterables and positional parameters. Simple, right?

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

When we define a function we can use **kwargs to indicate that all uncaptured keyword argumentsshould be stored in a dictionary called kwargs. As before neither the name args nor kwargs ispart of Python syntax but it is convention to use these variable names when declaringfunctions. Just like * we can use ** when calling a function as well as when defining it.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)

12. More generic decorators

Given our new power we can write a decorator that "logs" the arguements to functions. We’ll justprint to stdout for simplicity sake:

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

Notice our inner function takes any arbitrary number and type of parameters at point #1and passes them along as arguments to the wrapped function at point #2. This allows us to wrap ordecorate any function, no matter it's signature.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
>>> foo1(1)
Arguments were: (1,), {}
>>> foo2()
Arguments were: (), {}

Calling our functions results in a "logging" output line as well as the expected return value ofeach function.

More about decorators

If you followed the last example you understand decorators! Congratulations - Go forth and use yournew powers for good!

You might also consider a little further study: Bruce Eckel has an excellent essay ondecorators and implements them in Python with objects instead of functions. You might find the OOPcode easier to read than our purely functional version. Bruce also has a follow-up essay onproviding arguments to decoratorsthat may also easier to implement with objects than with functions. Finally - you might alsoinvestigate the builtin functoolswraps function which (confusingly) is a decorator that can be used in our decorators tomodify the signature of our replacement functions so they look more like the decorated function.

[1] I also recently read an essay on explainingdecoratorsthat set me thinking…

[2] "global" is a big fat lie in Python which is a wonderful thing, but adiscussion for another time…

Update: Thanks to Nick I've updated my terminology throughout to be clear that "parameters" are the named variables in function signatures while "arguments" are the values passed to functions.

Update: I've fixed several typos in the article. Thanks Greg

Reading this article is like eating a mushroom by Mario.

or by real life

Python Decorators

In August 2009, I wrote a post titled Introduction to Python Decorators. It was an attempt to explain Python decorators in a way that I (and I hoped, others) could grok.

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 decoration line, which starts with an @ sign:

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. I now prefer the term “decoration” line.)

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 decoration line, when it should begin with the decorator itself. The explanation should end, not start, with the decorated function and the decoration line. The decoration 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 decoration line. For example, in his Dr. Dobb’s Journal article, after using the term “decorator” to refer to the decoration 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 “decoration syntax” that uses the “@” to create decoration lines. This feature is basically syntactic sugar that makes it possible to re-write our last example this way:

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 decoration 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 invocation line” or a “decoration line” or simply a “decoration”… 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 decorators decoration 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”.

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.

个人分类: Python
下一篇Python Decorators 3
想对作者说点什么? 我来说一句