On Tue, May 25, 2021 at 04:01:35AM -0000, Joren Hammudoglu wrote:
> > Flat is better than nested.
> 
> I remember being confused a lot when I was learning how to write my 
> first decorator.

Imagine how confused you would be if they had included multiple 
parameter lists. Not only would have needed to learn:

* functions as first class values
* function decorators
* decorator syntax
* factory functions

but in addition:

* what multiple parameter lists mean
* something that looks like a non-nested function actually being
  implicitly nested.


That's fifty percent increase in complexity.

I keep coming across not just beginners, but even experienced Python 
programmers who don't get decorators, and sometimes even ask "why do we 
need them?". I'm not sure that making decorators even more complex is 
going to make things easier for them.


> Now eight years later, with the last two spent 
> full-time writing Python, I introduced a bug in production because I 
> forgot to return the inner function from within a decorator.

Ouch! I feel your pain. But honestly, "I don't test my changes before 
pushing them out onto production systems" is not going to be solved by 
more syntax.


> By introducing multiple parameter lists when defining a function, 
> Scala does a great job at improving readability for curried functions 
> (https://docs.scala-lang.org/tour/multiple-parameter-lists.html). 

Well, that's one interpretation. It's certainly more *compact* and 
*terse*, and saves some boilerplate, but I'm not sure that it's more 
readable. IMNSHO it's got all the readability advantages of jargon 
initialisms over regular words in sentences. *wink*


> In Python, that could look like e.g.:
> 
> def log(level=logging.DEBUG, logger=logging.root)(func)(*args, **kwargs):
>     logger.log(level, 'call: %s', func.__qualname__)
>     return func(*args, **kwargs)
> 
> Which would be sugar for:
> 
> def log(level=logging.DEBUG, logger=logging.root):
>     def _log(func):
>         def __log(*args, **kwargs):
>             logger.log(level, 'call: %s', func.__qualname__)
>             return func(*args, **kwargs)
>         return __log
>     return _log
> 
> The obvious problem in this example, is that `functools.wraps` is 
> missing.

Indeed. The even bigger problem is that it is only narrowly applicable. 
In the most general case, nested functions can contain `2*N + 1` blocks 
of code, where N is the level of nesting:


    # two levels of nesting
    def factory(arg):
        block 1  # setup before making decorator
        def decorator(func):
            block 2  # setup before making inner function
            def inner(*args, **kw):
                block 3
            block 4  # post-process inner function
            return inner
        block 5 # post-process decorator
        return decorator

but your sugar only allows a maximum of one block no matter how deep 
the nesting goes:

    def decorator(arg)(func)(*args, **kw):
        block 3

I can't say that I've ever needed all five blocks for a two-level nested 
decorator, but I've commonly needed two or three, and occasionally four, 
of those blocks. Even ignoring functools.wraps, I don't think I've ever 
had a two-level decorator that was just a straight pass through of

    def outer():
        def middle(func):
            def inner():
                block 3
            return inner
        return middle

with no setup or post-processing at all. At least, no examples are 
coming easily to mind.

So I think that this sugar is only useful in a fairly narrow set of 
circumstances. And to make it usable, you need to add not just one but 
two meta-decorators, so we can use wraps in the right place:

> One solution would be for me to flex my 8 years of 
> experience, and present these two (super/meta-)decorators:

As a beginner to decorators, I found functools.wraps hard to wrap my 
head around (pun intended). If I had to cope with your 
wraps_decorator_factory instead, I probably would have just decided that 
the whole decorator concept was an example of Architecture Astronauts 
Gone Wild.

You're basically saying that your sugar, far from making it easier to 
write decorators that do what we want, makes it *harder*, so we need a 
decorator-decorator-decorator to give use the functionality back that we 
lost by using your sugar.

And then, after all that, it's still not going to prevent you from 
accidentally pushing out a buggy decorator into a production system 
without testing it thoroughly first :-(

Going back to your syntax, it's not clear where docstring belong to:

    def decorate(spam=5, eggs=1)(func)(*args, **kwargs):
        """I'm a docstring. But whose docstring am I???"""
        block


If it's treated as a docstring for the inner function it will be lost 
if you use wraps, so we might not want to do that. But using wraps is 
not mandatory -- if you don't use it, you might want the docstring to be 
attached to the inner function.

On the third hand, if we attach it to the inner function, there's no 
easy way to document the outer factory functio.

And on the fourth hand(!), if the docstring is attached to the outer 
factor function, as the indentation suggests, that adds yet another 
oddity to learn. Both the docstring and the block are physically 
indented one level, but one is conceptually indented three levels in to 
the implicit inner function, and the other isn't.


-- 
Steve
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/[email protected]/message/DBJWOSRV2MIZEUTSBMSIC5ZGVGTQN4YH/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to