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):
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"
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`).
Generators were also introduced in 2.2, explicitly as a way for functions to *hold state from one call to the next*:
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.)
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.
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.