On Thu, May 27, 2021 at 08:06:22PM -0700, Christopher Barker wrote:
Well, there is thread safe, and there is thread dangerous. I have an enormous amount of code that is not strictly thread safe, but works fine when run under a multi-threaded web server because there are no cases where the same instances of objects are running in different threads.
Cool. The easiest way to write safe concurrent code is to avoid shared mutable data.
Well, not true, there are many shared function objects (and class objects). I very much assume that those function objects are not changing at run time. But having static variables would totally break that assumption.
That assumption is already broken, and has been forever. Functions already have potential shared state. Static variables would not add any risk that isn't already there. Functions can already use globals, or mutable defaults, or function attributes. If any of them are written in C, they can already use static variables.
That would be like mutating class attributes, which, of course, is perfectly possible, but a little harder to do without thinking about it.
self.items.append(x) Is that a class attribute or instance attribute being modified? [...]
Exactly— and functions are always shared if you are using threads at all.
But not mutable if used in the usual way.
They are mutable, always. What you mean to say is that they aren't mutated, because nobody actually mutates them. They could, but they don't. And that won't change. [...]
Mutable default values are a notable “gotcha”.
They're a gotcha for people who expect that function defaults are evaluated on each function call. Just like assignment is a notable gotcha for people who expect that assignment copies: a = [1, 2, 3] b = a b.append(5) # why does `a` magically change when I modified the copy??? For those who understand this, it is a feature, not a bug, that defaults are eagerly evaluated once and binding shares references and doesn't copy.
classes with attributes, etc.
Yes, I think this is most like class attributes, which are probably less well known as a “gotcha”.
There are plenty of people who get confused why class attributes are shared between all instances. Especially for those whose only exposure to OOP is with Java: class MyClass: # this is a shared class attribute # not a per instance attribute items = [] https://stackoverflow.com/questions/1680528/how-to-avoid-having-class-data-s... https://stackoverflow.com/questions/11040438/class-variables-is-shared-acros... https://stackoverflow.com/questions/45284838/are-the-attributes-in-a-python-... https://www.geeksforgeeks.org/python-avoiding-class-data-shared-among-the-in... among others. This is a genuine gotcha that trips up beginners who haven't yet mastered Python's object model.
But also not that widely used by accident. I don’t think I’ve even seen a student use them. And I sure have seen mutable default values mistakenly used in students' code.
Oh come on Christopher, you can't have it both ways -- mutable default values are either a gotcha that trip beginners up, or beginners never use it. It can't be both.
Another point -- whether there is a static variable in the function becomes a not obvious part of its API. That's probably the biggest issue -- some library author used static, and all its users may not know that, and then use the code in a multi-threaded application, or frankly in a single threaded application that isn't expecting functions to be mutated.
"whether there is a **global variable** in the function becomes a not obvious part of its API. That's probably the biggest issue -- some library author used global, and all its users may not know that..." "whether there is a **mutable default** used for static storage in the function becomes a not obvious part of its API. That's probably the biggest issue -- some library author used the default value trick, and all its users may not know that..." "whether there is a **shared class (or instance) attribute** used in the function becomes a not obvious part of its API. That's probably the biggest issue -- some library author used shared class attributes, and all its users may not know that..." You've been a Python user long enough that you know that there are many ways for functions to store data that will persists from one call to another. We have globals, we have classes, we have generators, we have closures, we can write data to files and read it back, there are probably others I haven't thought of. None of them are *nice to use*, they are slow and awkward, or have scope problems (globals), hard to debug, expose things that shouldn't be exposed, etc. But they already do everything you are afraid static variables will do. Static variables won't be a problem is practice because all those other functionally equivalent ways are not problems in practice. If people need persistent data, they're already using it. This proposal just makes it less painful. And if the function *doesn't* need persistent data, then adding static variables isn't going to make people suddenly use it when there is no need for it. "Oh, I was going to write a pure function that calculates the number of seconds between two dates, but now that we have static I'm going to make it impure just to screw up threaded code, mwahahahaha!!!" This is a usability, and maybe performance, improvement over the status quo, not a fundamentally new idea that nobody has ever had before. [...]
And since the entire point of classes is to hold state and the functions that work with that state in one place, it's expected behaviour that that state changes.
And yet, in this very post, you said that you use shared classes in threaded code: "there are many shared function objects (and class objects)" It is perfectly safe to use mutable objects in concurrent code if you don't actually mutate them. You have been doing that for many years, and nothing will change for you if we get statics.
Which makes me realize why I never wanted a function static variable -- if I wanted changable state associated with a function, I used a class.
Okay. Let's compare a trivial counter function with a class. # Make a callable that counts the number of times it is called. class Counter: def __init__(self): self.count = 0 def __call__(self): self.count += 1 return self.count counter = Counter() Seven lines, two objects with two methods. Versus hypothetical: def counter(): static count = 0 count += 1 return count Four lines, only a single object. And with the new walrus operator: def counter(): static count = 0 return count := (count + 1) if saving one line is important *wink* There is nothing that either closures or generators can do that can't be done with a class. But we have both of those because for many cases they make it easier, more convenient and faster (faster to write, faster to read, faster to run) than a class.
And most commonly there was more than one function associated with that state, so a class was the rigth solution anyway.
Great. And that won't go away.
I know that Jack Diederich says: "if a class has only two functions, and one of them is __init__ -- it's not a class", and I totally agree with him, but if you do have a case where you have some state and only one function associated with it, maybe a class IS the right solution.
Only because we don't have static variables, and the other alternatives (globals etc) are maybe worse than a class. But Jack is still right: for something that needs only a single method (plus init) it probably shouldn't be a class. -- Steve