Libraries.

In [1]:
import copy
import itertools
from collections import deque

Beginner

What are lists and why use them.


In more technical termes, lists are defined as :

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

Lists are a good "go-to" collection, it works in a lot of situation, and can be used if you are not sure what type of collection to use in a part of your code yet.

As a general rule, lists are good when no other collection fit the use case.

Lists are extremly flexible, but in return it is not the most effecient collection.

ToC

Beginner

Declaring a list.


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

ToC

In [2]:
l1 = list()

l1 = []

They can be initialized directly with values.

In [3]:
l1 = [1, 2, 3]

Note the list method needs an iterable as its only argument.

Writing l1 = list([1, 2, 3]) would be redundant because we would be creating a list from another list. This method is usefull when handling an iterable that we need to use as a list.


Lists can contain almost any type of object and several different types at the same time.

In [4]:
l1 = [1, 2, 3]

l2 = [1, "Hello", 3.4]

l3 = ["mouse", [8, 4, 6], ['a']]

Lists can also contain other lists or other iterables.

Every one of these list are of type list but its elements are of different types.

In [5]:
for i, list_i in enumerate([l1, l2, l3]):
    print(f"\nType of l{i+1} : {type(list_i)}")
    for j, elt in enumerate(list_i):
        print(f"element n°{j+1} : {elt} / type : {type(elt)}")
Type of l1 : <class 'list'>
element n°1 : 1 / type : <class 'int'>
element n°2 : 2 / type : <class 'int'>
element n°3 : 3 / type : <class 'int'>

Type of l2 : <class 'list'>
element n°1 : 1 / type : <class 'int'>
element n°2 : Hello / type : <class 'str'>
element n°3 : 3.4 / type : <class 'float'>

Type of l3 : <class 'list'>
element n°1 : mouse / type : <class 'str'>
element n°2 : [8, 4, 6] / type : <class 'list'>
element n°3 : ['a'] / type : <class 'list'>

Beginner

Accessing values in a list.


There are several ways to access elements from a list. The simplest way is to access an element by index.

To do that, we use the [] notation.

ToC

In [6]:
l1 = [1, 2, 3, 4, 5]

print(f"3rd element of the list : {l1[2]}.")
3rd element of the list : 3.

Same for nested lists.

In [7]:
# Nested List
l1 = [[2, 0, 1, 5], "Word."]

elt = l1[0][1]
print(f"The first element of l1 is a list, its second element is  : {elt}")

elt = l1[1][3]
print(f"\nThe second element of l1 is a string, its 4th element is  : {elt}")
The first element of l1 is a list, its second element is  : 0

The second element of l1 is a string, its 4th element is  : d

Strings can be considered lists of caracters to some extent.

Negative indices can be used to access elements from the end of a list.

In [8]:
# Indexes negatifs (partant de la fin)
l1 = ['a', 'b', 'c', 'd', 'e']

print(f"The last element of the list is : {l1[-1]}")
The last element of the list is : e

It is possible to get a subset of a list with slices.

A slice is declared as follows :

  • [N:M] -> from N included to M excluded.
  • [N:] -> from N to the end.
  • [:M] -> from the beginning to M excluded.

Slices work with negative indices.

Here are some more examples of slices.

In [9]:
l1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']

print("Original list :")
print(l1)
Original list :
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
In [10]:
print("3rd to 5th elements :")
print(l1[2:5])
3rd to 5th elements :
['c', 'd', 'e']
In [11]:
print("1st to 6th from the end :")
print(l1[:-5])
1st to 6th from the end :
['a', 'b', 'c', 'd']
In [12]:
print("6th to last :")
print(l1[5:])
6th to last :
['f', 'g', 'h', 'i']
In [13]:
print("first to last :")
print(l1[:])
first to last :
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
In [14]:
print("2nd to 2nd to last :")
print(l1[1:-2])
2nd to 2nd to last :
['b', 'c', 'd', 'e', 'f', 'g']

With slices, the second index is always excluded.

In [15]:
print("Last element :")
print(l1[-1])
Last element :
i
In [16]:
print("Everything except the last :")
print(l1[:-1])
Everything except the last :
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

A step can be added to a slice.

  • [N:M:s] -> with s as the step.
