Python Decorators in 12 Steps (2012)
simeonfranklin.com
simeonfranklin.com
Our infrastructure daemons were written in Python, and I wanted to make sure we had a debuggable system that allows me to trace any error without having to manually reproduce it (especially when, say, problems specific to a customer's network can't be reproduced on ours). I wrote a decorator, called @logme, that would log a function's name and parameters plus the module it belonged to (easier said than done!), run the function, then log and return the return value. Every function was decorated with @logme. I also made a variant for methods called @logmethod that would also log data about the class the method belonged to.
Now, obviously, this generated massive amounts of logs, so I put in a healthy amount of log rotation (the Python logger is beautifully customizable). On average, logs would keep for a few days.
So many times, I debugged customer problems -- problems that would've been impossible to reproduce in our lab because of edge cases our customers encountered -- just from the incredibly detailed logs they'd send me.
Also, it was imperative that the main infrastructure daemon never, ever crashes. It sat and waited for commands to be sent to it. When it finishes executing one command, it goes back to listening. If it got an error running some command, it should report that error and then go back to listening for the next command; an error running an individual command should never bring down the system. The decorators took care of that too. In addition to logging what goes into and out of every function, @logme and @logmethod also caught any exception those functions raised and logged them, allowing the system to recover following any exception just by decorating everything (I would not recommend this for a more stateful system: it only worked as well as it did because any configuration needed was always loaded from disk when each command was run, so the daemon overall had no state that could be corrupted by an error).
@decorator_without_args
def function():
pass
this is equivalent to function = decorator_without_args(function)
now consider the other case, where the decorator takes in some args: @dec_with_args("hello", "world")
def func():
pass
this is equivalent to function = decorator_with_args("hello", "world")(function)
Or in other words, the expression after @ needs to evaluate to a Callable which is then called on the function you are decorating, so with @a_decorator, you call a_decorator on your function. When you use @with_args(1, 2, 3), you are calling the function "with_args" and then the result of that function is your decorator. So an example of this working is like so: debug_list = []
other_list = []
def dynamic_debug_decorator(output_list=debug_list):
# dynamic debug decorator should return a decorator function
# realistically, ddd is a decorator factory, not a decorator
def debug_decorator(function):
# so this is our real decorator
def decorated_function(*args, **kwargs):
# and this is the new function we return
output_list.append(function) # because silly example
return function(*args, **kwargs)
return decorated_function
return debug_decorator
so now using that decorator: @dynamic_debug_decorator
def function():
pass
this fails, because dynamic debug decorator takes a list, but is now receiving a function. Instead we need to call it like so: @dynamic_debug_decorator
def function():
pass
this works, using the default values. @dynamic_debug_decorator()
def function():
passannoyed edit: replace ^ with asterisk, which I can't get HN to print even escaping them with backslashes. thanks bad syntax formatting code
When I read code not written by myself and see this:
@something
def foo(bar):
...
It is not immediately clear to me what foo actually does whereas
def foo(bar):
# some function calls here
# next: what foo does with these results
# some more function calls on what has been done before
gives me a hint without scrolling through the code.
Python was far and away my favorite, since we could take advantage of decorators to make everything relatively seamless.
Consider a function something like this:
def do_something_transactionally(db, *args, **kwargs):
tr = db.create_transaction()
# do some things with the transaction object
tr.commit().get()
This code was broken in a number of ways -- there's no retry, or handling of errors from commit, etc.So we exposed a decorator in Python to handle all of these issues for the common case:
@transactional
def do_something_transactionally(db, *args, **kwargs):
# do some things with the transaction object
This was more or less equivalent to: def do_something_transactionally(db, *args, **kwargs):
tr = db.create_transaction()
while True:
try:
# do some things with the transaction object
tr.commit().wait()
break
except Error as e:
tr.on_error(e).get()
It was actually even niftier than that, though, because the decorator allowed you to pass in either a transaction object or a database object. If the caller provided a transaction rather than a database, then the decorator did not create a new transaction or commit it; this allowed composition of more complicated transactional calls of decorated functions.So I will definitely grant you that if you encountered this code, you would need to understand what @transactional meant, but I would definitely argue that this was cleaner and easier to deal with than asking every function to explicitly implement the retry logic.
with transactional as transaction:
# do something
It's pretty clear at the call site what is and what isn't transactional and most importantly is practically the pythonic way of writing code with guaranteed cleanup.I could be wrong, though -- I only remember being seduced by the context manager option a couple times before running into the same wall.
I don't think the context manager, even if it had been doable, would have supported composition in the style of:
@transactional
def thing_one(tr, *args):
# stuff
@transactional
def thing_two(tr, *args):
# other stuff
@transactional
def both_things(tr, *args):
thing_one(tr, args)
thing_two(tr, args)
If it's not clear, in this case a caller could provide a database object to thing_one or thing_two and they would be executed as a single atomic transaction, but a call to both_things with a database object would execute both thing_one and thing_two inside of a single atomic transaction.foo1()
stuff
foo2()
would be two function calls. If every function has a helpful name then two function calls are more telling than one, no?
Of course the level of desired detail varies with what one does.
@logging might be enough of a hint if the function really does nothing else but write a log entry.
def baz():
bar1()
foo()
bar2()
is hugely better than: @bar
def baz():
foo()
I think it massively depends on naming and the general context. I can see myself choosing either depending on the precise situation.As with all things in Python (and coding in general) taste and discretion are key: use decorators when they improve legibility and stop using them when they don't.
def calling_context: bar(foo)
is cleaner.
The problem with decorators is that they don't know the calling context, or provide a mechanism to adapt to that context short of monkey patching.
That said, they are really, really convenient.
I would like to see some more articles on Python metaclasses as I think that could benefit a lot of people.
This could mean validating function inputs, changing inputs, raising exceptions, doing cleanup, etc. I use them liberally for web service endpoints to validate security, data integrity, etc.
I use them every day but I think this view only seems true when one is looking back in hindsight. Even fundamental ideas like assignment and the scoping of variables can be difficult for newbies. To really grok decorators (i.e. not just to use them as prescribed by the Django/Flask/whatever docs but to take the next step and create your own), one needs to be comfortable with the idea of first class functions - not a first day topic, at any rate.
I find them easiest to understand using the source-rewriting explanation:
@foo
def bar(...):
...
is the same as def bar(...):
...
bar = foo(bar)
Classes can be decorated in the same way. This also explains why decorators taking arguments work how they do: because @foo(...)
turns into bar = foo(...)(bar)
and thus foo needs to return a decorator, not be a decorator. (Where a decorator can be thought of as "any function that accepts a single argument", or as "any function which happens to be useful as a decorator", or whatever.)But explaining them like this doesn't make it obvious what they're for. (I can worry about that later, but others find it easier to understand things given a motivating example.) And some people haven't got their heads around first-class functions yet. So while this would be my preferred explanation if I was talking to a version of me who didn't know decorators yet, it's not the best for everyone.
[1] This hasn't previously occurred to me, and I've never seen it explored, but I just verified that it works:
def const(fun):
return fun()
@const
def bar():
return 3
print 3 * bar # 9
I'm not sure if this would be useful for anything.