On Fri, May 28, 2021 at 07:37:57PM -0700, Brendan Barnwell wrote:

>       I see your point, but I don't agree that static function variables 
>       are parallel to either closures or generators.

Okay, this is an important point, I think. I argue that some sort of 
sugar for static storage in functions is exactly analogous to closures 
and generators.

The history of Python demonstrates a pattern of using classes for 
complex data structures with state and *multiple* behaviours (methods), 
while using functions for the simple cases where you have only a single 
behaviour and a small amount of state, without the anti-pattern of 
global variables.

Closures, generators and coroutines are all ways of doing this.


>       Closures are, to my mind, just an outgrowth of Python's ability to 
> define functions inside other functions.  If you're going to allow such 
> nested functions, you have to have some well-defined behavior for 
> variables that are defined in the other function and used in the inner 
> one, and closures are just a reasonable way to do that.

When closures and lexical scoping were first introduced, they were 
intentionally and explicitly limited because classes could do everything 
that closures can. Here is the history of the feature.

Python didn't gain closures until version 2.1 (with a future import):

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

Before then, nested functions behaved quite differently. This is 
Python1.5:


    >>> x = "outside"
    >>> def function(): 
    ...         x = "inside"
    ...         def inner():
    ...                 print x
    ...         inner()
    ... 
    >>> function()
    outside


That's a simple, reasonable behaviour. Inner functions were allowed by 
the language but they didn't add much functionality and consequently 
were hardly ever used.

Lexical scoping changed that, and made inner functions much more useful. 
At the time people wanted a way to rebind nonlocal variables, but that 
was explicitly rejected because:

"this would encourage the use of local variables to hold state that is 
better stored in a class instance"

https://www.python.org/dev/peps/pep-0227/#id15

That comment has aged like milk. By the time Python 2.3 and 2.4 came 
along and people were talking about some future "Python 3000", it had 
already become clear that it would be useful to rebind nonlocal names 
and hold state encapsulated in a function without going to all the 
trouble of creating a class.

And so in Python 3 we gained the nonlocal keyword and the ability to 
store and modify state in a closure.

The proposed static statement would be sugar for functionality already 
possible: storing per function data in the function without needing to 
write a class.

Classes are great, but not everything is a nail that needs to be 
hammered with a class.


>       As for generators, they are tied to iteration, which is a 
> pre-existing concept in Python.  Generators provide a way to make your 
> own functions/objects that work in a `for` loop in a manner that 
> naturally extends the existing iteration behavior of lists, tuples, 
> etc.

We already had a way to create our own iterable values: classes using 
the sequence or iterator protocols.

Iteration using `__getitem__` and IndexError was possible all the way 
back to Python 1.x. The iterator protocol with `__iter__` and `__next__` 
wasn't introduced until 2.2 (and originally the second dunder was 
spelled `next`).

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

Generators were also introduced in 2.2, explicitly as a way for 
functions to *hold state from one call to the next*:

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

The motivation section of the PEP starts with this:

"When a producer function has a hard enough job that it requires 
maintaining state between values produced, most programming languages 
offer no pleasant and efficient solution ..."

and goes on to discuss alternatives such as functions with global state. 
(Global variables.) One alternative left out is to write a class, 
possibly because everyone acknowledged that writing a single function 
with its own state is so obviously superior to a class for solving this 
sort of problem that nobody bothered to list it as an alternative.

(You *can* spread butter on bread using a surf board, but why would you 
even try when you have a butterknife?)

The next step in the evolution of function-local state was to make 
generators two-way coroutines.

(Alas, the name "corountine" has been hijacked by async for a related 
but different concept, so there is some unavoidable terminology 
confusion here.)

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

Generators now have send and throw methods, even when you can't use 
them for anything useful!

    >>> g = (x+1 for x in range(10))
    >>> g.send
    <built-in method send of generator object at 0x7f38c8545b30>
    >>> g.throw
    <built-in method throw of generator object at 0x7f38c8545b30>

So that's yet another unobvious way to get static storage in a function: 
use a PEP 343 enhanced generator coroutine.

http://www.dabeaz.com/coroutines/index.html

The downside of this is that the body of the function has to use yield 
in an infinite loop, and the caller has to use a less familiar 
`func.send(arg)` syntax instead of using the familiar `func(arg)` 
syntax.

Again, there is nothing that coroutines or generators can do which 
cannot be done with classes. But people prefer generators, because 
having to write an entire class with multiple methods just to store a 
bit of state from one function call to the next sucks.

Here's an example of a coroutine that implements a simple counter:

    >>> def counter():
    ...     n = 0
    ...     while True:
    ...         n += 1
    ...         yield n
    ... 
    >>> func = counter()
    >>> func.send(None)
    1
    >>> func.send(None)
    2

Too much boilerplate to make this a compelling alternative, although 
with a bit of jiggery-pokery we can make it nicer:

    >>> from functools import partial
    >>> func = partial(func.send, None)
    >>> func()
    3
    >>> func()
    4

So there we go: a pocket tour of the history of per function state in 
Python. None of the alternatives are really smooth, and classes least of 
all. That's where this proposal for a static keyword comes into it: 
syntactic sugar for what we can and already do, to make it smoother and 
easier for functions to keep state alive from one call to the next.

Just like closures, generators and coroutines.


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

Reply via email to