Improved Function Decorators

At present, multi-argument function decorators are a little bit tricky to implement. As an example (somewhat contrived), suppose that `wait` is a function decorator which haults program execution for a few seconds before calling the wrapped function. If you do not pass a float value into *delay* then the default value is 1 second. #EXAMPLE 1A @wait def print_something(something): print (something) #EXAMPLE 2A @wait(0.2) def print_something_else(something): print (something) The `@wait` gets implemented almost like the following code: # EXAMPLE 1B def print_something(something): print (something) print_something = wait(print_something) # EXAMPLE 2B temp = wait(0.2) def print_something_else(something): print (something) print_something = temp(print_something) What is tricky is the following: * if `Wait` has no arguments: `Wait` is the decorator. else: # `Wait` receives arguments `Wait` is not the decorator itself. Instead, `Wait` ***returns*** the decorator* As a result, people write strange-looking (hard-to-read) decorators, like the following: def wait(func=None, delay=1.0): def decorator_wait(func): def wrapper_wait(*args, **kwargs): time.sleep(delay) return func(*args, **kwargs) return wrapper_wait return decorator_wait(func) if func is not None else decorator_wait This suggestion is that a new operator be introduced. The old @-syntax for decorators would remain unchanged, preserving backwards compatibility. It does not really matter to me what string is used for the operator, but I was thinking *$*, or if not that !@ with an interobang before the @. Our opening example of how it would work is shown below: # Example 3A $dec(1, 2, 3) def foo(): pass ########################################### # Example 3B def foo(): pass foo = dec(foo, 1, 2, 3) Examples 3A and 3B are meant to be equivalent. The new decorator operator would simply pass the decorated function into the decorator as the leftmost argument. @wait def slowly_print_something(something): print(something) #-------------------------------- def slowly_print_something(something): print(something) slowly_print_something = wait(slowly_print_something) ################################################## @wait(3.0) def slowly_print_something_else(something_else): print(something_else) #----------------------------------- def bar(something_else): print(something_else) slowly_print_something_else = wait(bar, 3.0) ###################################################### import time def wait(inny, delay=1.0): def outty(*args, **kwargs): time.sleep(delay) return inny(*args, **kwargs) return outty Maybe call them "dollar decorators," or if the !@ syntax is used, then maybe call them "bang decorators." I care more about the functionality than the exact strings of symbol which is used to signal the interpreter what's coming next.

On 18Nov2019 19:09, Samuel Muldoon <muldoonsamuel@gmail.com> wrote:
At present, multi-argument function decorators are a little bit tricky to implement.
Aye, but one can write a decorator for decorators which makes it easy. No extra syntax required. I've one of my own called @decorator, available on PyPI from the "cs.deco" module. Use: from cs.deco import decorator @decorator def wait(func, delay=1.0): def wrapped(*a, **kw): if delay > 0: time.sleep(delay) return func(*a, **kw) return wrapped and I can then use this as: @wait def print_something(something): print(something) @wait(delay=0.2) def print_something_else(something): print(something) as you desire. So any multiargument decorator I write such as the above I write as though it is unconditionally handed the function to wrap and the parameters of the decorator, and the @decorator decorator produces an outer decorator with the special logic you describe. I'm arguing here that (a) you could just grab my @decorator (and I'm sure there are other equivalent ones in PyPI) and (b) you don't need extra syntax. Of course, you could legitamtely argue that the above should be part of the protocol which @ implements, and that any sufficiently flexible decorator should expect a **kw. I'd support that. Again, no new syntax required, just better behaviour! Cheers, Cameron Simpson <cs@cskk.id.au>

On 2019-11-18 18:09, Samuel Muldoon wrote:
I would say this is a poor design. Just write "wait" so that you always have to call it, and you just pass no arguments if you want the default. So instead of this: # default delay @wait def foo(): # custom delay @wait(2) def foo(): . . . you do this: # default delay @wait() def foo(): # custom delay @wait(2) def foo(): -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

The only problem with this is the error handling. If you don’t check the type of the argument in wait, this isn’t an error: @wait def foo(spam): … and neither is calling the returned function with a single argument. If you call the value returned by _that_ function, you’ll get a TypeError, but you’re not going to do that. You’re just going to see that instead of waiting a second and doing some side effects, foo returns immediately having done nothing, and then have to debug that. Obviously this is the same bug as writing print on a line by itself instead of print(), or lambda dummy: frob instead of lambda dummy: frob(), etc. But the fact that so many decorators actually do make @foo and @foo() do the same thing raises the expectation that other decorators will do the same. (And because the error is two levels down, it’s harder to debug.) Maybe that means we need to put one of those nice helper tools that make it trivial to write a @foo that behaves that way into the stdlib. Or just mention how to do (and that helpers are readily available) in the docs on decorators. (I don’t think it means we need to add a whole new decorator syntax that just does that for you magically but is otherwise the same as the existing one.)

On 18Nov2019 19:09, Samuel Muldoon <muldoonsamuel@gmail.com> wrote:
At present, multi-argument function decorators are a little bit tricky to implement.
Aye, but one can write a decorator for decorators which makes it easy. No extra syntax required. I've one of my own called @decorator, available on PyPI from the "cs.deco" module. Use: from cs.deco import decorator @decorator def wait(func, delay=1.0): def wrapped(*a, **kw): if delay > 0: time.sleep(delay) return func(*a, **kw) return wrapped and I can then use this as: @wait def print_something(something): print(something) @wait(delay=0.2) def print_something_else(something): print(something) as you desire. So any multiargument decorator I write such as the above I write as though it is unconditionally handed the function to wrap and the parameters of the decorator, and the @decorator decorator produces an outer decorator with the special logic you describe. I'm arguing here that (a) you could just grab my @decorator (and I'm sure there are other equivalent ones in PyPI) and (b) you don't need extra syntax. Of course, you could legitamtely argue that the above should be part of the protocol which @ implements, and that any sufficiently flexible decorator should expect a **kw. I'd support that. Again, no new syntax required, just better behaviour! Cheers, Cameron Simpson <cs@cskk.id.au>

On 2019-11-18 18:09, Samuel Muldoon wrote:
I would say this is a poor design. Just write "wait" so that you always have to call it, and you just pass no arguments if you want the default. So instead of this: # default delay @wait def foo(): # custom delay @wait(2) def foo(): . . . you do this: # default delay @wait() def foo(): # custom delay @wait(2) def foo(): -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

The only problem with this is the error handling. If you don’t check the type of the argument in wait, this isn’t an error: @wait def foo(spam): … and neither is calling the returned function with a single argument. If you call the value returned by _that_ function, you’ll get a TypeError, but you’re not going to do that. You’re just going to see that instead of waiting a second and doing some side effects, foo returns immediately having done nothing, and then have to debug that. Obviously this is the same bug as writing print on a line by itself instead of print(), or lambda dummy: frob instead of lambda dummy: frob(), etc. But the fact that so many decorators actually do make @foo and @foo() do the same thing raises the expectation that other decorators will do the same. (And because the error is two levels down, it’s harder to debug.) Maybe that means we need to put one of those nice helper tools that make it trivial to write a @foo that behaves that way into the stdlib. Or just mention how to do (and that helpers are readily available) in the docs on decorators. (I don’t think it means we need to add a whole new decorator syntax that just does that for you magically but is otherwise the same as the existing one.)
participants (4)
-
Andrew Barnert
-
Brendan Barnwell
-
Cameron Simpson
-
Samuel Muldoon