Informatique et Calcul Scientifique

Cours 5

Author

Leonard Hardiman et Ghid Maatouk

Rappel: les méthodes de listes

méthode ce qu’elle fait ce qu’elle retourne
L.append(x) rajoute x None
L.insert(i, x) insère x à l’indice i None
L.remove(x) enlève la première occurrence de x None
L.pop() enlève dernier objet le dernier objet
L.pop(i) enlève l’objet à l’indice i l’objet à l’indice i

Un exemple: liste de listes

Les éléments d’une liste peuvent être de n’importe quel type; en particulier, ils peuvent être à leur tour des listes.

M = [[10, 20, 30], [1, 3, 5]]

print('M[0] =', M[0])
print('M[1] =', M[1])

# Parcourir une liste de listes
for ligne in M:
    for element in ligne:
        print(element, end=' ')
    print()  # Nouvelle ligne
M[0] = [10, 20, 30]
M[1] = [1, 3, 5]
10 20 30 
1 3 5 

Un exemple: liste de listes (matrices)

Une liste de listes est utile pour représenter une matrice m par n.

  • On utilise une liste contenant m listes, chacune contenant n éléments.
  • La matrice ci-dessous peut être représentée par M = [[1, 2, 3], [4, 5, 6]]
0 1 2
0 1 2 3
1 4 5 6
  • M[i][j] est l’élément de la ligne i et de la colonne j:
M = [[1, 2, 3], [4, 5, 6]]

print('M[1][2] =', M[1][2])
M[1][2] = 6

Exemple : Construction de matrices

On peut utiliser la méthode append() pour remplir une matrice de dimension m×n.

m, n = 3, 4
M = []

for i in range(m):
    ligne = []
    for j in range(n):
        valeur = i * n + (j % 2)
        ligne.append(valeur)
    M.append(ligne)

for ligne in M: print(ligne)
[0, 1, 0, 1]
[4, 5, 4, 5]
[8, 9, 8, 9]

Il faut pour cela remplir les n colonnes d’une ligne individuellement et les stocker dans une liste (boucle j), puis ajouter chaque ligne précédemment créée à la liste représentant la matrice M (boucle i).

Copier une liste

Pour une liste L1, l’instruction L2 = L1.copy() crée une nouvelle liste contenant les mêmes valeurs que L1 et affecte cette nouvelle liste à la variable L2.

L1 = [2, 4, 6, 8]
L2 = L1.copy()

print('L1 et L2 contiennent les mêmes valeurs:')
print('L1 =', L1, 'L2 =', L2)
print('L1 == L2:', L1 == L2)

print('L1 et L2 ne sont pas le même objet:')
print('id(L1) =', id(L1), ';', 'id(L2) =', id(L2))
L1 et L2 contiennent les mêmes valeurs:
L1 = [2, 4, 6, 8] L2 = [2, 4, 6, 8]
L1 == L2: True
L1 et L2 ne sont pas le même objet:
id(L1) = 128839677452992 ; id(L2) = 128839677457216

Remarque. Cette opération est équivalente à L2 = L1[:] vue précédemment.

Comme les listes L1 et L2 ne sont pas le même objet, elles évoluent de manière indépendante.

L1 = [2, 4, 6, 8]
L2 = L1.copy()
L1.pop()
L2[1] = 42

print('L1 =', L1, 'L2 =', L2)
L1 = [2, 4, 6] L2 = [2, 42, 6, 8]

Représentation de listes en mémoire

Faire une copie d’une liste de listes produit un comportement surprenant.

# Comportement prévisible
L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0] = [1, 3, 5]
print('L1 =', L1)
print('L2 =', L2)
L1 = [[1, 3, 5], [2, 4]]
L2 = [[1, 3], [2, 4]]
# Comportement bizarre?
L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0].append(5)
print('L1 =', L1)
print('L2 =', L2)
L1 = [[1, 3, 5], [2, 4]]
L2 = [[1, 3, 5], [2, 4]]

