Libraries.

In [1]:
import copy
from collections import namedtuple

Beginner

What are tuples and why use them.


In more technical termes, tuples are defined as :

  • Immutable.
  • Ordered.
  • Subscriptable / Slicable.
  • Accepts duplicate elements.

Tuples being immutable, it helps writing "safer" code. At any given time you know that the content of your tuple is what you set it to be at the beginning, so if you don't need to add other elements to your collection, tuples are great.

Another advantage is that tuple are faster to work with than lists, although this might not often be a good enough reason to choose one over the other.

ToC

Beginner

Declaring a tuple.


There are two ways to declare a tuple, they are equivalent.

ToC

In [2]:
t1 = tuple()

t1 = ()

They can be initialized directly with values.

In [3]:
# A basic tuple.
t1 = (1, 2, 3)

# A tuple can contain different types of data.
t1 = (1, 'word', '2', True)

# Singleton.
t1 = (1,)

A tuple cannot be created with a single value with t1 = (1)

This syntax is not possible because the parenthesis are already used to group statements.

We can add a coma after the lone element to say explicitly that we whant to create a tuple.

Beginner

Accessing elements in a tuple.


Just like lists there are several ways to access elemnts in a tuple.

Tupes being immutables, it is worth noting that accessing an element of a tuple doesn't allow to modify it.

ToC

In [4]:
# A basic tuple.
t1 = (1, 2, 3)

Accessing a specific value by index using the [] notation.

In [5]:
val = t1[0]
print(val)
1

Getting the index of a specific value with the tuple.index method.

In [6]:
_ = t1.index(1)

Everything related to slicing that works on lists works for tuples too.

Beginner

Operations on tuple.


Mathematical operations are supported and can help concatenate or reproduce tuples, for example.

ToC

Additions.

In [7]:
t1 = (1, 2) + (3, 4)

print(t1)
(1, 2, 3, 4)

Product.

In [8]:
t1 = (1,) * 5

print(t1)
(1, 1, 1, 1, 1)

Special assignment operators.

In [9]:
t1 += (2,)

print(t1)
(1, 1, 1, 1, 1, 2)

Beginner

Adding, modifying and deleting elements in a tuple.


Tuples being immutable, it is not possible to add or delete elements nor is it possible to modify a value already in the tuple.

A workaround can be to create a new tuple as a subset of an old tuple, for example.

ToC

Beginner

Copying a tuple.


Tuples are immutable and as such, copying does not work the same as for lists or other mutable collection

Furthermore, tuples can contain mutable object which add a layer of complexity.


The basic idea is : since a tuple is immutable, we do not need to copy it. If you need a second variable to hold the same values as another already existing tuple, a simple reference to this same tuple should suffice.

But since tuples can contain mutable object, we still need to be able to copy it in some occasions.

ToC

Creating a copy that works in every situation without thinking about it.

In [10]:
# A basic tuple ...
t1 = (1, 2)

# ... copied to another variable.
t2 = copy.deepcopy(t1)

The copy.deepcopy function is the simplest way to create a copy if you do not want to delve too much into how python handle copying immutable objects.

Intermediate

Let's look at a few examples. First with a basic tuple.

In [11]:
t1 = (1, 2)

t2 = t1
t3 = copy.copy(t1)
t4 = copy.deepcopy(t1)

In this case, none of t2, t3 or t4 are "real" copies of t1. They are all references to the first tuple.

One could expect that deepcopy would have created a real copy but t1 being immutable containing only immutable objects (integers are immutable objects), making a copy of t1 "does not make sense".


Now let's look at a more complex example with a tuple containing a mutable object.

In [12]:
t1 = (1, 2, [3, 4])

t2 = t1
t3 = copy.copy(t1)
t4 = copy.deepcopy(t1)

Unlike with a basic tuple, t3 is an actual copy (and it is the only real copy).

We can verify that by modifying one tuple and observing the effect on the others.

In [13]:
t1[2][0] = 10

