Python Study 4 - Decorator

Original answer from stackoverflow: http://stackoverflow.com/questions/739654/how-to-make-a-chain-of-function-decorators-in-python/1594484#1594484

Some Basic Concepts to Understand Decorators

Functions are objects and they:

  • can be assigned to a variable
  • can be defined in another function

which means that a function can return another function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def getTalk(kind="shout"):

# We define functions on the fly
def shout(word="yes"):
return word.capitalize()+"!"

def whisper(word="yes") :
return word.lower()+"...";

# Then we return one of them
if kind == "shout":
# We don't use "()", we are not calling the function, we are returning the function object
return shout
else:
return whisper

# How do you use this strange beast?

# Get the function and assign it to a variable
talk = getTalk()

# You can see that "talk" is here a function object:
print(talk)
#outputs : <function shout at 0xb7ea817c>

# The object is the one returned by the function:
print(talk())
#outputs : Yes!

# And you can even use it directly if you feel wild:
print(getTalk("whisper")())
#outputs : yes...

What’s more, if you can return a function, you can pass one as a parameter:

1
2
3
4
5
6
7
8
def doSomethingBefore(func): 
print("I do something before then I call the function you gave me")
print(func())

doSomethingBefore(scream)
#outputs:
#I do something before then I call the function you gave me
#Yes!

Definition

Decorators are:

  • “wrappers” - which means that they let you execute code before and after the function they decorate without modifying the function itself
  • a pythonic variant of the decorator design pattern

Note

  • Decorators were introduced in Python 2.4.
  • Decorators slow down the function call.
  • The order you set the decorators MATTERS.
  • You cannot un-decorate a function. (There are hacks to create decorators that can be removed, but nobody uses them.) So once a function is decorated, it’s decorated for all the code.
  • Decorators wrap functions, which can make them hard to debug. (This gets better from Python >= 2.5 by having functools module which includes the function functools.wraps() that copies the name, module, and docstring of the decorated function to its wrapper.)

Why decorators?

Classic uses are extending a function behavior from an external lib (you can’t modify it), or for debugging (you don’t want to modify it because it’s temporary).

You can use them to extend several functions in a DRY’s way, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def benchmark(func):
"""
A decorator that prints the time a function takes
to execute.
"""

import time
def wrapper(*args, **kwargs):
t = time.clock()
res = func(*args, **kwargs)
print("{0} {1}".format(func.__name__, time.clock()-t))
return res
return wrapper


def logging(func):
"""
A decorator that logs the activity of the script.
(it actually just prints it, but it could be logging!)
"""

def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print("{0} {1} {2}".format(func.__name__, args, kwargs))
return res
return wrapper


def counter(func):
"""
A decorator that counts and prints the number of times a function has been executed
"""

def wrapper(*args, **kwargs):
wrapper.count = wrapper.count + 1
res = func(*args, **kwargs)
print("{0} has been used: {1}x".format(func.__name__, wrapper.count))
return res
wrapper.count = 0
return wrapper

@counter
@benchmark
@logging
def reverse_string(string):
return str(reversed(string))

print(reverse_string("Able was I ere I saw Elba"))
print(reverse_string("A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!"))

#outputs:
#reverse_string ('Able was I ere I saw Elba',) {}
#wrapper 0.0
#wrapper has been used: 1x
#ablE was I ere I saw elbA
#reverse_string ('A man, a plan, a canoe, pasta, heros, rajahs, a coloratura, maps, snipe, percale, macaroni, a gag, a banana bag, a tan, a tag, a banana bag again (or a camel), a crepe, pins, Spam, a rut, a Rolo, cash, a jar, sore hats, a peon, a canal: Panama!',) {}
#wrapper 0.0
#wrapper has been used: 2x
#!amanaP :lanac a ,noep a ,stah eros ,raj a ,hsac ,oloR a ,tur a ,mapS ,snip ,eperc a ,)lemac a ro( niaga gab ananab a ,gat a ,nat a ,gab ananab a ,gag a ,inoracam ,elacrep ,epins ,spam ,arutaroloc a ,shajar ,soreh ,atsap ,eonac a ,nalp a ,nam A

Of course the good thing with decorators is that you can use them right away on almost anything without rewriting. DRY, I said:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@counter
@benchmark
@logging
def get_random_futurama_quote():
from urllib import urlopen
result = urlopen("http://subfusion.net/cgi-bin/quote.pl?quote=futurama").read()
try:
value = result.split("<br><b><hr><br>")[1].split("<br><br><hr>")[0]
return value.strip()
except:
return "No, I'm ... doesn't!"


print(get_random_futurama_quote())
print(get_random_futurama_quote())

#outputs:
#get_random_futurama_quote () {}
#wrapper 0.02
#wrapper has been used: 1x
#The laws of science be a harsh mistress.
#get_random_futurama_quote () {}
#wrapper 0.01
#wrapper has been used: 2x
#Curse you, merciful Poseidon!

Python itself provides several decorators: property, staticmethod, etc.

  • Django uses decorators to manage caching and view permissions.
  • Twisted to fake inlining asynchronous functions calls.

This really is a large playground.

Handcrafted Decorators vs Decorator Syntax in Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# A decorator is a function that expects ANOTHER function as parameter
def my_shiny_new_decorator(a_function_to_decorate):

# Inside, the decorator defines a function on the fly: the wrapper.
# This function is going to be wrapped around the original function
# so it can execute code before and after it.
def the_wrapper_around_the_original_function():

