Function decorator having arguments is complicated

Chris Angelico rosuav at gmail.com
Mon Apr 27 05:20:32 CEST 2015


On Mon, Apr 27, 2015 at 12:37 PM, Makoto Kuwata <kwa at kuwata-lab.com> wrote:
> If function decorator notation could take arguments,
> decorator definition would be more simple:
>
>   def multiply(func, n):
>     def newfunc(*args, **kwargs):
>       return n * func(*args, **kwargs)
>     return newfunc
>
>   @multiply 4      # ex: @decorator arg1, arg2, arg3
>   def f1(x, y):
>     return x+y
>

I agree it would be nice to have extra parameters directly handled,
but before you go further with the proposal, I suggest having a read
of the original PEP that introduced decorators:

https://www.python.org/dev/peps/pep-0318/

There are a *lot* of different ways that decorators could have been
done. Currently, decorators use a restricted subset of expression
syntax; the bit after the @ is guaranteed [1] to be a valid expression
which results in a callable which is passed one argument (the
function) and whose return value replaces the function. What you
propose would no longer be a valid expression, and may introduce
parsing problems for the interpreter and/or for humans.

But if the writing of decorators is hard and common for you, you can
always use a bit of metaprogramming to simplify it. (Since decorators
are already metaprogramming, this would be metametaprogramming, I
guess.) Something like:

import functools

def decorator(deco):
    """Make a function into an argument-accepting decorator"""
    @functools.wraps(deco)
    def outer(*a, **kw):
        def inner(func):
            return deco(func, *a, **kw)
        return inner
    return outer

@decorator
def multiply(func, n):
    @functools.wraps(func)
    def newfunc(*a, **kw): return n * func(*a, **kw)
    return newfunc

@multiply(4)
def f1(x, y):
    return x+y

print(f1(2, 3))   #=> 20  (= 4 * (2+3))

(Note that I'm using functools.wraps here so docstrings and stuff get
carried over)

And if you're writing a lot of functions that call the original and
then do something with the result, you could push that part into the
decorator too:

import functools

def modifier(deco):
    """Make a function into an argument-accepting decorator"""
    @functools.wraps(deco)
    def outer(*a1, **kw1):
        def middle(func):
            def inner(*a2, **kw2):
                return deco(func(*a2, **kw2), *a1, **kw1)
            return inner
        return middle
    return outer

@modifier
def multiply(result, n):
    return n * result

@multiply(4)
def f1(x, y):
    return x+y

print(f1(2, 3))   #=> 20  (= 4 * (2+3))

Python lets you be as insane as you like :) There are a lot of places
in Python code (actually, in code generally) where usage is clean and
implementation is horrific, and this can be one of them. The
modifier() function's implemenation is deeply nested, but it's
beautifully easy to use. A namedtuple is really convenient to use
(just subscript it or use attribute lookup!), creating a new
namedtuple type is reasonably alright, but the implementation of
namedtuple() itself is scary. Nested and complicated, I got no problem
with, long as she does it quiet-like!

Can you do everything you need with a metadecorator? Either way, it's
worth working on as part of a plan for parameterized decorators.
Either you can say "here's this new syntax, and it's equivalent to
this massive block of code" (look at the definition of "yield from" in
PEP 380 - sure, you *can* manage without it, but getting the edge
cases right is messy), or you can say "here's this new syntax, and it
allows us to do this, this, and this, which otherwise wouldn't be
possible".

Given how common parameterized decorators are, it's likely there'll be
some support for at least the broad concept of the proposal.

ChrisA

[1] At least, I'm pretty sure it is; I may be wrong.



More information about the Python-list mailing list