def times2(x):
return x * 2
>>> list(map(times2, [0, 1, 2, 3, 4]))
[0, 2, 4, 6, 8]
Yeah yeah, I know that you can do the same thing with a list comprehension or generator expression, but my point was about an independent piece of logic [like times2()] and mapping that function across a data set ([0, 1, 2, 3, 4]) to generate a new data set ([0, 2, 4, 6, 8]). However, since mapping functions like times2()aren't tied to any particular chunk of data, you can reuse them elsewhere with other unrelated (or related) data.
Along similar lines, consider function calls. You have independent functions and methods in classes. Now, think about "mapped" execution across functions. What are things that you can do with functions that don't have much to do with the behavior of the functions themselves? How about logging function calls, timing them, or some other introspective, cross-cutting behavior. Sure you can implement that behavior in each of the functions that you care about such information, however since they're so generic, it would be nice to only write that logging code just once.
Introduced in 2.4, decorators modularize
cross-cutting behavior so that developers don't
have to implement near duplicates of the same piece of code for each function.
Rather, Python gives them the ability to put that logic in one place and use
decorators with its at-sign ("@") syntax to "map" that
behavior to any function (or method). This compartmentalization of cross-cutting
functionality gives Python an aspect-oriented programming flavor.
How do you do this in Python? Let's take a look at a simple
example, the logging of function calls. Create a decorator function that takes
a function object as its sole argument, and implement the cross-cutting
functionality. In logged() below, we're just going to log function calls by
making a call to the print() function each time a logged function is called.
def logged(_func):
def _wrapped():
print('Function %r called at: %s' % (
print('Function %r called at: %s' % (
_func.__name__, ctime()))
return _func()
return _wrapped
return _wrapped
In logged(), we use the function's name (given by
func.__name__) plus a timestamp from time.ctime() to build our output string.
Make sure you get the right imports, time.ctime() for sure, and if using Python
2, the print() function:
from __future__ import print_function # 2-3 compatibility
from time import ctime
from time import ctime
Now that we have our logged() decorator, how do we use it? On
the line above the function which you want to apply the decorator to, place an
at-sign in front of the decorator name. That's followed immediately on the next
line with the normal function declaration. Here's what it looks like, applied
to a boring generic foo() function which just print()s it's been called.
@logged
def foo():
print('foo()
called')
When you call foo(), you can see that the decorator logged()
is called first, which then calls foo() on your behalf:
$ log_func.py
Function 'foo' called at: Sun Jul 27 04:09:37 2014
foo() called
If you take a closer look at logged() above, the way the decorator works is that the decorated function is
"wrapped" so that it is passed as func to the decorator then the
newly-wrapped function _wrapped()is (re)assigned as foo(). That's why it now behaves the
way it does when you call it.
The entire script:
#!/usr/bin/env python
'log_func.py -- demo of decorators'
from __future__ import print_function # 2-3 compatibility
from time import ctime
def logged(_func):
def _wrapped():
print('Function %r called at: %s' % (
_func.__name__, ctime()))
return _func()
return _wrapped
@logged
def foo():
print('foo() called')
foo()
That was just a simple example to give you an idea of what decorators are. If you dig a little deeper, you'll discover one caveat is that the wrapping isn't perfect. For example, the attributes of foo() are lost, i.e., its name and docstring. If you ask for either, you'll get _wrapped()'s info instead:
>>> print("My name:", foo.__name__) # should be 'foo'!
My name: _wrapped
>>> print("Docstring:", foo.__doc__) # _wrapped's docstring!
Docstring: None
In reality, the "@" syntax is just a shortcut. Here's what you really did, which should explain this behavior:
So as you can tell, it's not a complete wrap. A convenience function that ties up these loose ends is functools.wraps(). If you use it and run the same code, you will get foo()'s info. However, if you're not going to use a function's attributes while it's wrapped, it's less important to do this.
There's also support for additional features, such calling decorated functions with parameters, applying more complex decorators, applying multiple levels of decorators, and also class decorators. You can find out more about (function and method) decorators in Chapter 11 of Core Python Programming or live in my upcoming course which starts in just a few days near the San Francisco airport... there are still a few seats left!
>>> print("My name:", foo.__name__) # should be 'foo'!
My name: _wrapped
>>> print("Docstring:", foo.__doc__) # _wrapped's docstring!
Docstring: None
def foo():
print('foo() called')
foo = logged(foo) # returns _wrapped (and its attributes)
So as you can tell, it's not a complete wrap. A convenience function that ties up these loose ends is functools.wraps(). If you use it and run the same code, you will get foo()'s info. However, if you're not going to use a function's attributes while it's wrapped, it's less important to do this.
There's also support for additional features, such calling decorated functions with parameters, applying more complex decorators, applying multiple levels of decorators, and also class decorators. You can find out more about (function and method) decorators in Chapter 11 of Core Python Programming or live in my upcoming course which starts in just a few days near the San Francisco airport... there are still a few seats left!