Pour comprendre, il faut réfléchir à comment les listes Python sont implémentées en mémoire.

  • Une liste Python est stockée en mémoire comme un bloc contigu de références aux éléments de la liste.
    • Une référence est l’adresse en mémoire de l’élément auquel elle se réfère.
    • Chaque référence occupe un bloc de taille constante en mémoire.
  • Ainsi, Python peut allouer efficacement l’espace de stockage d’une liste, même si les éléments de la liste sont arbitrairement grands (par exemple, dans le cas d’une liste de listes).

Le comportement prévisible

L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0] = [1, 3, 5]
print('L1 =', L1)
print('L2 =', L2)
  1. L1 contient deux références vers les listes [1, 3] et [2, 4] stockées en mémoire:

  2. L2 est une copie des références vers lesquelles pointent les éléments de la liste L1. Les éléments de L1 et L2 pointent ainsi vers les mêmes objets:

  3. Le 0ᵉ élément de L1 est remplacé par une nouvelle référence vers la liste [1, 3, 5] qui est créée ailleurs dans la mémoire, alors que le 0ᵉ élément de L2 reste inchangé:

Le comportement bizarre

L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0].append(5)
print('L1 =', L1)
print('L2 =', L2)
  1. L1 contient deux références vers les listes [1, 3] et [2, 4] stockées en mémoire:

  2. L2 est une copie des références vers lesquelles pointent les éléments de la liste L1. Les éléments de L1 et L2 pointent ainsi vers les mêmes objets:

  3. La liste [1,3] à laquelle le 0ᵉ élément de la liste L1 fait référence est modifiée. Comme L2[0] fait référence au même objet, son affichage affichera la même valeur:

Moralité

  • Les listes ne contiennent pas des objets, mais des références à des objets existant en mémoire.
  • Les listes sont des objets mutables (c’est-à-dire des objets qu’on peut modifier après les avoir créés).
  • Lorsqu’on exécute L2 = L1.copy() ou L2 = L1[:] on crée en une nouvelle copie de la liste L1, qui contient les mêmes références.
L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0] = [1, 3, 5]

print(L1)
print(L2)
[[1, 3, 5], [2, 4]]
[[1, 3], [2, 4]]
L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0].append(5)

print(L1)
print(L2)
[[1, 3, 5], [2, 4]]
[[1, 3, 5], [2, 4]]

Donc le comportement “bizarre” dans le second exemple est dû au fait que L1 et L2 satisfont la règle (1) et que leurs éléments, étant aussi des listes, satisfont la règle (2). L1 et L2 sont des listes différents qui contiennent les mêmes références !

Remarque. copy() n’est pas le seul moyen de créer deux listes différentes contenant les mêmes références. L1 = L1 + [2, 3] crée également une nouvelle liste contenant toutes les références de L1 (ainsi que deux nouvelles références).

Copie profonde

Pour faire une “vraie” copie, ou copie profonde d’une liste de listes (qui va copier chacune des sous-listes au lieu de pointer vers les mêmes sous-listes) on utilise la fonction deepcopy() du module copy.

import copy

# Copie superficielle
L1 = [[1, 3], [2, 4]]
L2 = L1.copy()
L1[0].append(5)

print('Copie superficielle:')
print('L1 =', L1)
print('L2 =', L2)
Copie superficielle:
L1 = [[1, 3, 5], [2, 4]]
L2 = [[1, 3, 5], [2, 4]]
import copy

# Copie profonde
L1 = [[1, 3], [2, 4]]
L2 = copy.deepcopy(L1)
L1[0].append(5)

print('Copie profonde:')
print('L1 =', L1)
print('L2 =', L2)
Copie profonde:
L1 = [[1, 3, 5], [2, 4]]
L2 = [[1, 3], [2, 4]]

Avec copy.deepcopy(), les modifications sur les sous-listes de L1 n’affectent pas L2.

Compréhension de listes

On peut créer une liste de manière concise en utilisant la compréhension de listes. On utilise la syntaxe suivante :

[exp(var) for var in iter]

var est une variable, exp(var) est une expression qui peut (ou peut ne pas) contenir var et iter est un objet itérable (une liste, range, tuple, str, …).

# Compréhension de liste
L = [i for i in range(5)]
print('Compréhension:', L)

# Méthode traditionnelle
L = []
for i in range(5):
    L.append(i)