In [17]:
l1 = [1, 2, 3, 4, 5, 6, 7]
In [18]:
print("Element from index 0 to 6 with a step of 2 :")
print(l1[0:7:2])
Element from index 0 to 6 with a step of 2 :
[1, 3, 5, 7]
In [19]:
print("Elements from index 6 to 0 with a step of -3 :")
print(l1[7::-3])
Elements from index 6 to 0 with a step of -3 :
[7, 4, 1]

Beginner

Adding items to a list.


There are several method to add items to a list, some simpler, some with more handy features.

ToC

In [20]:
l1 = [1, 3, 5]

The list.append method adds an item to the end of a list.

In [21]:
l1.append(7)

print(l1)
[1, 3, 5, 7]

The list.extend method adds multiples items from another collection.

In [22]:
l1.extend([9, 11, 13])

print(l1)
[1, 3, 5, 7, 9, 11, 13]

Beware of unordered collections, such as sets, results can be unexpected.

In [23]:
l1.extend({15, 17})

print(l1)
[1, 3, 5, 7, 9, 11, 13, 17, 15]

The list.insert method adds an item at the specified index.

In [24]:
l1.insert(1, 2)

print(l1)
[1, 2, 3, 5, 7, 9, 11, 13, 17, 15]

Not that any type of object can be inserted.

When inserting a list, it is inserted as a whole and not only its content.

Same goes for append.

In [25]:
l1.insert(2, [19, 21])

print(l1)
[1, 2, [19, 21], 3, 5, 7, 9, 11, 13, 17, 15]

Finaly, items can also be added by concatenation.

In [26]:
l2 = [19, 21]

print(l1 + l2)
[1, 2, [19, 21], 3, 5, 7, 9, 11, 13, 17, 15, 19, 21]

The * can also be used to duplicate a list.

In [27]:
print([1, 2] * 3)
[1, 2, 1, 2, 1, 2]

Intermediate

The list.append, list.extend and list.insert methods allows to keep the same object whereas the + operator creates a third object.

Advanced

Demonstration.

First with the three specific methods.

In [28]:
l1 = [1, 2, 3]

print(f"Address of l1              : {hex(id(l1))}")

l1.append(4)

print(f"\nAddress of l1 after append : {hex(id(l1))}")

l1.extend([5, 6])

print(f"\nAddress of l1 after extend : {hex(id(l1))}")

l1.insert(0, 0)

print(f"\nAddress of l1 after insert : {hex(id(l1))}")
Address of l1              : 0x7fc9d9509880

Address of l1 after append : 0x7fc9d9509880

Address of l1 after extend : 0x7fc9d9509880

Address of l1 after insert : 0x7fc9d9509880

Now with a concatenation.

In [29]:
l1 = [1, 2, 3]
l2 = [4, 5]

print(f"Address of l1                     : {hex(id(l1))}")

l1 = l1 + l2

print(f"\nAddress of l1 after concatenation : {hex(id(l1))}")
Address of l1                     : 0x7fc9d94fb1c0

Address of l1 after concatenation : 0x7fc9d94fb680

There, l1 + l2 is run and the result is being stored in a third variable, this result is then stored in l1.

Note this does not happen with the += operator since it implicitely calls the list.extend method.

Beginner

Modifying items in a list.


Just like other mutable sequence types it is possible to access items from a list by index or with a slice and change its value.

ToC

In [30]:
l1 = [1, 3, 5, 7, 9]

l1[1] = 2

print(l1)
[1, 2, 5, 7, 9]

Slices can be used to insert an item in place of a part of a list.

Their size do not have to match.

In [31]:
l1[0:4] = [1, 3]

print(l1)
[1, 3, 9]

Beginner

Deleting items from a list.


Just like adding, there are several ways to delete items from a list, with slightly different behaviour.

ToC

In [32]:
l1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g']

The list.pop(index) method removes the element at the given index.

The value is returned.

In [33]:
print('Item return by pop :')
print(l1.pop(2))

print('\nState of the list :')
print(l1)
Item return by pop :
c

State of the list :
['a', 'b', 'd', 'e', 'f', 'g']

The list.remove(elem) method removes the element given as a parameter.

Nothing is returned.

In [34]:
print('Item return by remove :')
print(l1.remove('a'))

print('\nState of the list :')
print(l1)
Item return by remove :
None

State of the list :
['b', 'd', 'e', 'f', 'g']

The del statement removes the element at the given index without returning it.

In [35]:
print("Deleting the third element.")
del l1[2]

print('\nState of the list :')
print(l1)
Deleting the third element.

