[Python-ideas] Augmented assignment syntax for objects.

Chris Angelico rosuav at gmail.com
Fri Apr 28 03:23:59 EDT 2017


On Fri, Apr 28, 2017 at 4:57 PM, Steven D'Aprano <steve at 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


More information about the Python-ideas mailing list