print('Traditionnelle:', L)
Compréhension: [0, 1, 2, 3, 4]
Traditionnelle: [0, 1, 2, 3, 4]
# Compréhension de liste
L = [2 * i for i in range(5)]
print('Compréhension:', L)

# Méthode traditionnelle
L = []
for i in range(5):
    L.append(2 * i)
print('Traditionnelle:', L)
Compréhension: [0, 2, 4, 6, 8]
Traditionnelle: [0, 2, 4, 6, 8]
# Compréhension de liste
L = [c + 'a' for c in 'pizza']
print('Compréhension:', L)

# Méthode traditionnelle
L = []
for c in 'pizza':
    L.append(c + 'a')
print('Traditionnelle:', L)
Compréhension: ['pa', 'ia', 'za', 'za', 'aa']
Traditionnelle: ['pa', 'ia', 'za', 'za', 'aa']

Compréhension de listes (avec condition)

On peut également ajouter une condition dans la compréhension de listes. La syntaxe devient :

[exp(var) for var in iter if condition(var)]

condition(var) est une expression booléenne qui filtre les éléments de iter.

# Compréhension de liste
L = [x for x in range(10) if x % 2 == 0]
print('Compréhension:', L)

# Méthode traditionnelle
L = []
for x in range(10):
    if x % 2 == 0:
        L.append(x)
print('Traditionnelle:\n', L)
Compréhension: [0, 2, 4, 6, 8]
Traditionnelle:
 [0, 2, 4, 6, 8]
s =  "it's the dance of the ducks"

# Compréhension de liste
voyelles = [c for c in s if c in 'aeiouy']
print('Compréhension:\n', voyelles)

# Méthode traditionnelle
voyelles = []
for c in s:
    if c in 'aeiouy':
        voyelles.append(c)
print('Traditionnelle:\n', voyelles)
Compréhension:
 ['i', 'e', 'a', 'e', 'o', 'e', 'u']
Traditionnelle:
 ['i', 'e', 'a', 'e', 'o', 'e', 'u']

Compréhension de listes (boucles multiples)

On peut également utiliser plusieurs boucles dans une compréhension de listes. La syntaxe est :

[exp(var1, var2) for var1 in iter1 for var2 in iter2]

var1 parcourt iter1 et pour chaque valeur de var1, var2 parcourt iter2.

# Compréhension de liste
voyelles = ['a', 'o']
consonnes = ['n', 'h']
combis = [v + c for v in voyelles for c in consonnes]
print('Compréhension:', combis)

# Méthode traditionnelle
combis = []
for v in voyelles:
    for c in consonnes:
        combis.append(v + c)
print('Traditionnelle:', combis)
Compréhension: ['an', 'ah', 'on', 'oh']
Traditionnelle: ['an', 'ah', 'on', 'oh']
# Compréhension de liste
L1 = [1, 2]
L2 = [10, 20, 30]
produits = [x * y for x in L1 for y in L2]
print('Compréhension:', produits)

# Méthode traditionnelle
produits = []
for x in L1:
    for y in L2:
        produits.append(x * y)
print('Traditionnelle:', produits)
Compréhension: [10, 20, 30, 20, 40, 60]
Traditionnelle: [10, 20, 30, 20, 40, 60]
# Compréhension de liste
coords = [(x, y) for x in range(3) for y in range(2)]
print('Compréhension:', coords)

# Méthode traditionnelle
coords = []
for x in range(3):
    for y in range(2):
        coords.append((x, y))
print('Traditionnelle:', coords)
Compréhension: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
Traditionnelle: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]

Dictionnaires

Nous avons vu que certaines structures en Python sont indexables, c’est-à-dire qu’on peut accéder à l’une de leurs valeurs en les indexant avec un nombre :

agent = ['James', 'Bond', 32]
print('Âge :', agent[2])
Âge : 32

Cependant, il pourrait être très utile d’avoir une structure qui puisse être indexée par des objets qui ne sont pas des nombres. Une telle structure existe en Python : il s’agit de dictionnaires.

  • Un dictionnaire (dict) est une structure de données qui permet d’indexer des objets (les valeurs) non pas avec des entiers 0, 1, 2, … mais avec des clés.
  • Il s’agit d’un ensemble de paires dict = {clé: valeur}.