State of the list :
['b', 'd', 'f', 'g']

the list.clear method removes every items from a list.

In [36]:
print("Clearing the list.")
l1.clear()

print('\nState of the list :')
print(l1)
Clearing the list.

State of the list :
[]

Beginner

Copying a list.


Copying a list is not as simple as one might think and can lead to unexpected results.

ToC

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

In [37]:
# A basic list ...
l1 = [1, 2, 3]

# ... copied to another variable.
l2 = copy.deepcopy(l1)

Now let's look at aa basic example.

First we create a list l1.

In [38]:
l1 = [1, 2, 3]

Then we create a list l2 and assign it l1 directly.

In [39]:
l2 = l1

Now let's change the first element of l1 and observe the effect on l2.

In [40]:
l1[0] = 4

print(l2)
[4, 2, 3]

The change affected l2.

This is because l2 = l1 affect to l2 the address of the list referenced by l1 and not the list itself. So if change are made to the list, both variables reference the same object and therefor, both variables are "changed".


To create an actual copy of a list both list.copy() and copy.copy(list) can be used.

In [41]:
l1 = [1, 2, 3]

l2 = l1.copy()

l1[0] = 4

print(l2)
[1, 2, 3]

Here change to the first item of l1 does not affect l1, they are distinct objects.

Intermediate

Note the problem reappears with nested lists. The copy method makes a copy of every items in the list. If an item in the list is a list itself (or another collection) the reference to this object is copyied and not the object itself.

Let's see an example.

In [42]:
l1 = [1, 2, [3, 4]]

This time we use the list.copy method.

In [43]:
l2 = l1.copy()

We can then change values in l2 and observe the changes.

In [44]:
l2[0] = 0
l2[2][0] = 5

print(l2)
[0, 2, [5, 4]]

Finally let's see how it impacted l1.

In [45]:
print(l1)
[1, 2, [5, 4]]

The first element is not impacted, as expected because l2 is a copy. But the nested list is changed.


To avoid that, we can use the copy.deepcopy function that recursively copies every object in a collection.

Advanced

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

In [46]:
l1 = [1, 2, [3, 4]]
l2 = l1
l3 = copy.copy(l1)
l4 = copy.deepcopy(l1)

print(f"l1 : {hex(id(l1))}")
print(f"l2 : {hex(id(l2))}")
print(f"l3 : {hex(id(l3))}")
print(f"l4 : {hex(id(l4))}")
l1 : 0x7fc9d94fd8c0
l2 : 0x7fc9d94fd8c0
l3 : 0x7fc9c82d1880
l4 : 0x7fc9d9509700

As expected, l1 and l2 are the same object, but l3 and l4 are distinct from l1.


Now, how about the second list, nested in l1.

In [47]:
print(f"Nested list accessed from l1 : {hex(id(l1[2]))}")
print(f"Nested list accessed from l2 : {hex(id(l2[2]))}")
print(f"Nested list accessed from l3 : {hex(id(l3[2]))}")
print(f"Nested list accessed from l4 : {hex(id(l4[2]))}")
Nested list accessed from l1 : 0x7fc9c82d1f40
Nested list accessed from l2 : 0x7fc9c82d1f40
Nested list accessed from l3 : 0x7fc9c82d1f40
Nested list accessed from l4 : 0x7fc9d94fb680

Again, as expected, only the secondary list from l4 is really a copy.

Beginner

Iterating through a list.


for loops are built in python to iterate over every item in a collection.

ToC

In [48]:
l1 = ['word', 'apple', 'red', 'bicyle']

The for ELEM in LIST: syntax is the most common.

In [49]:
for str_word in l1:
    print(str_word)
word
apple
red
bicyle

The enumerate built-in function can be use to get an iterator over a collection.

It provides every item in order along with its index.

The start argument allow to offset the returned index.

In [50]:
for i, elem in enumerate(l1, start=1):
    print(f'{elem:6} is the element n°{i} of the list.')
word   is the element n°1 of the list.
apple  is the element n°2 of the list.
red    is the element n°3 of the list.
bicyle is the element n°4 of the list.

Beginner

Logic tests with lists.


In Python, every object can be tested for truth value.

Testing a list for its trush value is simple.

  • It is True if the list is not empty.

  • It is False otherwise.

ToC

Examples of Falsy and Truthy lists.

In [51]:
l1 = []

print(bool(l1))
False
In [52]:
l1 = [0]

print(bool(l1))
True

Use-case : a list can be used as a condition for a while statement. It is truthy until it becomes empty.

In [53]:
l1 = [1, 2, 3, 4, 5]

res = 0
while l1:
    res += l1.pop()

print(res)
15

Here we cummulatively add elements of the list by removing the first element until it is empty.

Beginner

Unpacking a list.


With pattern matching, python is able map multiple variables on the left of an equal sign and assign the right value from an iterable on the right side.

ToC

A simple unpacking.

In [54]:
a, b, c = [1, 2, 3]

print(f"a : {a}")
print(f"b : {b}")
print(f"c : {c}")
a : 1
b : 2
c : 3

Intermediate

Upacking can be a bit more complex.

The * notation indicate that this variable should receive multiple values (as a list) if possible.

In [55]:
a, *b, c = [1, 2, 3, 4, 5]
print(f"a : {a}")
print(f"b : {b}")
print(f"c : {c}")
a : 1
b : [2, 3, 4]
c : 5

Here, b is in second place. the * indicate that b should receive every starting from the second one.

The c tells us that values should stop being given to b (still as a list) so c can take the last value.

Unpacking can only have one starred expression.

Intermediate

Concrete example.

In [56]:
def test(string_to_split):
    """Test function."""
    return string_to_split.split('-', maxsplit=3)


val1, val2, val3 = test('one-two-three')

print(val1)
print(val2)
print(val3)
one
two
three

Intermediate

List comprehension.


List comprehensions provide a concise way to create lists. Common applications are to make new lists where each element is the result of some operations applied to each member of another sequence or iterable, or to create a subsequence of those elements that satisfy a certain condition.

ToC

Simple list comprehension.

In [57]:
# Getting powers of two from a list from 0 to 9.
pow2 = [2 ** x for x in range(10)]
print(pow2)
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]

List comprehension with a logic test.

In [58]:
# Getting powers of two from a list from 0 to 9 from elements greater than 5.
pow2 = [2 ** x for x in range(10) if x > 5]
print(pow2)
[64, 128, 256, 512]

List comprehension with a logic test and an else statement.

Note that the syntax is different than with a simple test.

In [59]:
pow2 = [2 ** x if x > 5 else 0 for x in range(10)]
print(pow2)
[0, 0, 0, 0, 0, 0, 64, 128, 256, 512]

List comprehension with two lists.

In [60]:
l1 = [x + y for x in ['Python ', 'C '] for y in ['Language', 'Programming']]

print(l1)
['Python Language', 'Python Programming', 'C Language', 'C Programming']

List comprehension with a lag.

In [61]:
l1 = [1, 2, 4, 2]
roll_sum = [y + x for x, y in zip(l1, l1[1:])]

print(roll_sum)
[3, 6, 6]

Beginner

Ranges


The range type represents an immutable sequence of numbers and is commonly used for looping a specific number of times in for loops.

While they share some similarities with lists, they are very different in terms of how python handle them.

A range is a much lighter object thant its list equivalent.

A range only needs to know :

  • Them current value.
  • How to get to the next value.
  • When to stop.

ToC

In [62]:
r1 = range(0, 10, 2)

In this example, r1 starts at 0, ends at 9 (10 is exluded) and goes through element by 2.


Ranges have an array of interesting features.

start attribute.

In [63]:
print(f"Starting point of the range : {r1.start}")
Starting point of the range : 0

stop attribute.

In [64]:
print(f"End point of the range (excluded) : {r1.stop}")
End point of the range (excluded) : 10

step attribute.

In [65]:
print(f"Step of the range : {r1.step}")
Step of the range : 2

index method.

In [66]:
print(f"6th index of the range : {r1.index(6)}")
6th index of the range : 3

A list (or other collection) can be created from a range if it is absolutly needed but for iterating a range is often much prefered.

In [67]:
l1 = list(r1)

print(l1)
[0, 2, 4, 6, 8]

Intermediate

Deques.


Juste like ranges, deques are a more specialized collection. It is made to work as a stack or a queue.

A stack is a collection of elements where any new element is added on top and any removed element is taken from the top.

A queue is a collection of elements where any new element is added on top and any removed element is taken from the bottom.

ToC

In [68]:
dq1 = deque([1, 5, 8])

