simeonfranklin.com –
Understanding Python Decorators in 12 Easy Steps!
original source : http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/
9. Decorators!
A decorator is just a callable that takes a function as an argument and returns a replacement function. 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 2
Look carefully through our decorator example. We defined a function named outer that has a single parameter some_func. Inside outer we define an nested function named 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 each time outer is called, but whatever function it is we’ll call it. Finally inner returns the return value ofsome_func() + 1 – and we can see that when we call our returned function stored in decorated at point #2 we get the results of the print and also a return value of 2 instead of the original return value 1 we would expect to get 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 to replace 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 variable that 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 our decorated 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 of x andy coordinate pairs. Sadly the coordinate objects don’t support mathematical operators and we can’t modify the source so we can’t add this support ourselves. We’re going to be doing a bunch of math, however, so we want to make add and sub functions that take two coordinate objects and do the appropriate mathematical thing. These functions would be easy to write (I’ll provide a sample Coordinate class for the sake 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? Perhaps you can only sum or subtract based on positive coordinates and any result should be limited to positive 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 one and three be {x: 100, y: 200} without modifying one, two, or three. Instead of adding bounds checking to the input arguments of each function and the return value of each function let’s write 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 case it 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 bounds checking in its own function and applying it to all the functions we care to by wrapping them with a decorator. The alternative would be a function call on each input argument and on the resulting output before returning inside each math function and it is undeniable that using the decorator is at least less repetitious in terms of the amount of code needed to apply bounds checking to a function. In fact – if its our own functions we’re decorating we could make the decorator application 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 definition with a decorator name and the @ symbol. In the code samples above we decorated our function by replacing 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 variable add with the return from the wrapper function – Python just adds some syntactic sugar to make what 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!