agent = {'nom': 'Bond', 'prénom': 'James', 'age': 32}
print('Âge :', agent['age'])
Âge : 32

Analogie avec les dictionnaires de langues

  • On peut penser à un dictionnaire de langues: il contient des paires (mot, définition). On peut accéder à la définition en cherchant le mot correspondant dans le dictionnaire.
  • Comme un dictionnaire de langues, un dictionnaire Python stocke des valeurs, (les définitions) indexées par des clés uniques (les mots).
dico = {
  'avion': 'engin volant',
  'voiture': 'engin roulant',
  'sous-marin': 'engin coulant'
}
print(dico['avion'])
engin volant

Opérations sur les dictionnaires

Comme les listes, les dictionnaires sont des objets mutables.

On peut donc:

  • Créer un dictionnaire
    • Effectuer des opérations de lecture:
    • Lire un élément d’un dictionnaire
    • Itérer sur un dictionnaire
  • Effectuer des opérations d’écriture:
    • Modifier un élément d’un dictionnaire
    • Ajouter/enlever un élément d’un dictionnaire

Créer un dictionnaire

On peut créer un dictionnaire :

  • vide, avec une paire d’accolades vide: d = {}
  • contenant déjà des paires (clé, valeur) avec la syntaxe

d = {clé_1:valeur_1, clé_2:valeur_2, ..., clé_n:valeur_n,}

Pour la lisibilité on peut aussi introduire des retours de ligne:

d = {

'nom': 'Dupont',

'prenom': 'Jean',

'age': 25,
}
print(d)
{'nom': 'Dupont', 'prenom': 'Jean', 'age': 25}

Les clés d’un dictionnaire

Les clés peuvent être n’importe quel objet non mutable.

d1 = {1: 123, 2: 345}
d2 = {'a': 123, 'b': 345}
d3 = {(1, 2): 123, (3, 4): 345}
d4 = {True: 123, False: 456}

print(d1[1], d2['a'], d3[(1,2)], d4[True])
123 123 123 123
d = {[1, 2]: 123}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[25], line 1
----> 1 d = {[1, 2]: 123}

TypeError: unhashable type: 'list'

Les clés doivent être uniques. Si on définit plusieurs paires (clé, valeur) avec la même clé, seule la dernière sera gardée.

d = {1: 123, 2: 345, 1: 345}
print(d)
{1: 345, 2: 345}

Accéder à un élément

On peut accéder à l’élément de clé key du dictionnaire d avec la syntaxe d[key], si un tel élément existe:

d = {10: 'poire', 20: 'pomme'}
print(d[10])
poire
d = {10: 'poire', 20: 'pomme'}
print(d[30])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[28], line 2
      1 d = {10: 'poire', 20: 'pomme'}
----> 2 print(d[30])

KeyError: 30

S’il n’y a pas d’élément de clé key dans d, d[key] produira une erreur. Pour éviter cela, on peut exécuter l’instruction d.get(key). La méthode get de la classe dict retourne l’élément de clé key si un tel élément existe, et retourne None autrement.

d = {10: 'poire', 20: 'pomme'}
print(d.get(10))
print(d.get(5))
poire
None

Autres opérations sur les dictionnaires

  • Insérer/modifier : d[key] = val insère une nouvelle paire (key, val) si key n’existe pas, ou modifie la valeur associée à key si elle existe déjà.

    # Insérer un élément
    d = {10: 'poire'}
    d[20] = 'pomme'
    print('Après insertion:\n', d)
    Après insertion:
     {10: 'poire', 20: 'pomme'}
    # Modifier un élément
    d = {10: 'poire', 20: 'pomme'}
    d[10] = 'fraise'
    print('Après modification:\n', d)
    Après modification:
     {10: 'fraise', 20: 'pomme'}
  • Supprimer : del d[key] supprime l’élément de clé key. Produit une erreur si key n’existe pas.

    # Supprimer un élément
    d = {10: 'poire', 20: 'pomme'}
    del d[10]
    print('Après suppression:\n', d)
    Après suppression:
     {20: 'pomme'}
  • Appartenance : key in d vérifie si la clé key existe dans le dictionnaire d.

    # Vérifier l'appartenance
    d = {'pomme': 'malus', 'poire': 'pyrus'}
    print('pomme in d:', 'pomme' in d)
    print('fraise in d:', 'fraise' in d)
    pomme in d: True
    fraise in d: False

