Decorators¶
Getting Started¶
Simple decorators¶
def decorator(func):
print 'name:', func.__name__
print '# arguments:', func.func_code.co_argcount
print '-' * 80
return func
@decorator
def func1(a, b, c):
print a, b, c
@decorator
def func2(w, x, y, z):
print w, x, y, z
Expected output:
name: func1
# arguments: 3
--------------------------------------------------------------------------------
name: func2
# arguments: 4
--------------------------------------------------------------------------------
This is a simple decorator that accepts no arguments. It is standard practice to have a decorator return a function (though it’s not required). If you are not planning on transforming a function, then just return the same function that was passed in.
In this particular decorator, all we do is print the name of the target function and the number of arguments it takes. Note that we didn’t have to invoke either of the functions for that information to be printed.
Keep in mind that the @
syntax is not strictly necessary, it’s just syntactic sugar. To apply the decorator, you could have written:
func1 = decorator(func1)
Decorators that transform¶
import time
def decorator(func):
def newfunc(*args):
startTime = time.time()
result = func(*args)
endTime = time.time()
print 'Took %f seconds to run' % (endTime - startTime)
return result
return newfunc
@decorator
def sum_of_range(start, stop, step):
return sum(i for i in xrange(start, stop, step))
print sum_of_range(345, 9999999, 3)
Expected output:
Took 1.172000 seconds to run
16666658313669
This decorator replaces a function with a new function that does the the same thing but also prints the duration of execution. Inside the decorator
function we create a new function called newfunc
. Inside newfunc
, func
is invoked with the same arguments that were received by newfunc
.
Decorator methods¶
class Template(object):
def __init__(self, message):
self.message = message
def __call__(self, func):
def newfunc(*args):
return self.message % func(*args)
return newfunc
@Template('%s your %s are belong to %s')
def func1(quantity, object, group):
return quantity, object, group
print func1('All', 'base', 'us')
We can create a callable object that acts as the decorator. This is a common method of creating decorators that accept non-function input.
In this example, the original function only returns a tuple. But after being transformed by the decorator, it now returns a string.
The following is an equivalent way to apply our decorator method:
func1 = Template('%s your %s are belong to %s')(func1)
Decorators that return decorators¶
def template(message):
def decorator2(func):
def newfunc(*args):
return message % func(*args)
return newfunc
return decorator2
@template('%s your %s are belong to %s')
def func1(quantity, object, group):
return quantity, object, group
print func1('All', 'base', 'us')
Expected output:
All your base are belong to us
OK, things are getting a little tricky now. This example is equivalent to the last one, except that we are using nested functions and closures to achieve the same affect.
This is the rule: A decorator that does not accept a single function argument must return another decorator. This returned decorator is then applied to the function.
The following is an equivalent way to apply our decorator:
func1 = template('%s your %s are belong to %s')(func1)
As a side note, decorators that return decorators almost always use closures.
Applying multiple decorators¶
def decorator1(func):
print 'Inside decorator1'
return func
def decorator2(func):
print 'Inside decorator2'
return func
@decorator1
@decorator2
def func1(a, b, c):
return a, b, c
print func1('All', 'base', 'us')
Expected output:
Inside decorator2
Inside decorator1
('All', 'base', 'us')
Python gives a nice syntax for applying multiple decorators. When we use this syntax, we often say that we are “stacking decorators”. Note that the decorators are applied in the opposite order in which they were stacked.
The non-nice way to applying multiple decorators looks like this:
func1 = decorator1(decorator2(func1))
Hopefully, this will help you remember why you should return a function inside a decorator – if you don’t, then the next decorator in the stack won’t have a function to apply itself to.
What Exactly Are Decorators?¶
A decorator is either:
- A callable that accepts a function
- A callable that accepts some arguments and returns a decorator
Any callable can be a decorator. That means functions, methods, and objects that implement the magic method __call__
.
Although decorators have been in Python for a long time, the special decorator syntax using @
was not added to Python until version 2.4. So stay away from earlier versions of Python if you want to make extensive use of decorators.
Use Cases¶
Event Handling¶
Pyglet is a multimedia framework that is useful for making games. It gives you an easy way to do event handling through the Window.event
decorator method. Attaching Window.event
to a function causes that function to respond to a particular window event.
from pyglet import window
from pyglet.gl import *
win = window.Window()
@win.event
def on_key_press(symbol, modifiers):
print symbol, modifiers
@win.event
def on_mouse_press(x, y, button, modifiers):
print x, y, button, modifiers
@win.event
def on_resize(width, height):
glClearColor(0.2, 0.6, 0.3, 1)
Which event does a function get attached to? The Window.event
decorator decides this by looking at the name of the function (available through the __name__
magic attribute).
Pyglet also lets you bind event callbacks using a more traditional API:
win = window.Window()
def on_key_press(symbol, modifiers):
print symbol, modifiers
win.on_key_press = on_key_press
def on_mouse_press(x, y, button, modifiers):
print x, y, button, modifiers
win.on_mouse_press = on_mouse_press
def on_resize(width, height):
glClearColor(0.2, 0.6, 0.3, 1)
win.on_resize = on_resize
As you can see, this version uses the same number of lines, but is more redundant and not as easy to read.
Adding Metadata to a Function¶
TurboGears is a full stack web framework. In TurboGears, the expose
decorator essentially causes a method to be mapped to a URL. For example, navigating to http://localhost/pagelist
causes the exposed pagelist
method to be invoked.
from turbogears import controllers, expose
class Root(controllers.RootController):
@expose
def index(self, pagename="FrontPage"):
page = Page.byPagename(pagename)
content = 'Salutations, Friend'
return content
@expose
def pagelist(self):
pages = [page.pagename for page in Page.select()]
return '<br>'.join(str(p) for p in pages)
Part of how expose
works is to set or modify attributes on the target function. For example, the exposed
attribute is set to True
to let the framework know which methods are to exposed to the Web and which are just “private” methods. Also, since multiple expose
decorators might be used on a method, the information for each one is appended to the _ruleinfo
attribute.
Transforming a Function¶
TurboGears’s expose
decorator also allows you to attach a template to a method. This alters the behavior of the method. In the following example, the index
method returns a dictionary inside its body. However, the expose
decorator causes it to return a string instead.
from turbogears import controllers, expose
class Root(controllers.RootController):
@expose(template="wiki20.templates.page")
def index(self, pagename="FrontPage"):
page = Page.byPagename(pagename)
content = 'Salutations, Friend'
return dict(data=content, page=page)
@expose(template="wiki20.templates.pagelist")
@expose("json")
def pagelist(self):
pages = [page.pagename for page in Page.select()]
return dict(pages=pages)
Templates in this context are usually HTML pages with “holes” in them; the holes are filled by the values in the dictionary returned by the original function. In the context of the Model-View-Controller pattern, decorators provide the “glue” that connects views to controllers.
The Root.pagelist
method shows that you can stack decorators on top of each other. The top invocation, @expose(template="wiki20.templates.pagelist")
exposes the method and assigns a template to it. The bottom invocation, @expose("json")
, allows the method to render its output in JSON format. If you navigate to http://localhost/pagelist?tg_format=json
, you’ll see JavaScript instead of a web page.
Although a decorator certainly can modify a function, that’s not what usually happens. Most likely the original function has been substituted for another function (and this substituted function probably makes use of the original function). That’s why we usually say that a decorator transforms its target function.
Decorators work with both functions and methods. Just remember that even though we keep talking about functions, we really mean functions and methods.
Recipes¶
Custom Event Binding¶
In this recipe, we explore ways to bind callbacks to an event that we create. Specifically, we create a class Downloader
that has a done
event that gets fired when all downloads have completed. A naive implementation might involve creating a done_callback
attribute, then assigning a function to it:
downloader = Downloader(url_list)
def ondone(num_downloaded, time_elapsed):
print "Fetched", num_downloaded, "files in", time_elapsed, "seconds"
downloader.done_callback = ondone
downloader.start_downloads()
Once we’re done fully implementing our recipe, we’ll be able to bind and define our callbacks in a single step. Not only that, we’ll have the ability to bind multiple callbacks to the done
event:
downloader = Downloader(url_list)
@downloader.done
def _(count, time_elapsed):
print "Fetched", count, "files in", time_elapsed, "seconds"
@downloader.done
def _(count, time_elapsed):
print '%s downloaded in %0.3f minutes' % (count, time_elapsed / 60.0)
downloader.start_downloads()
Event Binding in Tkinter¶
Tkinter is the default GUI toolkit for Python. It’s great! Well, it’s not great, per se, but it ships with Python, so it’s probably sitting on your system somewhere, ready to be used. So let’s use it! Here’s an example of binding a callback function to a button widget:
from Tkinter import *
def onclick():
print 'You clicked on a button'
frame = Frame()
frame.master.title("Event binding with decorators")
frame.pack()
btn1 = Button(frame, text="One")
btn1.pack()
btn2 = Button(frame, text="Two")
btn2.pack()
btn1['command'] = onclick
btn2['command'] = onclick
frame.mainloop()
The resulting GUI program:
Tkinter has a pretty simple event binding mechanism for buttons: just assign the command
item to the callback function. However, notice that you have to first define the function, then bind it to the button’s command event. Why can’t we define and bind at the same time?
Once you’ve properly implemented this recipe, you’ll be able to write the following:
btn1 = MyButton(frame, text="One")
btn1.pack()
btn2 = MyButton(frame, text="Two")
btn2.pack()
@btn1.command
@btn2.command
def onclick(target):
print 'You clicked on button <%s>' % target['text']
The resulting code is more cohesive than the original.
The first solution to event binding that we describe involves subclassing Tkinter’s Button
class. However, this isn’t necessarily a great idea because we probably don’t want to subclass every single widget in Tkinter. Instead, we can use a decorator function:
lb = Listbox(frame, name='lb')
for s in ['One', 'Two', 'Three', 'Four']:
lb.insert(END, s)
lb.pack()
@bind(lb, '<<ListboxSelect>>')
def onselect(evt):
w = evt.widget
index = int(w.curselection()[0])
value = w.get(index)
print 'You selected item %d: "%s"' % (index, value)
The resulting GUI program:
In the above example, we bind the onselect
function to the ListboxSelect
event on the lb
object.
Decorator Caveats¶
Can make a mess of your code¶
Using a lot of decorators can make your code fairly convoluted. Decorators that transform other functions usually need to use nested functions. Too much nesting of functions can really make a mess of your code. If you find yourself writing tons of nested functions, ask yourself if there isn’t a simpler way to do it. In many cases, metaclasses can be used in place of decorators, since a metaclass is very much like a “decorator” that acts on classes. So if you have a set of related functions that all need to be transformed in some way, consider grouping them into methods under a single class and then writing a metaclass.
Be nice to the decorated function¶
If you return a new function in place of an old one, you run the risk of confusing something else in the program that expected attributes from the original function. For example, if you use a discovery based test runner that looks for methods prefixed with “test” then you will need to copy the func.__name__
attribute in your decorated function. In Python 2.5, the builtin @functools.wraps(func)
is a decorator that returns a function to look like func
and is meant to solve this problem.