Comments on Peculiar Self-References

Post Comment

XG15 said:

This is an interesting example that the internal models of two programming languages can be quite different, even if they ostensibly support the same features.

a = a['k'] = {} is also valid JavaScript syntax, however running it throws an exception and generally works as OP probably expected the statement to work. The reason is that in JS, the statement is parsed as

a = (a['k'] = {})

where (a['k'] = {}) is executed first and also returns the assigned value. Meanwhile in Python, apparently multi-assignment is a grammar production of its own, so the above would be parsed as sort of:

multi-assign(a, a['k']) = {}

and therefore first assign to a, then to a['k'].

12 Nov 2021 21:07 GMT (#1 of 21 comments)

JWMH said:

In my opinion, it would make more sense to just treat assignments as expressions, as is done in C-like languages. Maybe I'm just more accustomed to C-like languages, though.

I think that one thing that makes the statement confusing is that the first equals sign serves a purpose completely different from the second equals sign, but both of them are represented by the same character.

12 Nov 2021 22:57 GMT (#2 of 21 comments)

Oshiar said:

XG15, Yet another reason is all the references in LHS expressions are resolved before the inner expression is executed. Thus

a['k'] = a = {}

won't still work.

13 Nov 2021 03:03 GMT (#3 of 21 comments)

o11c said:

Yeah, Python really messed up the order of multiple assignments. This could be fixed by deprecating arbitrary multiple assignments. Instead, only allow:

Use of "or" rather than "and" is deliberate, to fix an obscure edge case. We can't even limit the restriction to "assignments involving a function call". Notably, self.a = a = {} is not always the same as a = self.a = {}. Proof:

#!/usr/bin/env python3

class Foo:
    def __init__(self):
        self.get_a = lambda: a
        a = 1
        a = self.a = 2
        a = 3
        self.a = a = 4

    def __setattr__(self, name, val):
        if name != 'get_a':
            print(self.get_a())
        self.__dict__[name] = val

if __name__ == '__main__':
    Foo()
13 Nov 2021 16:35 GMT (#4 of 21 comments)

Susam Pal said:

For those who are wondering what the code in the previous comment does, here is its output:

2
3

When a = self.a = 2 is evaluated, the assignment to self.a triggers the __setattr__ call which tells us that the __init__ method's local variable a has already been updated to 2. That's the first line of output.

When self.a = a = 4 is evaluated, once again the assignment to self.a triggers the __setattr__ call which tells us that the value of local a is still 3. It has not been updated to 4 yet. It gets updated to 4 after the assignment to self.a completes.

Both lines of output demonstrate the left-to-right assignment quite clearly.

13 Nov 2021 20:13 GMT (#5 of 21 comments)

Kaushik Ghose said:

This is very neat, but I would like to approach it a bit differently. Python allows self-referential objects and is able to handle this gracefully while printing them.

>>> a = {}
>>> a['self'] = a
>>> a
{'self': {...}}
>>> a['self']
{'self': {...}}
>>> a['self']['self']
{'self': {...}}

Python also allows chained assignments.

>>> a = b = c = d = 23.4567
>>> a
23.4567
>>> b
23.4567
>>> c
23.4567
>>> d
23.4567

Also, a dictionary has to be initialized before a key access.

>>> a['k'] = {}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

So what the following example teaches us is how Python goes about allocating things in a chained assignment.

>>> a = a['k'] = {}
>>> a
{'k': {...}}

Let us look at something else with recursion which is easier for me to illustrate

>>> a = a[0] = [0, 1]
>>> a
[[...], 1]
>>> a = a[1] = [0, 1]
>>> a
[0, [...]]

The operations look like following:

[0, 1] → a
a → a[0]

If we extend the chain (a = b = c = d = ...) what is the pattern?

I could not think of a test that would let me dissociate if what is happening is

a = x
b = a
c = a
d = a
or
a = x
b = a
c = b
d = c

I might think of something tomorrow.

14 Nov 2021 02:31 GMT (#6 of 21 comments)

Carl M. Johnson said:

Every Python object has an infinitely recursive set of attributes because methods are functions and functions are objects and all objects have methods. It is a fun way to hit a stack overflow chasing them down.

14 Nov 2021 03:52 GMT (#7 of 21 comments)

Susam Pal said:

Carl, Yes, indeed!

None.__eq__.__eq__.__eq__.__eq__.__eq__ # and so on ...
14 Nov 2021 07:02 GMT (#8 of 21 comments)

Blind Mathematician said:

This looks like a big oops in language design. Time to upgrade to Python4 for a fix!

18 Nov 2021 13:36 GMT (#9 of 21 comments)

Knome said:

Wow. That is surprising. It makes sense but strikes me as the exact opposite of what I would expect looking at the code. I would definitely have expected the binding order to be the more common right-to-left and for the original code to generate a name error, rather than the latter.

18 Nov 2021 13:36 GMT (#10 of 21 comments)

Knome said:

a = a[len(a)] = a[len(a)] = a[len(a)] = a[len(a)] = a[len(a)] = a[len(a)] = {}

This is knowledge that should have remained hidden.

18 Nov 2021 13:41 GMT (#11 of 21 comments)

Ayra said:

aaaaaaaaaaaaaaaa

18 Nov 2021 13:44 GMT (#12 of 21 comments)

Shellac said:

Knome: ... It makes sense ...

Knome,

I think you are overstating that. It is not outright crazy or inconsistent, but I quibble with it being sensible.

18 Nov 2021 14:06 GMT (#13 of 21 comments)

Piu Tevon said:

The thing I learned today is that multiple assignments are evaluated left-to-right. I assumed it would be the other way around. Like a[0] = a = [0] would have been a lot less surprising.

18 Nov 2021 17:36 GMT (#14 of 21 comments)

Anitil said:

I am amazed at how long I have been working in Python and never known that assignment works in this way. I suppose I just avoid parts of the language that I do not understand.

Looking through the archives, it is a really interesting blog in general.

18 Nov 2021 22:06 GMT (#15 of 21 comments)

Kache said:

I think it would be elegant to let assignments be expressions, and from that simple core rule, your parsing naturally follows.

I find it irksome that Python makes a distinction between statements and expressions, instead.

18 Nov 2021 22:24 GMT (#16 of 21 comments)

Oliver said:

It is a fallout of the fact that assignment must be a statement, and cannot be an expression. For example, this is wrong in Python, while it would be allowed in C-like languages:

print(a = 5)
18 Nov 2021 23:54 GMT (#17 of 21 comments)

Tom Forbes said:

Oliver, It is possible now with the walrus operator though:

print(a := 5)
18 Nov 2021 23:57 GMT (#18 of 21 comments)

Heavyset Go said:

Some languages wear the things that make you go WTF on their sleeves. In contrast, Python's surprises pop up slowly over time, and in places where you'd least expect them.

19 Nov 2021 01:20 GMT (#19 of 21 comments)

Solar Fields said:

Heavyset, I wonder if it's because of their subtlety. I assume you are referencing JavaScript, where the warts are impossible to ignore because you can't write an everyday program as a beginner without running into them. This sort of thing from the article is just not something I would typically be trying to do in the normal course of shuffling data around.

19 Nov 2021 05:22 GMT (#20 of 21 comments)

Malf said:

This is the first true "PHPism" I have seen in a "real language". (Is there a name for "we did not want the general thing (e.g., assignment expressions) but we wanted one narrow subset of it (serial assignment), so we implemented that, but with different semantics for no good reason"?)

20 Nov 2021 22:41 GMT (#21 of 21 comments)
Post Comment