# Put here the code you want to be executed BEFORE the original function is called
print("Before the function runs")

# Call the function here (using parentheses)
a_function_to_decorate()

# Put here the code you want to be executed AFTER the original function is called
print("After the function runs")

# At this point, "a_function_to_decorate" HAS NEVER BEEN EXECUTED.
# We return the wrapper function we have just created.
# The wrapper contains the function and the code to execute before and after. It’s ready to use!
return the_wrapper_around_the_original_function

# Now imagine you create a function you don't want to ever touch again.
def a_stand_alone_function():
print("I am a stand alone function, don't you dare modify me")

a_stand_alone_function()
#outputs: I am a stand alone function, don't you dare modify me

# Well, you can decorate it to extend its behavior.
# Just pass it to the decorator, it will wrap it dynamically in
# any code you want and return you a new function ready to be used:

a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

Now, you probably want that every time you call a_stand_alone_function, a_stand_alone_function_decorated is called instead. That’s easy, just overwrite a_stand_alone_function with the function returned by my_shiny_new_decorator:

1
2
3
4
5
6
7
8
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#outputs:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

# That’s EXACTLY what decorators do!

The previous example, using the decorator syntax in Python:

1
2
3
4
5
6
7
8
9
@my_shiny_new_decorator
def another_stand_alone_function():
print("Leave me alone")

another_stand_alone_function()
#outputs:
#Before the function runs
#Leave me alone
#After the function runs

Therefore, @decorator is just a shortcut to:

1
another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

Of course, you can accumulate decorators:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def bread(func):
def wrapper():
print("</''''''\>")
func()
print("<\______/>")
return wrapper

def ingredients(func):
def wrapper():
print("#tomatoes#")
func()
print("~salad~")
return wrapper

def sandwich(food="--ham--"):
print(food)

sandwich()
#outputs: --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

Using the Python decorator syntax:

1
2
3
4
5
6
7
8
9
10
11
12
@bread
@ingredients
def sandwich(food="--ham--"):
print(food)

sandwich()
#outputs:
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

The order you set the decorators MATTERS:

1
2
3
4
5
6
7
8
9
10
11
12
@ingredients
@bread
def strange_sandwich(food="--ham--"):
print(food)

strange_sandwich()
#outputs:
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

Passing arguments to the decorated function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# It’s not black magic, you just have to let the wrapper 
# pass the argument:

def a_decorator_passing_arguments(function_to_decorate):
def a_wrapper_accepting_arguments(arg1, arg2):
print("I got args! Look: {0}, {1}".format(arg1, arg2))
function_to_decorate(arg1, arg2)
return a_wrapper_accepting_arguments

# Since when you are calling the function returned by the decorator, you are
# calling the wrapper, passing arguments to the wrapper will let it pass them to
# the decorated function

@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
print("My name is {0} {1}".format(first_name, last_name))

print_full_name("Peter", "Venkman")
# outputs:
#I got args! Look: Peter Venkman
#My name is Peter Venkman

Decorating methods
One nifty thing about Python is that methods and functions are really the same. The only difference is that methods expect that their first argument is a reference to the current object (self).

That means you can build a decorator for methods the same way! Just remember to take self into consideration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def method_friendly_decorator(method_to_decorate):
def wrapper(self, lie):
lie = lie - 3 # very friendly, decrease age even more :-)
return method_to_decorate(self, lie)
return wrapper


class Lucy(object):

def __init__(self):
self.age = 32

@method_friendly_decorator
def sayYourAge(self, lie):
print("I am {0}, what did you think?".format(self.age + lie))

l = Lucy()
l.sayYourAge(-3)
#outputs: I am 26, what did you think?

If you’re making general-purpose decorator–one you’ll apply to any function or method, no matter its arguments–then just use *args, **kwargs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def a_decorator_passing_arbitrary_arguments(function_to_decorate):
# The wrapper accepts any arguments
def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs):
print("Do I have args?:")
print(args)
print(kwargs)
# Then you unpack the arguments, here *args, **kwargs
# If you are not familiar with unpacking, check:
# http://www.saltycrane.com/blog/2008/01/how-to-use-args-and-kwargs-in-python/
function_to_decorate(*args, **kwargs)
return a_wrapper_accepting_arbitrary_arguments

@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
print("Python is cool, no argument here.")

function_with_no_argument()
#outputs
#Do I have args?:
#()
#{}
#Python is cool, no argument here.

@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)

function_with_arguments(1,2,3)
#outputs
#Do I have args?:
#(1, 2, 3)
#{}
#1 2 3

@a_decorator_passing_arbitrary_arguments
def function_with_named_arguments(a, b, c, platypus="Why not ?"):
print("Do {0}, {1} and {2} like platypus? {3}".format(a, b, c, platypus))

function_with_named_arguments("Bill", "Linus", "Steve", platypus="Indeed!")
#outputs
#Do I have args ? :
#('Bill', 'Linus', 'Steve')
#{'platypus': 'Indeed!'}
#Do Bill, Linus and Steve like platypus? Indeed!

class Mary(object):

def __init__(self):
self.age = 31

@a_decorator_passing_arbitrary_arguments
def sayYourAge(self, lie=-3): # You can now add a default value
print("I am {0}, what did you think?".format(self.age + lie))

m = Mary()
m.sayYourAge()
#outputs
# Do I have args?:
#(<__main__.Mary object at 0xb7d303ac>,)
#{}
#I am 28, what did you think?