Parcourir un dictionnaire

Un dictionnaire est un objet itérable. On peut itérer sur ses clés avec une boucle for.

  • Les éléments du dictionnaire sont alors parcourus dans le même ordre que celui dans lequel il a été rempli.
d = {'Montbenon': 4.2,
     'Les Brasseurs': 3.8,
     'La Bavaria': 5}

for cle in d:
    print(cle)
Montbenon
Les Brasseurs
La Bavaria

Parcourir les clés d’un dictionnaire nous donne accès également aux valeurs associées.

d = {'Montbenon': 4.2,
     'Les Brasseurs': 3.8,
     'La Bavaria': 5}

for cle in d:
    print('d[' + cle + '] =', d[cle])
d[Montbenon] = 4.2
d[Les Brasseurs] = 3.8
d[La Bavaria] = 5

Parcourir un dictionnaire (méthodes avancées)

On peut parcourir un dictionnaire d de différentes manières en utilisant les méthodes :

  • d.keys() : itère sur les clés:

    # Parcourir les clés
    d = {'Montbenon': 4.2,
         'Les Brasseurs': 3.8,
         'La Bavaria': 5}
    
    for nom in d.keys():
        print(nom)
    Montbenon
    Les Brasseurs
    La Bavaria
  • d.values() : itère sur les valeurs:

    # Parcourir les valeurs
    d = {'Montbenon': 4.2,
         'Les Brasseurs': 3.8,
         'La Bavaria': 5}
    
    for note in d.values():
        print(note)
    4.2
    3.8
    5
  • d.items() : itère sur les paires (clé, valeur) sous forme de tuples:

    # Parcourir les paires (clé, valeur)
    d = {'Montbenon': 4.2,
         'Les Brasseurs': 3.8,
         'La Bavaria': 5}
    
    for item in d.items():
        print(item)
    ('Montbenon', 4.2)
    ('Les Brasseurs', 3.8)
    ('La Bavaria', 5)
    # Décomposer et utiliser les paires
    d = {'Montbenon': 4.2,
         'Les Brasseurs': 3.8,
         'La Bavaria': 5}
    
    for nom, note in d.items():
        s = nom + ': ' + str(note) + '/5'
        if note >= 4.0:
            s += ' ⭐'
        print(s)
    Montbenon: 4.2/5 ⭐
    Les Brasseurs: 3.8/5
    La Bavaria: 5/5 ⭐

Remarque. Les objets retournés par .keys(), .values() et .items() sont des objets à part entière (appelés vues), mais ils sont peu utilisés en dehors de l’itération dans des boucles for.

Compréhension de dictionnaire

Comme pour les listes, on peut créer un dictionnaire de manière concise avec une compréhension. La syntaxe est :

{clé: valeur for élément in itérable}

# Compréhension de dictionnaire
d = {i: i**2 for i in range(1, 6)}
print('Compréhension:', d)

# Méthode traditionnelle
d = {}
for i in range(1, 6):
    d[i] = i**2
print('Traditionnelle:', d)
Compréhension: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
Traditionnelle: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# À partir d'une liste
mots = ['chat', 'chien', 'oiseau']

d = {mot: len(mot) for mot in mots}
print(d)
{'chat': 4, 'chien': 5, 'oiseau': 6}
# À partir d'une liste de paires
livres = [
    ('Le Seigneur des Anneaux', 'Frodon'),
    ('1984', 'Winston'),
    ('Les Misérables', 'Jean Valjean')
]

d = {titre: pers for titre, pers in livres}
print(d)
{'Le Seigneur des Anneaux': 'Frodon', '1984': 'Winston', 'Les Misérables': 'Jean Valjean'}

On peut également ajouter une condition avec if pour filtrer les éléments.

# Avec condition
d = {x: x**2 for x in range(1,11) if x % 2 == 0}
print('Carrés des pairs:\n', d)
Carrés des pairs:
 {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}