[Tutor] Writing decorators?
Steven D'Aprano
steve at pearwood.info
Tue Jul 5 11:26:34 EDT 2016
On Tue, Jul 05, 2016 at 09:22:52AM -0400, Alex Hall wrote:
> Hi list,
> I've read three or four articles on creating decorators. I don't know why,
> but the use of decorators makes sense, yet the creation of them isn't
> clicking. I get the idea, but when it comes to precisely how to write
> one--what goes where and gets returned/called when--it's just not making
> complete sense.
*Technically* a decorator can return anything at all, but the most
common use for them is to return a function. In this case, the decorator
is a function that:
(1) Takes as input a single function;
(2) Processes or wraps that function in some way; and
(3) Returns the original function, or the wrapped function, as output.
Back before Python had the "@decorator" syntax, we used to use
decorators like this:
def func(arg):
...
func = decorator(func)
which has the disadvantage that we have to write the function name three
times, and that the call to the decorator is kinda lost there at the end
instead of near the function declaration, but it does have one
advantage: it makes it clear that "func" gets set to whatever
decorator() returns.
Which may be None if you're not careful.
> To simplify things, what might be an example of a decorator that, say,
> prints "decorated" before whatever string the decorated function prints?
import functools
def prependDecorated(function):
"""Wrap function so that it prints "decorated" before running."""
# Here we define a new function that prints "decorated",
# then calls the original function.
#
# We use functools.wraps as a bit of magic to make sure our new
# function looks like the original (same name, docstring, etc).
#
@functools.wraps(function)
def inner(*args, **kwargs):
print("Decorated!")
return function(*args, **kwargs)
#
# Now that we have our new "inner" function that calls the old
# one, we have to return that inner function.
return inner
@prependDecorated # NO PARENTHESES!!!
def lunch():
print("What's for lunch?")
print("How about some wonderful SPAM!!!")
That's what the great bulk of decorators look like:
(1) declare a function that takes one function as argument;
(2) define an inner function (usually called "inner");
(3) it will often take arbitrary *args, **kwargs parameters, since you
don't normally know what arguments the WRAPPED (decorated) function will
take;
(3) use functools.wraps to disguise the inner function as the original
function (this is important so that it keeps the docstring, the name,
any other attached attributes etc. that the original had);
(4) inside the "inner" function, do your pre-processing, then call the
original function, do any post-processing, and then return the result;
(5) finally return the "inner" function.
Here's a decorator that makes sure the decorated function returns a
string:
def stringify(func):
@functools.wraps(func)
def inner(*args, **kwargs):
result = func(*args, **kwargs)
return "stringified result: " + str(result)
return inner
@stringify
def add(a, b):
"""Return the stringified result of adding a and b."""
return a+b
Here's a decorator that makes sure the first argument is greater than
zero:
def verify_arg0(func):
@functools.wraps(func)
def inner(x, *args, **kwargs):
if x <= 0:
raise ValueError('x must be greater than zero')
return func(x, *args, **kwargs)
return inner
Here's a neat trick: here's a decorator that doesn't actually modify the
function, but registers its name somewhere!
THE_REGISTER = []
def register(func):
THE_REGISTER.append(func.__name__)
return func
@register
def plus(a, b): return a + b
@register
def minus(a, b): return a - b
> That is:
>
> @prependDecorated()
> def printSomething(str):
> print str
>
> printSomething("Hello world.")
> #result should be:
> decoratedHello world.
This one is trickier than you think, because it requires knowledge of
what the original function will do. It *requires* that the original
function MUST call print at least once, otherwise you'll have started to
print something, but not completed the line. That may mess up your
python prompt (in the interactive interpreter) or any other output you
print. But here we go:
def prepend_actual_print(func):
@functools.wraps(func)
def inner(*args, **kwargs):
## print("decorated", end='') # Python 3 syntax
sys.stdout.write("decorated") # Python 2
return func(*args, **kwargs)
return inner
Hope this helps!
--
Steve
More information about the Tutor
mailing list