Peculiar Self-References

By Susam Pal on 21 Feb 2019

Peculiar Results

Here is a tiny Python example that creates a self-referential list and demonstrates the self-reference:

>>> a = a[0] = [0]
>>> a
[[...]]
>>> a[0]
[[...]]
>>> a[0][0]
[[...]]
>>> a is a[0]
True

The output shows that a[0] refers to a itself which makes it a self-referential list. Why does this simple code create a self-referential list? Should it not have failed with NameError because a is not yet defined while assigning the list [0] to a[0]?

Here is another similar example that creates a self-referential list too:

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

Here is a similar example for dictionary:

>>> a = a[0] = {}
>>> a
{0: {...}}

Note that 0 is used as a dictionary key in the above example. Here is another very simple example that uses a string key:

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

The Language Reference

My first guess was that the statement

a = a[0] = [0]

behaves like

new = [0]
a = new
a[0] = new

which would indeed create a self-referential list.

Section 7.2 (Assignment statements) of The Python Language Reference confirms this behaviour. Quoting the relevant part from this section here:

Assignment statements are used to (re)bind names to values and to modify attributes or items of mutable objects:

assignment_stmt ::=  (target_list "=")+ (starred_expression | yield_expression)
target_list     ::=  target ("," target)* [","]
target          ::=  identifier
                     | "(" [target_list] ")"
                     | "[" [target_list] "]"
                     | attributeref
                     | subscription
                     | slicing
                     | "*" target

(See section Primaries for the syntax definitions for attributeref, subscription, and slicing.)

An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.

We see that the assignment statement is defined as follows:

assignment_stmt ::=  (target_list "=")+ (starred_expression | yield_expression)

Thus the statement

a = a[0] = [0]

has two target_list elements (a and a[0]) and a starred_expression element ([0]). As a result, the same list on the right-hand-side is assigned to both a and a[0], from left to right, i.e., the list [0] is first assigned to a, then a[0] is set to the same list. As a result, a[0] is set to a itself.

The behaviour of the statement

a = a[0] = {}

can be explained in a similar way. The dictionary object on the right-hand-side is first assigned to a. Then a key 0 is inserted within the same dictionary. Finally the value of a[0] is set to the same dictionary. In other words, a[0] is set to a itself.

More Experiments

The evaluation of the expression list on the right hand side first and then assigning the result to each target list from left to right explains the behaviour we observed in the previous sections. This left-to-right assignment is quite uncommon among mainstream programming languages. For example, in C, C++, Java, and JavaScript the simple assignment operator (=) has right-to-left associativity. The left-to-right assignment in Python can be further demonstrated with some intentional errors. Here is an example:

>>> a[0] = a = [0]
Traceback (most recent call last):
  File "", line 1, in 
NameError: name 'a' is not defined

In this example, when the assignment to a[0] to occurs, the variable named a is not defined yet, so it leads to NameError.

Here is another example:

>>> a = a[0] = 0
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'int' object does not support item assignment

In this example, 0 is first assigned to a. Then a[0] needs to be evaluated before 0 can be assigned to it but this evaluation fails because a is an int, a type that does not support subscription (also known as indexing), so it fails with TypeError.

Comments | #python | #programming | #technology