9 February 2012 0:00 AM (python | coding | decorators)
Time for me to post something about decorators, whynot.
Simple decorator
Functional
From what I see, most documents start out with a decorator as a class. I actually find, for Python, the functional one much easier to start with.
The simplest, functional, and still illustrative decorator, as found here, is:
def entryExit(function): def new_function(): print "Entering", function.__name__ function() print "Exited", function.__name__ return new_function @entryExit def func(): print 'inside func' func()
That's pretty easy, really. All it does is replace
func
with an instance ofnew_function
. Thisnew_function
calls the originalfunc
, but before that it prints "Entering func" and afterwards it prints "Exited func". So basically this one is just a wrapper around your function.
OO
I find that, for the first time looking at it, the functional decorator is easier to understand. Although it does require that you know how functional programming works, of course. This solution is somewhat clearer once you understand that the
__call__
function is called when you try to use an object like a function:class entryExit: def __init__(self, function): self.function = function def __call__(self): print "Entering", self.function.__name__ self.function() print "Exited", self.function.__name__ @entryExit def func(): print 'inside func' func()
To explain it's a lot easier. At the moment
@entryExit
is foundentryExit.__init__
is called and we save the function for later use. Whenfunc
is then called, it actually calls theentryExit
, which causesentryExit.__call__
to be called, which in turn callsfunc
, here calledself.function
.
Result
Entering func inside func Exited func
Known parameters
Functional
Since we just saw how decorators work without any parameters, it's time we check to see how one works where we know the parameters. It's pretty much the same example:
def entryExit(function): def new_function(someparam): print "Entering", function.__name__ function(someparam) print "Exited", function.__name__ return new_function @entryExit def func(param) print 'inside func, with param', param func('foo')
Not much different there, just the idea that the
entryExit
function doesn't take any extra parameters, but instead only thenew_function
does. This is functional programming, doesn't actually have anything to do with the decorator though.
OO
Once you understand the first example, this one is actually clearer than the functional one, since by now we should know to recognize that
entryExit.__call__
is called, it should be clear how to handle that:class entryExit: def __init__(self, function): self.function = function def __call__(self, someparam): print "Entering", self.function.__name__ self.function(someparam) print "Exited", self.function.__name__ @entryExit def func(param): print 'inside func, with param', param func('foo')
Since it's just
entryExit.__call__
that gets called in the end, it should be clear that any param that should be used by the function should be part of theentryExit.__call__
function.
Result
Entering func inside func, with param foo Exited func
Arbitrary arguments
Functional
Having to match your decorator function to the function you want to apply it to isn't very useful in any case where there might be functions that have different parameter lengths, and writing a decorator for each isn't all that much fun. Here's how we work around that:
def entryExit(function): def new_function(*args, **kwargs): print "Entering", function.__name__ function(*args, **kwargs) print "Exited", function.__name__ return new_function @entryExit def func(): print 'inside func' @entryExit def func2(arg): print 'inside func2, with arg', arg @entryExit def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('foo') print func3('bar', 'baz')
This gets a little stranger, with the
*arg
and**kwargs
, but that is one of the reasons Python is so dynamic.
OO
From known parameters to arbitrary parameters is again a rather small step for this method:
class entryExit: def __init__(self, function): self.function = function def __call__(self, *args, **kwargs): print "Entering", self.function.__name__ self.function(*args, **kwargs) print "Exited", self.function.__name__ @entryExit def func(): print 'inside func' @entryExit def func2(arg): print 'inside func2, with arg', arg @entryExit def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('foo') print func3('bar', 'baz')
The only thing, just as with the functional example, is the
*args
and**kwargs
parts, if you understand that then this is pretty much the same as when you know which variables are needed.
Result
Entering func inside func Exited func Entering func2 inside func2, with arg foo Exited func2 Entering func3 inside func3, with args bar and baz Exited func3
Mixing
Fixed and arbitrary arguments
Functional
There's just one idea left, mixing known, or required, and arbitrary arguments. It's not difficult to extrapolate what needs to be done from the information we have so far:
def entryExit(function): def new_function(arg, *args, **kwargs): print "Entering", function.__name__, "with arg", arg function(arg, *args, **kwargs) print "Exited", function.__name__ return new_function @entryExit def func(arg): print 'inside func, with arg', arg @entryExit def func2(arg1, arg2): print 'inside func2, with args', arg1, 'and', arg2 @entryExit def func3(arg1, arg2, arg3): print 'inside func3, with args', arg1, ',', arg2, 'and', arg3 func('foo') print func2('bar', 'baz') print func3('baz', 'foo', 'bar')
From what we know about both fixed and arbitrary args, this wasn't very difficult to figure out, but this does offer other possibilities as well.
OO
Well this shouldn't be very hard, it's almost downright logical.
class entryExit: def __init__(self, function): self.function = function def __call__(self, arg, *args, **kwargs): print "Entering", self.function.__name__, 'with arg', arg self.function(arg, *args, **kwargs) print "Exited", self.function.__name__ @entryExit def func(arg): print 'inside func, with arg', arg @entryExit def func2(arg1, arg2): print 'inside func2, with args', arg1, 'and', arg2 @entryExit def func3(arg1, arg2, arg3): print 'inside func3, with args', arg1, ',', arg2, 'and', arg3 func('foo') print func2('bar', 'baz') print func3('baz', 'foo', 'bar')
As long as
entryExit.__call__
can be called with the same arguments as the decorated function, it really doens't matter how many required and how many optional arguments a decorator gets, but if it needs at least one and none are given, then yes, that will be problematic.
Result
Entering func with arg foo inside func, with arg foo Exited func Entering func2 with arg bar inside func2, with args bar and baz Exited func2 Entering func3 with arg baz inside func3, with args bar , foo and baz Exited func3
Hiding arguments
Functional
Though it can make your code very difficult to understand or even maintain if used unwisely:
def entryExit(function): def new_function(*args, **kwargs): print "Entering", function.__name__ function('foo', *args, **kwargs) print "Exited", function.__name__ return new_function @entryExit def func(arg): print 'inside func, with arg', arg @entryExit def func2(arg1, arg2): print 'inside func2, with args', arg1, 'and', arg2 @entryExit def func3(arg1, arg2, arg3): print 'inside func3, with args', arg1, ',', arg2, 'and' arg3 func() print func2('foo') print func3('bar', 'baz')
This way a decorator can 'eat away' arguments from a function call. Of course this means that when you look at the code later you see the function takes 1 argument, but you don't see it being given to the function.
OO
It can get very messy with all the strange ways to apply them.
class entryExit: def __init__(self, function): self.function = function def __call__(self, *args, **kwargs): print "Entering", self.function.__name__ self.function('foo', *args, **kwargs) print "Exited", self.function.__name__ @entryExit def func(arg): print 'inside func, with arg', arg @entryExit def func2(arg1, arg2): print 'inside func2, with args', arg1, 'and', arg2 @entryExit def func3(arg1, arg2, arg3): print 'inside func3, with args', arg1, ',', arg2, 'and', arg3 func() print func2('foo') print func3('bar', 'baz')
I believe that code could get very messy very quickly this way.
Result
Entering func inside func with arg foo Exited func Entering func2 inside func2 with args foo and foo Exited func Entering func3 inside func3 with args foo , bar and baz Exited func
Adding arguments
Functional
Another possibility would be:
def entryExit(function): def new_function(arg, *args, **kwargs): print "Entering", function.__name__, 'with argument', arg function(*args, **kwargs) print "Exited", function.__name__ return new_function @entryExit def func(): print 'inside func' @entryExit def func2(arg): print 'inside func2 with arg', arg @entryExit def func3(arg1, arg2): print 'inside func3 with args', arg1, 'and', arg2 func('foo') print func2('bar', 'baz') print func3('baz', 'foo', 'bar')
This does pretty much the opposite of the previous example, this adds an argument to the function. Again you see a difference between invocation and declaration, which can get very confusing, I'm sure.
OO
class entryExit: def __init__(self, function): self.function = function def __call__(self, arg, *args, **kwargs): print "Entering", self.function.__name__, 'with argument', arg self.function(*args, **kwargs) print "Exited", self.function.__name__ @entryExit def func(): print 'inside func' @entryExit def func2(arg): print 'inside func2, with arg', arg @entryExit def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func('foo') print func2('bar', 'baz') print func3('baz', 'foo', 'bar')
Result
Entering func with argument foo inside func Exited func Entering func2 with argument bar inside func2, with arg baz Exited func2 Entering func3 with argument baz inside func3, with args foo and bar Exited func3
Parameters to the decorator
Functional
The decorator itself could also get/use some parameters, this adds another level of functional programming and gets even stranger to look at:
def entryExit(arg): def entry_exit_function(function): def new_function(*args, **kwargs): print "Entering", function.__name__, 'with argument', arg function(*args, **kwargs) print "Exited", function.__name__ return new_function return entry_exit_function @entryExit('foo') def func(): print 'inside func' @entryExit('bar') def func2(arg): print 'inside func2, with arg', arg @entryExit('baz') def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('baz') print func3('foo', 'bar')
OO
class entryExit: def __init__(self, arg): self.arg = arg def __call__(self, function): def internal_call(*args, **kwargs): print "Entering", function.__name__, 'with argument', self.arg function(*args, **kwargs) print "Exited", function.__name__ return internal_call @entryExit('foo') def func(): print 'inside func' @entryExit('bar') def func2(arg): print 'inside func2, with arg', arg @entryExit('baz') def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('baz') print func3('foo', 'bar')
Result
Entering func with argument foo inside func Exited func Entering func2 with argument bar inside func2, with arg baz Exited func2 Entering func3 with argument baz inside func3, with args foo and bar Exited func3
This way the extra parameter(s) can be added per function definition, and they stay the same over each call of that function.
Multiple decorators
Functional
Of course, a function can have more than one decorator:
def entry(function): def new_function(*args, **kwargs): print "Entering" function(*args, **kwargs) return new_function def exit(function): def new_function(*args, **kwargs): function(*args, **kwargs) print "Exited", function.__name__ return new_function @exit def func(): print 'inside func' @entry def func2(arg): print 'inside func2, with arg', arg @entry @exit def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('baz') print func3('foo', 'bar')
OO
class entry: def __init__(self, function): self.function = function def __call__(self, *args, **kwargs): print "Entering" self.function(*args, **kwargs) class exit: def __init__(self, function): self.function = function def __call__(self, *args, **kwargs): self.function(*args, **kwargs) print "Exited", self.function.__name__ @exit def func(): print 'inside func' @entry def func2(arg): print 'inside func2, with arg', arg @entry @exit def func3(arg1, arg2): print 'inside func3, with args', arg1, 'and', arg2 func() print func2('baz') print func3('foo', 'bar')
Result
inside func Exited func Entering inside func2, with arg baz Entering inside func3, with args foo and bar Exited func3
As you can see, the
__name__
does change to the new function that we added.
Sources
What I've learned today and what I've written here has all been made possible thanks to the following sources: