On Mon, May 25, 2020, 6:49 AM Rob Cliffe via Python-ideas < python-ideas@python.org> wrote:
(Possibly heretical) Thought: ISTM that when the decision was made that arg default values should be evaluated once, at function definition time, rather than every time the function is called and the default needs to be supplied that that was the *wrong* decision. There may have been what seemed good reasons for it at the time (can anyone point me to any relevant discussions, or is this too far back in the Python primeval soup?). But it is a constant surprise to newbies (and sometimes not-so-newbies). As is attested to by the number of web pages on this topic. (Many of them defend the status quo and explain that it's really quite logical - but why does the status quo *need* to be defended quite so vigorously?)
Not heretical so much as just half-baked. DISCLAIMER: I'm far from an expert. I'm not even a professional developer. But I think the idea that handling defaults this way was a mistake from the beginning is exactly wrong, and that this aspect of python doesn't need to be touched. I think that people who complain about this are missing the largeness of some fundamental problems that changing it would cause. And I think the set of decisions that would have to be made to implement it- no matter what those decisions they are- would lead to no end frustration, probably more than the current state of affairs. *First of all*: supplying a default object one time and having it start fresh at every call would *require copying the object*. But it is not clear what kind of copying of these default values should be done. The language doesn't inherently know how to arbitrarily make copies of every object; decisions have to be made to define what copying the object would MEAN in different contexts. Would it mean: *1.* copy.copy() applied to the object every function call? *2.* copy.deepcopy() applied every function call? The two options above would allow the object class to decide HOW the copying occurs (by relying on the default copy.copy or copy.deepcopy method, or providing __copy__ or __deepcopy__). But it would NOT allow the class to decide WHICH CHOICE is made-- copy, or deepcopy. No matter which is chosen, it will be problematic. deepcopy() will be desirable sometimes, copy() will be desirable other times. And other times, NEITHER will be desirable. copy/deepcopying doesn't even always actually make a copy, which will be unexpected for some: class C: ... copy(C) is C # True copy('xyz') is 'xyz' # True Some objects in the wild are simply not meant to be copied, but ignorant users WILL try to use them as copied defaults anyway ("ignorant" not used as a sleight; it is critical for being productive developer to maintain just the right level of ignorance so that I do not have to CARE about details of the things I am using). If a user tries to use such an object like so: from foo import bar def h(a:=bar()): ... ...it will require the creator of the foo library to maintain a __copy__ or __deepcopy__ method to provide an error message telling the user their object can't be used this way. And copying doesn't come for free; copying persistent objects, especially that take up a lot of memory at every function call, rather than freshly initializing them, could become extremely slow. Yes, this can be circumvented by simply not using the new feature. However, then you have to explain, in detail, how to know when to use the feature or not. *SIGH*. On top of this, Python already has a reputation for slowness; some of this is earned, some if it isn't and is just a result of people writing bad python code. If we made it EASY and actually encouraged people-- by providing a specific feature-- to get in the habit of writing code that slows things down considerably, this seems like a bad plan. Worse: it will create a situation where for every public-facing type, every developer of every library will have to stop for a minute and think "What will happen if the user tries to make my object work using the new := copy-at-call feature?" Allowing objects to be faithfully copied, OFTEN, is generally NOT a concern I worry about at all. I am not sure if pro developers do either, but I doubt it. *3.* The language does something more fancy like an eval() of the actual code text entered into the default parameter definition. If this 3rd one: do references inside of this definition get updated at the time of the call? Or do we create different behavior depending on whether the default value is a literal? Both could feel unexpected to the user depending on the case: # literal case def f(a:=[]): ... f() # a inside f is eval('[]') at call time; no problem here # non literal case _A = [] def g(a := _A): ... _A.append(1) g() What is the default of a inside g() here? eval ('_A'), which is [1]? Would not THAT be every bit as confusing to a newbie as the current situation? If we want the value of a in g() to be [1], an object does not be able to control how it is copied; the current copy/deepcopy machinery has to be totally circumvented. If we want the default value of a in g() to be [], we'll need some combination of *1* *or* *2* *and* *3*: an eval() of the initial value when the module runs, and then at the function call, a copy() or deepcopy() is applied to whatever was evaluated at the module import. It seems to me that this second version of *3* -- an eval followed by a copy() or deepcopy() -- makes the most sense and would be least surprising. However, now you still run into all of the same pitfalls that need to be solved in the case of *1* and *2*. *4.* Instead of copy() or deepcopy(), use the same basic copying algorithm contained therein, but with no calls to __copy__ and __deepcopy__; it is essentially a new kind of copying operation. This would make things a little faster. But this way of copying is another thing I have to learn if I want my user defined objects to behave in predictable and thoroughly understood ways. *Second of all*: no matter which way forward is chosen, it seems to be that this new syntax would have to slow things down, considerably in many cases. To summarize: We are already talking about making things easier to understand for the type of new coder that finds this issue confusing, but I don't think it is going to be THAT much easier to explain to the same coder that: A. there are two ways to provide default values for function parameters, = and := B. you can make sure you have a fresh new object every time by using the new shiny := syntax! C. ...except you often shouldn't use that because it will needlessly slow things down for large objects D. also, let me explain to you the difference between copy and deepcopy... hold on, yeah i know that's not what you asked about, just hear me out E. oh btw now that you understand about copy/deepcopy, many objects aren't copied even if you use the := --- Ricky. "I've never met a Kentucky man who wasn't either thinking about going home or actually going home." - Happy Chandler