print(f"t1 : {t1}")
print(f"t2 : {t2}")
print(f"t3 : {t3}")
print(f"t4 : {t4}")
t1 : (1, 2, [10, 4])
t2 : (1, 2, [10, 4])
t3 : (1, 2, [10, 4])
t4 : (1, 2, [3, 4])

Advanced

We can illustrate that by accessing the memories addresses of these objects.

In [14]:
print(f"t1 : {hex(id(t1))}")
print(f"t2 : {hex(id(t2))}")
print(f"t3 : {hex(id(t3))}")
print(f"t4 : {hex(id(t4))}")
t1 : 0x7f92c182cac0
t2 : 0x7f92c182cac0
t3 : 0x7f92c182cac0
t4 : 0x7f92c182f1c0

One question remain. If a tuple is immutable, how can it be modified and still be the same object ?

In [15]:
print(f"t1 before modification : {hex(id(t1))}")
t1[2][0] = 100
print(f"t1 before modification : {hex(id(t1))}")
t1 before modification : 0x7f92c182cac0
t1 before modification : 0x7f92c182cac0

It can be that way because the list inside of t1 is mutable. It means that it can be modified without having to create a new object.

t1 does not actually contain the list itself but rather a reference to this link so even if elements in the list are changed, the reference is still the same and t1 is still the same object.

Beginner

Iterating on a tuple.


This is the same as with lists.

ToC

In [16]:
# A simple tuple.
t1 = (10, 12, 31)

# Iterating directly on the tuple.
for elem in t1:
    print(elem)
10
12
31
In [17]:
# Using the enumerate function.
for ind, elem in enumerate(t1, start=1):
    print(f'{elem} is the {ind}{"st" if ind == 1 else "nd" if ind == 2 else "th"} element of the tuple.')
10 is the 1st element of the tuple.
12 is the 2nd element of the tuple.
31 is the 3th element of the tuple.

Beginner

Logic tests with tuples.


Just like with lists, a tuple can be tested and is Truthy if not empty.

Since tuples are immutable, they cannot be used in while loops by removing elements each iterations. They can stil be used in if statements.

ToC

In [18]:
t1 = (1, 2)

if t1:
    print('This tuple is not empty.')
This tuple is not empty.

Beginner

Unpacking a tuple.


This is the same as with lists.

ToC

In [19]:
# Unpacking a simple tuple.
data = (1, 2, 3)
x, y, z = data

Note that tuple are the default when a function returns multiple values. Its immutable caracteristics makes it the best candidate for the job.

Intermediate

Tuple comprehension.


Tuple comprehension works the same as with list comprehension. The syntax is slightly deferent though.

It is needed to specify "tuple( ... ) ".

ToC

In [20]:
# A simple tuple.
t1 = (1, 2, 3, 4)

# Using tuple comprehension to get every element times two.
t2 = tuple(i * 2 for i in t1)

Using only parenthesis doe not yield a tuple but a generator.

In [21]:
gen = (i * 2 for i in t1)

The next function allows us to iterate through the generator.

In [22]:
next(gen)
next(gen)
list(gen)
Out[22]:
[6, 8]

A generator does not compute every element when it is created, countrary to list and tuple comprehension.

The generator calculate only the needed element when it is required to.

This makes generator a powerfull tool for iteratinf over a very large number of elements.

Intermediate

Named tuples.


Named tuples assign meaning to each position in a tuple and allow for more readable, self-documenting code. They can be used wherever regular tuples are used, and they add the ability to access fields by name instead of position index.


Let's look at an example.

ToC

In [23]:
Point = namedtuple('Point', ['x', 'y'])

Here we created a named tuple named Point.

It takes to arguments which are its x and y coordinates.

This allow us to create a new point like so :

In [24]:
p = Point(11, y=22)

print(p)
Point(x=11, y=22)

We can then access its attributes.

In [25]:
print(p[0] + p[1])
33
In [26]:
print(p.x + p.y)
33

Namedtuples are great for readability and structure of the code.