That's actually pretty interesting!
As we know, the list l
in the function definition is initialized only once at the definition of this function, and for all invocations of this function, there will be exactly one copy of this list. Now, the function modifies this list, which means that multiple calls to this function will modify the exact same object multiple times. This is the first important part.
Now, consider the expression that adds these lists:
f()+f()+f()
According to the laws of operator precedence, this is equivalent to the following:
(f() + f()) + f()
...which is exactly the same as this:
temp1 = f() + f() # (1)
temp2 = temp1 + f() # (2)
This is the second important part.
Addition of lists produces a new object, without modifying any of its arguments. This is the third important part.
Now let's combine what we know together.
In line 1 above, the first call returns [0]
, as you'd expect. The second call returns [0, 1]
, as you'd expect. Oh, wait! The function will return the exact same object (not its copy!) over and over again, after modifying it! This means that the object that the first call returned has now changed to become [0, 1]
as well! And that's why temp1 == [0, 1] + [0, 1]
.
The result of addition, however, is a completely new object, so [0, 1, 0, 1] + f()
is the same as [0, 1, 0, 1] + [0, 1, 2]
. Note that the second list is, again, exactly what you'd expect your function to return. The same thing happens when you add f() + ["-"]
: this creates a new list
object, so that any other calls to f
won't interfere with it.
You can reproduce this by concatenating the results of two function calls:
>>> f() + f()
[0, 1, 0, 1]
>>> f() + f()
[0, 1, 2, 3, 0, 1, 2, 3]
>>> f() + f()
[0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]
Again, you can do all that because you're concatenating references to the same object.