Why Can Tuples Contain Mutable Items

Why can tuples contain mutable items?

That's an excellent question.

The key insight is that tuples have no way of knowing whether the objects inside them are mutable. The only thing that makes an object mutable is to have a method that alters its data. In general, there is no way to detect this.

Another insight is that Python's containers don't actually contain anything. Instead, they keep references to other objects. Likewise, Python's variables aren't like variables in compiled languages; instead the variable names are just keys in a namespace dictionary where they are associated with a corresponding object. Ned Batchhelder explains this nicely in his blog post. Either way, objects only know their reference count; they don't know what those references are (variables, containers, or the Python internals).

Together, these two insights explain your mystery (why an immutable tuple "containing" a list seems to change when the underlying list changes). In fact, the tuple did not change (it still has the same references to other objects that it did before). The tuple could not change (because it did not have mutating methods). When the list changed, the tuple didn't get notified of the change (the list doesn't know whether it is referred to by a variable, a tuple, or another list).

While we're on the topic, here are a few other thoughts to help complete your mental model of what tuples are, how they work, and their intended use:

  1. Tuples are characterized less by their immutability and more by their intended purpose.

    Tuples are Python's way of collecting heterogeneous pieces of information under one roof. For example,
    s = ('www.python.org', 80)
    brings together a string and a number so that the host/port pair can be passed around as a socket, a composite object. Viewed in that light, it is perfectly reasonable to have mutable components.

  2. Immutability goes hand-in-hand with another property, hashability. But hashability isn't an absolute property. If one of the tuple's components isn't hashable, then the overall tuple isn't hashable either. For example, t = ('red', [10, 20, 30]) isn't hashable.

The last example shows a 2-tuple that contains a string and a list. The tuple itself isn't mutable (i.e. it doesn't have any methods that for changing its contents). Likewise, the string is immutable because strings don't have any mutating methods. The list object does have mutating methods, so it can be changed. This shows that mutability is a property of an object type -- some objects have mutating methods and some don't. This doesn't change just because the objects are nested.

Remember two things. First, immutability is not magic -- it is merely the absence of mutating methods. Second, objects don't know what variables or containers refer to them -- they only know the reference count.

Hope, this was useful to you :-)

Can tuple be considered both as mutable and immutable based on content

All a tuple does is contain a fixed list of references. Those references cannot be changed, and so this makes a tuple immutable. Whether the referenced objects are mutable is another story, but that's beyond the scope of tuple, so it would not be accurate to say a tuple can be mutable if it references mutable objects.

Why is a NamedTuple containing mutable objects hashable, when a Tuple containing mutable objects is not?

But when I create a namedtuple with list as items it is still
hashable...

You never do that. You create a named-tuple with a str, 'adam' and an int, 20

The following:

named_tuple = namedtuple("TestTuple", 'name age')

And

named_tuple = namedtuple("TestTuple", ["name", "age"])

Do not create namedtuple objects, they create namedtuple classes. According to the docs:

Returns a new tuple subclass named typename.

In other words, collections.namedtuple is a factory function that returns a class. If you create instances of those classes, their instances follow the same rules as regular tuple instances.

So consider:

>>> from collections import namedtuple
>>> TestTuple = namedtuple('TestTuple', ['name', 'age'])
>>> type(TestTuple)
<class 'type'>
>>> class A: pass
...
>>> type(A)
<class 'type'>

TestTuple, the return value of the namedtuple factory function, is not a namedtuple instance, it is an instance of type, like all other classes.

When you create instances of this class:

>>> test_tuple = TestTuple('adam',32)
>>> type(test_tuple)
<class '__main__.TestTuple'>

They follow the usual rules of hashability that regular tuple objects do:

>>> hash(test_tuple)
5589201399616687819
>>> test_tuple = TestTuple('adam', [32, 31])
>>> hash(test_tuple)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Note, the fieldnames argument accepts either a sequence (e.g. a list) of fieldnames, or for convenience, a space/comma-delimited string of fieldnames, so also from the docs:

...
The field_names are a sequence of strings such as ['x', 'y'].
Alternatively, field_names can be a single string with each fieldname
separated by whitespace and/or commas, for example 'x y' or 'x, y'.

In Python a tuple is immutable object but allows mutation of a contained list?

You did understand the immutable part the wrong way. The tuple is indeed immutable, as you cannot change its list:

In [3]: tup[2] = []
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-02255d491606> in <module>()
----> 1 tup[2] = []

TypeError: 'tuple' object does not support item assignment

The reference to the list is immutable in that tuple. However, the list itself is mutable, and hence can be modified, as you already know.

In other words, your tuple, which is immutable, contains three elements:

  • Integer 1
  • string "a"
  • A reference to a list.

You can imagine it as the adress of the list, if you know C. If you don't know anything about memory, you can see it as "the tuple knows where to find the list". But it doesn't contain the list itself.

Why tuple is not mutable in Python?

A few reasons:

  • Mutable objects like lists cannot be used as dictionary keys or set members in Python, since they are not hashable. If lists were given __hash__ methods based on their contents, the values returned could change as the contents change, which violates the contract for hash values.
  • If Python only had mutable sequences, constructors which accepted sequences would often need to copy them to ensure that the sequences couldn't be modified by other code. Constructors can avoid defensive copying by only accepting tuples. Better yet, they can pass sequence arguments through the tuple method which will copy only when necessary.

Lists are mutable but tuples are immutable, then how is the value of x changed when running function tuple?

Let's see this example.

x_list = [1, 2, 3, 1, 2, 3]
x_tuple = (1, 2, 3, 1, 2, 3)

def change_them_comprehension(x, y, z):
return [z if i == y else i for i in x]

def change_them_manual(x, y, z):
for idx, i in enumerate(x):
if i == y:
x[idx] = z
return x

print("list")
print(id(x_list)) # 1877716860160
print(id(change_them_comprehension(x_list, 2, 'zzz'))) # 1877716698368
print(id(change_them_manual(x_list, 2, 'zzz'))) # 1877716860160

print("tuple")
print(id(x_tuple)) # 1877716262336
print(id(change_them_comprehension(x_tuple, 2, 'zzz'))) # 1877716698368
print(id(change_them_manual(x_tuple, 2, 'zzz'))) # TypeError: 'tuple' object does not support item assignment

Notice how your change_them returns a new list comprehension, which is an absolutely different list from the original one (see they have different ids). This does not involve mutation; instead of editing the list you return a new list (in both the original tuple and original list cases).

Now check change_them_manual. This method does edit the input list/tuple and not creates a new one. See how the returned list is the same (same id). We can edit it because lists are mutable. However, when we try to do the same with the tuple we get an error, which tells us we can't edit the tuple, this is what not being mutable means.

python tuple is immutable - so why can I add elements to it

5 is immutable, too. When you have an immutable data structure, a += b is equivalent to a = a + b, so a new number, tuple or whatever is created.

When doing this with mutable structures, the structure is changed.

Example:

>>> tup = (1, 2, 3)
>>> id(tup)
140153476307856
>>> tup += (4, 5)
>>> id(tup)
140153479825840

See how the id changed? That means it's a different object.

Now with a list, which is mutable:

>>> lst = [1, 2, 3]
>>> id(lst)
140153476247704
>>> lst += [4, 5]
>>> id(lst)
140153476247704

The id says the same.



Related Topics



Leave a reply



Submit