[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