Meta decorator with parameters, defined in explicit functions

Steven D'Aprano steve at pearwood.info
Wed Jun 29 23:49:53 EDT 2016


On Thu, 30 Jun 2016 12:43 pm, Lawrence D’Oliveiro wrote:

> On Tuesday, June 28, 2016 at 5:03:08 PM UTC+12, Ben Finney wrote:
>> There is a clever one-line decorator that has been copy-pasted without
>> explanation in many code bases for many years::
>> 
>>     decorator_with_args = lambda decorator: lambda *args, **kwargs:
>>     lambda func: decorator(func, *args, **kwargs)
>> 
>> My problem with this is precisely that it is clever: it explains nothing
>> about what it does, has many moving parts that are not named, it is
>> non-obvious and lacks expressiveness.
> 
> It is written in a somewhat roundabout way: why not just
> 
>     decorator_with_args = lambda decorator, *args, **kwargs : lambda func
>     : decorator(func, *args, **kwargs)
> 
> ? Could it be this was not valid in earlier versions of Python?

Your version has a much inferior API than the original. You can't wrap your
decorators ahead of time, you have to keep calling decorator_with_args each
time you want to use them. Contrast your version:


# LDO's version
import functools
decorator_with_args = (
        lambda decorator, *args, **kwargs : 
        lambda func : decorator(func, *args, **kwargs)
        )

def chatty(func, name, age, verbose=True):
    if verbose:
        print("decorating function...")
    @functools.wraps(func)
    def inner(*args, **kwargs):
        print("Hi, my name is %s and I am %d years old!" % (name, age))
        return func(*args, **kwargs)
    return inner

@decorator_with_args(chatty, "Bob", 99)
def calculate(x, y, z=1):
    return x+y-z

@decorator_with_args(chatty, "Sue", 35, False)
def spam(n):
    return ' '.join(['spam']*n)



Here's the original. It's a much better API, as the decorator "chatty" only
needs to be wrapped once, rather than repeatedly. For a library, it means
that now you can expose "chatty" as a public function, and keep
decorator_with_args as an internal detail, instead of needing to make them
both public:


# original meta decorator version

import functools
decorator_with_args = (
        lambda decorator: 
        lambda *args, **kwargs: 
        lambda func: decorator(func, *args, **kwargs)
        )

@decorator_with_args
def chatty(func, name, age, verbose=True):
    if verbose:
        print("decorating function...")
    @functools.wraps(func)
    def inner(*args, **kwargs):
        print("Hi, my name is %s and I am %d years old!" % (name, age))
        return func(*args, **kwargs)
    return inner

@chatty("Bob", 99)
def calculate(x, y, z=1):
    return x+y-z

@chatty("Sue", 35, False)
def spam(n):
    return ' '.join(['spam']*n)



Think of the use-case where you are the author of a library that provides
various decorators. `decorator_with_args` is an implementation detail of
your library: *you* say:

@decorator_with_args
def chatty(func, name, age, verbose=True): ...

@decorator_with_args
def logged(func, where_to_log): ...

@decorator_with_args
def memoise(func, cache_size): ...


and then offer chatty, logged and memoise as public functions to the users
of your library, who just write:

@memoise(1000)
def myfunc(arg): ...

etc. as needed. But with your version, you have to make decorator_with_args
a public part of the API, and require the user to write:

@decorator_with_args(memoise, 1000)
def myfunc(arg): ...


which I maintain is a much inferior API for the users of your library.



> I don’t know why this fear and suspicion of lambdas is so widespread among
> Python users ... former Java/C# programmers, perhaps?

Not so much fear as a little healthy respect for them, I think.




-- 
Steven
“Cheer up,” they said, “things could be worse.” So I cheered up, and sure
enough, things got worse.



More information about the Python-list mailing list