Append to a List Defined in a Tuple - Is It a Bug

Append to a list defined in a tuple - is it a bug?

Yes it's expected.

A tuple cannot be changed. A tuple, like a list, is a structure that points to other objects. It doesn't care about what those objects are. They could be strings, numbers, tuples, lists, or other objects.

So doing anything to one of the objects contained in the tuple, including appending to that object if it's a list, isn't relevant to the semantics of the tuple.

(Imagine if you wrote a class that had methods on it that cause its internal state to change. You wouldn't expect it to be impossible to call those methods on an object based on where it's stored).

Or another example:

>>> l1 = [1, 2, 3]
>>> l2 = [4, 5, 6]
>>> t = (l1, l2)
>>> l3 = [l1, l2]
>>> l3[1].append(7)

Two mutable lists referenced by a list and by a tuple. Should I be able to do the last line (answer: yes). If you think the answer's no, why not? Should t change the semantics of l3 (answer: no).

If you want an immutable object of sequential structures, it should be tuples all the way down.

Why does it error?

This example uses the infix operator:

Many operations have an “in-place” version. The following functions
provide a more primitive access to in-place operators than the usual
syntax does; for example, the statement x += y is equivalent to x =
operator.iadd(x, y). Another way to put it is to say that z =
operator.iadd(x, y) is equivalent to the compound statement z = x; z
+= y.

https://docs.python.org/2/library/operator.html

So this:

l = [1, 2, 3]
tup = (l,)
tup[0] += (4,5,6)

is equivalent to this:

l = [1, 2, 3]
tup = (l,)
x = tup[0]
x = x.__iadd__([4, 5, 6]) # like extend, but returns x instead of None
tup[0] = x

The __iadd__ line succeeds, and modifies the first list. So the list has been changed. The __iadd__ call returns the mutated list.

The second line tries to assign the list back to the tuple, and this fails.

So, at the end of the program, the list has been extended but the second part of the += operation failed. For the specifics, see this question.

Python, is this a bug, append to a list within a tuple results in None?

list.append() always returns None

so arf[2].append(3) will append 3 to arf and return None

you never get to see this change to arf[2] because you are immediately rebinding arf to the newly created tuple

Perhaps this is what you want

arf = (arf[0], arf[1], arf[2]+[3])

Why can you extend/append to a list in a tuple, but not assign to it?

If tuples are immutable, why is it possible to change a list within a tuple?

Because "tuples are immutable" only means you cannot modify the tuple. The list that's referred to from the tuple is not part of the tuple, it doesn't "know" that it is in a tuple, and it has no means to resist being modified.

Why does the first example raise an error and the other two not?

Because of how += works. It calls __iadd__ on the list and then (because __iadd__ is not required to return the original object) attempts to assign the resulting modified object back to the tuple. The first thing succeeds, the second thing fails.

That is, for this case where t[2] has an __iadd__ function, t[2] += [5,6] is equivalent to:

t[2] = t[2].__iadd__([5,6])

In the first example, why is the list inside the tuple changed even though an error is raised?

Because this Python operation doesn't offer what in C++ we'd call a "strong exception guarantee". The first part of the operation has already been performed, and cannot be (or at any rate is not) reversed when the second part fails. For the official version see Why does a_tuple[i] += [‘item’] raise an exception when the addition works?

Updating a list within a tuple

This is actually documented in the Python docs.

EDIT: Here's a summary so that this is a more complete answer.

  1. When we use +=, Python calls the __iadd__ magic method on the item, then uses the return value in the subsequent item assignment.
  2. For lists, __iadd__ is equivalent to calling extend on the list and then returning the list.
  3. Therefore, when we call tup[3] += [6], it is equivalent to:

    result = tup[3].__iadd__([6])
    tup[3] = result
  4. From #2, we can determine this is equivalent to:

    result = tup[3].extend([6])
    tup[3] = result
  5. The first line succeeds in calling extend on the list, and since the list is mutable, it updates. However, the subsequent assignment fails because tuples are immutable, and throws the error.

Why does += of a list within a Python tuple raise TypeError but modify the list anyway?

As I started mentioning in comment, += actually modifies the list in-place and then tries to assign the result to the first position in the tuple. From the data model documentation:

These methods are called to implement the augmented arithmetic assignments (+=, -=, =, /=, //=, %=, *=, <<=, >>=, &=, ^=, |=). These methods should attempt to do the operation in-place (modifying self) and return the result (which could be, but does not have to be, self).

+= is therefore equivalent to:

t[0].extend(['world']);
t[0] = t[0];

So modifying the list in-place is not problem (1. step), since lists are mutable, but assigning the result back to the tuple is not valid (2. step), and that's where the error is thrown.



Related Topics



Leave a reply



Submit