
On Fri, Apr 28, 2017 at 4:57 PM, Steven D'Aprano <steve@pearwood.info> wrote:
On Fri, Apr 28, 2017 at 03:30:29PM +1000, Chris Angelico wrote:
Obviously we can define syntax to do anything we like, but what is the logical connection between the syntax and the semantics? What part of "function parameter list" suggests "assign attributes to arbitrary objects"? [...] What part of a 'for' loop suggests that you can do this?
I'm not sure what "this" is supposed to do. You've written some obscure but perfectly legal Python code:
odds = [0]*10; evens = [0]*10 for idx, (odds if idx%2 else evens)[idx//2] in stuff: ...
My guess is that "this" refers to the side-effect that assigning to a list item updates the list item. Um, yeah, it does. What's your point? Of course it does. That's what its supposed to do.
Perhaps you think that there's something weird about using an arbitrary assignment target as the for-loop. I don't know why you think that's weird. Here's a simpler example:
for obj.attr in seq: ...
Yes, its a bit unusual to do that, but its not weird. The assignment target for a loop is just an ordinary assignment target, and the assignment occurs during the execution of code, just like any other assignment that occurs inside the body of the function.
What is weird is to have the function *declaration* have global side effects and perform assignments outside of the function. The parameter list is *not* a general assignment statement, it is a declaration of what local variables will be assigned to when you call the function.
Waaaaaait a minute. Since when is the *declaration* doing this? No no no. In every proposal, it's been a part of the function's *execution*. The original example was an __init__ function that does the exact same thing as my for loop example: it first sets "self" to the first parameter, and then sets some attributes on self. As you say:
Um, yeah, it does. What's your point? Of course it does. That's what its supposed to do.
There is nothing surprising in it (although my example was deliberately convoluted and a bad example of code). It's assignment, pure and simple. When you call a function, the values you pass to it get assigned to the names provided in the declaration. No assignments happen at declaration other than the function's own name. (I suppose you could say that default argument values are "assigned" at that point, but I prefer to say "collected", since they're basically assembled into a tuple and slapped into an attribute on the function.)
Nothing whatsoever says that this is a good idea, but it's perfectly legal, because the for loop is defined in terms of assignment. If this were to be accepted (which, fwiw, I'm not actually advocating, but IF), it would also be defined in terms of assignment.
Why should it be defined in terms of general assignment? That's the point I'm making. While function sigs are a form of assignment, they're more of a declaration than an executable statement like the other binding statements. There's a superficial connection, but they really are quite different things.
There's a lot more similarities than you might think. For example, both of these will create "spam" as a local name, shadowing the global: spam = 1 def func1(spam): print(spam) def func2(): spam = 2 print(spam) As will many other forms of assignment, like "import spam" or "with x as spam:". Some forms are more restricted than others ("import" requires a NAME, not an arbitrary expression), but they all behave the same way with regard to global/nonlocal/local naming - you can "global numpy; import numpy" inside a function to do a delayed import, for instance.
(For example: if you allow spam.foo as a parameter, that can call arbitrary Python code in spam.__setattr__, which assigning to foo as a parameter will not do.)
Sure, but that's a difference between simple name assignment and item/attribute assignment, not a difference between function parameters and other types of variables. Are you next going to point out that you can't "import spam as foo[0]"? Or that importing is somehow special because of this? No. It's still assignment, just one with a restricted syntax.
You still shouldn't be assigning to arbitrary objects, especially not randomly rebinding module names, but it's easy to grok the assignment equivalence.
You need to speak to more beginners if you think the connection between spam.x and x is obvious:
def func(spam.x): print(x)
Where is x declared? It looks like there's a local spam.x which isn't used, and a global x that is. But that's completely wrong. Without the context of somebody telling you "this is syntax for magically assigning to self.attributes in the constructor", I believe this will be unfathomable to the average non-expert.
Not sure what you mean. By my reading, that's exactly correct - there IS a global x that is being used here, although "local spam.x" is not accurate. I wouldn't call that "completely wrong". And if you find that confusing, let's look at importing. os = "Linux" path = "/tmp" def func(): import os.path print(path) What's local and what's global? What name is actually bound by "import os.path"? What if you said "import os.path as path"? "import os.path as os"? Does all this confuse people? IMO it's *less* confusing if you can simply say "this is equivalent to <parameter> = <argument>".
And yes, it WOULD reinstate the argument unpacking removed by PEP 3113. So for this to go forward, the objections in that PEP would have to be addressed.
Nicely remembered :-)
I didn't remember the PEP number, but the Google is helpful :) ChrisA