1.4 Containers#

Estimated time for this notebook: 10 minutes

1.4.1 Checking for containment.#

The list we saw is a container type: its purpose is to hold other objects. We can ask python whether or not a container contains a particular item:

"Dog" in ["Cat", "Dog", "Horse"]
True
"Bird" in ["Cat", "Dog", "Horse"]
False
2 in range(5)
True
99 in range(5)
False

1.4.2 Mutability#

A list can be modified: (is mutable)

name = "James Philip John Hetherington".split(" ")
print(name)
['James', 'Philip', 'John', 'Hetherington']
name[0] = "Dr"
name[1:3] = ["Griffiths-"]
name.append("PhD")

print(" ".join(name))
Dr Griffiths- Hetherington PhD

1.4.3 Tuples#

A tuple is an immutable sequence. It is like a list, except it cannot be changed. It is defined with round brackets.

x = (0,)
type(x)
tuple
my_tuple = ("Hello", "World")
my_tuple[0] = "Goodbye"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 2
      1 my_tuple = ("Hello", "World")
----> 2 my_tuple[0] = "Goodbye"

TypeError: 'tuple' object does not support item assignment
type(my_tuple)
tuple

str is immutable too:

fish = "Hake"
fish[0] = "R"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 2
      1 fish = "Hake"
----> 2 fish[0] = "R"

TypeError: 'str' object does not support item assignment

But note that container reassignment is moving a label, not changing an element:

fish = "Rake"  # OK!

Supplementary material: Try the online memory visualiser for this one.

1.4.4 Memory and containers#

The way memory works with containers can be important:

x = list(range(3))
x
[0, 1, 2]
y = x
y
[0, 1, 2]
z = x[0:3]
y[1] = "Gotcha!"
x
[0, 'Gotcha!', 2]
y
[0, 'Gotcha!', 2]
z
[0, 1, 2]
z[2] = "Really?"
x
[0, 'Gotcha!', 2]
y
[0, 'Gotcha!', 2]
z
[0, 1, 'Really?']

Supplementary material: This one works well at the memory visualiser.

The explanation: While y is a second label on the same object, z is a separate object with the same data. Writing x[:] creates a new list containing all the elements of x (remember: [:] is equivalent to [0:<last>]). This is the case whenever we take a slice from a list, not just when taking all the elements with [:].

The difference between y=x and z=x[:] is important!

Nested objects make it even more complicated:

x = [["a", "b"], "c"]
y = x
z = x[0:2]
x[0][1] = "d"
z[1] = "e"
x
[['a', 'd'], 'c']
y
[['a', 'd'], 'c']
z
[['a', 'd'], 'e']

Try the visualiser again.

Supplementary material: The copies that we make through slicing are called shallow copies: we don’t copy all the objects they contain, only the references to them. This is why the nested list in x[0] is not copied, so z[0] still refers to it. It is possible to actually create copies of all the contents, however deeply nested they are - this is called a deep copy. Python provides methods for that in its standard library, in the copy module. You can read more about that, as well as about shallow and deep copies, in the library reference.

1.4.5 Identity vs Equality#

Having the same data is different from being the same actual object in memory:

[1, 2] == [1, 2]
True
[1, 2] is [1, 2]
False

The == operator checks, element by element, that two containers have the same data. The is operator checks that they are actually the same object.

But, and this point is really subtle, for immutables, the python language might save memory by reusing a single instantiated copy. This will always be safe.

"Hello" == "Hello"
True
"Hello" is "Hello"
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
/tmp/ipykernel_11459/3904443404.py:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
  "Hello" is "Hello"
True

This can be useful in understanding problems like the one above:

x = range(3)
y = x
z = x[:]
x == y
True
x is y
True
x == z
True
x is z
False