Adding elements can be done with deque.append, deque.appendleft, deque.extend or deque.extendleft. Any of these methods work in a similar fashion as they do with lists.

In [69]:
dq1.append(0)
print("Inserting 0 :")
print(dq1)
Inserting 0 :
deque([1, 5, 8, 0])
In [70]:
dq1.appendleft(12)
print("Inserting 12 to the left :")
print(dq1)
Inserting 12 to the left :
deque([12, 1, 5, 8, 0])
In [71]:
dq1.extend([2, 4])
print("Inserting [2, 4] :")
print(dq1)
Inserting [2, 4] :
deque([12, 1, 5, 8, 0, 2, 4])
In [72]:
dq1.extendleft([6, 5])
print("Inserting [6, 5] to the left :")
print(dq1)
Inserting [6, 5] to the left :
deque([5, 6, 12, 1, 5, 8, 0, 2, 4])

Note the deque.insert method allows to insert elements at specific indices.


Removing elements is as easy with the deque.pop, deque.popleft, deque.remove or deque.clear methods.

In [73]:
dq1.pop()
print("Popping to the right :")
print(dq1)
Popping to the right :
deque([5, 6, 12, 1, 5, 8, 0, 2])
In [74]:
dq1.popleft()
print("Popping to the left :")
print(dq1)
Popping to the left :
deque([6, 12, 1, 5, 8, 0, 2])
In [75]:
dq1.remove(2)
print("Remove the first occurence of 2 :")
print(dq1)
Remove the first occurence of 2 :
deque([6, 12, 1, 5, 8, 0])
In [76]:
dq1.clear()
print("Purging the deque :")
print(dq1)
Purging the deque :
deque([])

The deque.rotate(n) method can be interesting when you have to shift priorities in a stack or a queue.

In [77]:
dq1 = deque([1, 2, 3, 4, 5])
In [78]:
print("Before rotation :")
print(dq1)
Before rotation :
deque([1, 2, 3, 4, 5])
In [79]:
dq1.rotate(2)
print("After rotating by 2 :")
print(dq1)
After rotating by 2 :
deque([4, 5, 1, 2, 3])

Intermediate

The itertools library.


The itertools library contains a large array of really interesting features helpful when working with list and iterables.

ToC

Flatten a nested list.

In [80]:
l1 = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(l1)))
[1, 2, 3, 4, 5, 6]

Pairwise product of lists.

In [81]:
for p in itertools.product([1, 2, 3], [4, 5]):
    print(p)
(1, 4)
(1, 5)
(2, 4)
(2, 5)
(3, 4)
(3, 5)

Permutations of elements of lists.

In [82]:
# Permutations à partir de listes.
for p in itertools.permutations([1, 2, 3, 4]):
    print(p)
(1, 2, 3, 4)
(1, 2, 4, 3)
(1, 3, 2, 4)
(1, 3, 4, 2)
(1, 4, 2, 3)
(1, 4, 3, 2)
(2, 1, 3, 4)
(2, 1, 4, 3)
(2, 3, 1, 4)
(2, 3, 4, 1)
(2, 4, 1, 3)
(2, 4, 3, 1)
(3, 1, 2, 4)
(3, 1, 4, 2)
(3, 2, 1, 4)
(3, 2, 4, 1)
(3, 4, 1, 2)
(3, 4, 2, 1)
(4, 1, 2, 3)
(4, 1, 3, 2)
(4, 2, 1, 3)
(4, 2, 3, 1)
(4, 3, 1, 2)
(4, 3, 2, 1)

Combinations of elements of lists.

In [83]:
# Combinaisons.
for p in itertools.combinations([1, 2, 3, 4], 2):
    print(p)
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)

Cummulative min.

In [84]:
list(itertools.accumulate([9, 21, 17, 5, 11, 12, 2, 6], min))
Out[84]:
[9, 9, 9, 5, 5, 5, 2, 2]

The itertools.takewhile method.

In [85]:
list(itertools.takewhile(lambda x: x < 3, [0, 1, 2, 3, 4]))
Out[85]:
[0, 1, 2]

Beginner

Tips and tricks.

ToC

In [86]:
l1 = [1, 2, 3, 4, 2, 2, 3, 1, 4, 4, 4]

Most frequent element of a list.

In [87]:
print(max(set(l1), key=l1.count))
4

Index of min or max.

In [88]:
print(max(enumerate(l1), key=lambda x: x[1])[0])
3