Parcourir, répéter : cours

\(\newcommand{\ds}{\displaystyle}\) \(\newcommand{\Frac}{\ds\frac}\) \(\renewcommand{\r}{\mathbb{ R}}\) \(\newcommand{\C}{\mathbb{ C}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\z}{\mathbb{ Z}}\) \(\newcommand{\Q}{\mathbb{ Q}}\) \(\newcommand{\N}{\mathbb{ N}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\ol}{\overline}\) \(\newcommand{\abs}[1]{\left| \,{#1} \right|}\) \(\newcommand{\pv}{\;;\;}\) \(\newcommand{\ens}[1]{\left\{ {#1} \right\}}\) \(\newcommand{\mens}[1]{\setminus\left\{ {#1} \right\}}\) \(\newcommand{\Par}[1]{\left({#1}\right)}\) \(\newcommand{\pe}[1]{\left\lfloor {#1} \right\rfloor}\) \(\newcommand{\trans}[1]{\,^t\!{#1}}\)

Parcourir, répéter : cours

Cours

append : adjoindre un élément à une liste

La méthode append modifie une liste en lui ajoutant un élément à sa fin. Exemple :

1
2
3
4
5
6
7
L = [65, 31, 9]
print(L)
print("taille de L :", len(L))
print()
L.append(81)
print(L)
print("taille de L :", len(L))
 8
 9
10
11
12
[65, 31, 9]
taille de L : 3

[65, 31, 9, 81]
taille de L : 4
  • Lignes 5, 11 : l’élément 81 est ajouté à la fin de la liste.
  • Lignes 9, 12 : on voit aussi que la taille de la liste a changé.
  • Lignes 5 et 6 : bien qu’ayant changée, la liste est toujours référencée par L.

On notera la syntaxe particulière de append utilisant un point (« la notation suffixe ») : chaque liste L admet un « attribut » append que l’on obtient par L.append et qui s’utilise comme une fonction.

La méthode append ne renvoie rien :

1
2
3
4
5
L = [65, 31, 9]
print(L)
x = L.append(81)
print(L)
print(x)
6
7
8
[65, 31, 9]
[65, 31, 9, 81]
None
  • Ligne 3 : le membre de droite est un appel de la méthode append. Le membre de gauche référence le retour de cet appel.
  • Ligne 8 : on observe qu’une fois l’appel à append effectué, x vaut None autrement dit rien.

Ainsi, contrairement à ce que l’on pourrait imaginer, L.append(truc) ne représente pas la liste L complétée par truc.

Adjoindre plusieurs éléments

Un unique appel à la méthode append ne permet pas d’ajouter plusieurs éléments. Pour ajouter plusieurs éléments, il faut appeler autant de fois la méthode append :

1
2
3
4
L = [65, 31, 9]
L.append(81)
L.append(100)
print(L)
5
[65, 31, 9, 81, 100]

Parcours complet d’une liste sans indice

Soit une liste de nombres, comme L = [65, 31, 9, 32, 81, 82, 46, 12]. On cherche à afficher le produit \(\mathtt{10\times z}\) pour chaque terme \(\mathtt{z}\) de la liste. La façon la plus simple d’y parvenir en Python est :

1
2
3
4
L = [65, 31, 9, 32, 81, 82, 46, 12]

for z in L:
    print(10 * z)
 5
 6
 7
 8
 9
10
11
12
650
310
90
320
810
820
460
120

On observera qu’on accède aux éléments de la liste sans utiliser d’indice et juste en nommant l’élément courant. Cette syntaxe s’applique chaque fois qu’on cherche à lire successivement la totalité des éléments d’une liste. C’est un idiome de la programmation Python.

Cette syntaxe ne sera pas adaptée si :

  • on veut modifier la liste
  • si on ne veut lire qu’à certaines positions définies par un indice de la liste.

Beaucoup de débutants confondent le parcours d’une liste sans indice ou avec indice. Ce dernier parcours est considéré, dans certains cas, comme non pythonnique mais il a l’avantage de s’appliquer à de nombreuses situations.

Parcours complet d’une liste par indices

Comme on peut accéder à chaque élément d’une liste de \(n\) éléments par son indice \(i\) avec \(0\leq i < n\), un usage très fréquent de la boucle for est le parcours d’une liste par ses indices. C’est un idiome de la programmation impérative.

Par exemple, soit une liste de nombres, comme t = [65, 31, 9, 32, 81, 82, 46, 12]. On cherche à afficher le produit \(\mathtt{10\times x}\) pour chaque terme \(\mathtt{x}\) de la liste :

1
2
3
4
t = [65, 31, 9, 32, 81, 82, 46, 12]

for i in range(8):
    print(10 * t[i])
 5
 6
 7
 8
 9
10
11
12
650
310
90
320
810
820
460
120
  • Ligne 1: t contient 8 éléments donc pour parcourir t avec un indice i, on utilise l’en-tête for i in range(8)
  • Ligne 4 : t[i] permet d’accéder à l’élément courant de t.

Code amélioré

Pour une meilleure lisibilité et une meilleure maintenance, un des deux codes ci-dessous est nettement préférable au code précédent :

1
2
3
4
5
t = [65, 31, 9, 32, 81, 82, 46, 12]
n = len(t)

for i in range(n):
    print(10 * t[i])
6
7
8
9
t = [65, 31, 9, 32, 81, 82, 46, 12]

for i in range(len(t)):
    print(10 * t[i])
  • Ligne 2 ou ligne 8 : la longueur de t n’est pas écrite littéralement : si dans le code ligne 1, on modifie t en rajoutant un élément, le code en-dessous n’a pas besoin d’être modifié.

Boucle for sans indice et calcul de somme

La technique de la variable accumulatrice permet de calculer la somme des termes d’une liste sans utiliser d’indice des éléments de la liste :

1
2
3
4
5
6
7
t = [10, 3, 12, 5]
s=0

for x in t:
    s = s + x

print(s)
8
30
  • variable accumulatrice initialisée qui va accumuler les termes de la liste t et fournir la somme cherchée.
  • La liste est parcourue et à chaque itération, le terme courant de la liste est ajouté à s et qui est mis-à-jour.

Boucle for : décompression de la variable de contrôle

Le problème suivant illustre un usage de la syntaxe de décompression d’une variable de contrôle d’une boucle for.

On dispose d’une suite de couples d’entiers, par exemple :

1
[81, 65], [32, 31], [31, 9], [82, 32], [12, 81]

Pour chaque couple, on veut calculer la somme des deux entiers. On peut procéder ainsi :

1
2
3
4
t = [[81, 65], [32, 31], [31, 9], [82, 32], [12, 81]]

for a,b in t:
    print(a+b)
5
6
7
8
9
146
63
40
114
93
  • Ligne 3 : au lieu de parcourir la liste t avec une variable unique x et qui serait une liste à deux éléments (comme les éléments de t), on extrait les éléments de cette directement à l’aide de variables a et b les deux élements de l’élément courant de la liste.

Le fait de placer directement dans des variables les éléments d’une liste s’appelle la décompression ou le décompactage (unpacking en anglais) de la liste.

Exercice type : Somme des positifs

On donne une liste L d’entiers et on demande de calculer la somme s des éléments positifs de cette liste. Si aucun élément de la liste n’est positif, on conviendra que la somme s vaut 0. Exemples

1
2
3
L = [-2, 6, 0, -1, 4, 5, -3] -> s = 15
L = [-2, -3] -> s = 0
L = [42] -> s = 42

Solution

Le principe de calcul est le suivant, il s’agit d’une légère variante du calcul d’une somme : on parcourt la liste et on ajoute à une variable accumulatrice s initialisée à 0 chaque terme de la liste qui est strictement positif :

1
2
3
4
5
6
7
8
L=[-2, 6, 0, -1, 4, 5, -3]
s=0

for v in L:
    if v > 0:
        s = s + v

print(L, "->",s)
9
[-2, 6, 0, -1, 4, 5, -3] -> 15

Exercice type : Somme sauf des extrémités

On donne une liste L d’entiers. Calculer la somme s des éléments de L en excluant du calcul de la somme le premier et le dernier élément de L. Par exemple, si L = [310, 12, 8100, 90, 31] alors s = 8202.

Solution

Il va falloir parcourir la liste mais comme le résultat demandé dépend de la position particulière de l’élément dans la liste, on utilise un parcours de la liste avec indice.

La solution qui peut venir spontanément à l’esprit est de parcourir la liste avec l’idiome habituel et de filtrer avec une instruction if pour savoir si le terme courant est au début ainsi qu’à la fin et ainsi exclure ces termes de la somme. Ce qui donnerait le code suivant :

1
2
3
4
5
6
7
8
L =  [310, 12, 8100, 90, 31]
n=len(L)

s=0
for i in range(n):
    if i != 0 and i != n -1:
     s=s+L[i]
print(s)
9
8202

C’est bien le résultat attendu mais le code est particulièrement maladroit. En effet, l’instruction if ne sert que deux fois sur toute la liste et à des endroits prévisibles. Or, le début de la liste est à l’indice 0 et la fin à l’indice n-1n = len(L). Par suite, il suffit juste d’itérer entre les indices 1 et n - 2. D’où le code :

1
2
3
4
5
L =  [310, 12, 8100, 90, 31]
s=0
for i in range(1,len(L)-1):
     s=s+L[i]
print(s)
6
8202

Des mesures de vitesse d’exécution montreraient que le premier code à une pénalité de \(\mathtt{10\%}\) par rapport au second.

Il serait possible de ne pas utiliser d’indice en ayant des connaissances supplémentaires ou en simulant un indice. Ou encore, il était possible de faire la somme de tous les éléments et de retirer le premier et le dernier du résultat :

1
2
3
4
5
6
7
8
L =  [310, 12, 8100, 90, 31]

s=0
for z in L:
    s = s + z

s=s-L[0]-L[len(L)-1]
print(s)
9
8202

Boucle for, filtrage et comptage

Étant donné une liste L d’entiers, soit à afficher les entiers pairs de L ; dans le jargon, on dit qu’on effectue un filtrage des éléments de L, le filtre étant ici le caractère pair de l’élément de la liste. Voici un code répondant au problème :

1
2
3
4
5
L = [65, 31, 9, 32, 81, 82, 46, 12]

for z in L:
    if z % 2 == 0:
        print(z)
6
7
8
9
32
82
46
12
  • Lignes 3-5 : parcours de la liste
  • Ligne 4 : filtrage avec une instruction if

Comptage

Pour un programme de comptage, on crée à l’aide d’une variable un compteur. La variable porte souvent le nom de cpt ou encore (en anglais) cnt. Le plus souvent la variable est initialisée à 0. Le comptage est souvent associé à un filtre qui indique ce que l’on compte. Par exemple, soit à compter les entiers pairs d’une liste donnée :

1
2
3
4
5
6
7
8
9
L = [65, 31, 9, 32, 81, 82, 46, 12]

cpt=0

for z in L:
    if z % 2 == 0:
        cpt = cpt + 1

print(cpt)
10
4
  • Ligne 3 : initialisation d’un compteur.
  • Lignes 5-7 : parcours de la liste.
  • Ligne 6 : filtrage.
  • Ligne 7 : incrémentation du compteur lorsque l’élément est filtré.

Boucle for et calcul de maximum

La technique de la « variable accumulatrice » peut s’adapter au calcul du plus grand élément des termes d’une liste :

1
2
3
4
5
6
7
8
9
L = [10, 3, 12, 5]
maxi = L[0]
n = len(L)

for i in range(1, n):
    if L[i] > maxi:
        maxi = L[i]

print(maxi)
10
12
  • Ligne 1 : on observe que le plus grand élément de L est 12.
  • Ligne 2 : variable initialisée au premier terme de la liste et qui, en fin de programme, contiendra le maximum la liste L.
  • Lignes 5-7 : la liste est parcourue et à la fin de chaque itération, maxi est le plus grand de tous les termes examinés.
  • Ligne 5 : le range commence à 1 car comme maxi est initialisé à L[0], il est inutile d’effectuer la comparaison pour i = 0 puisque L[0] > maxi (cf. ligne 6) autrement dit L[0] > L[0] vaut False.

Même si ce n’est pas formellement interdit, on évitera d’appeler max la variable accumulatrice du maximum car cela empêcherait d’utiliser la fonction Python native nommé max. De même on évitera le nom min pour un minimum.

Il aurait été possible de ne pas utiliser d’indice pour parcourir la liste :

1
2
3
4
5
6
7
8
9
L = [10, 3, 12, 5]
maxi = L[0]
n = len(L)

for z in L:
    if z > maxi:
        maxi = z

print(maxi)

Le seul inconvénient est que le premier passage dans l’instruction if compare maxi avec lui-même.

Parcours de liste : indices voisins de l’indice courant

L’avantage de parcourir une liste via ses indices est que l’on peut accéder à l’indice suivant ou l’indice précédent. Par exemple, soit à afficher toutes les sommes de deux termes successifs de la liste t = [65, 31, 9, 32, 81, 82, 46, 12], à savoir les sommes suivantes :

96, 40, 41, etc.

L’algorithme de base consiste à parcourir t jusqu’à son avant-dernier terme, et à afficher \(x+y\)\(x\) est le terme courant de t et \(y\) le terme suivant dans t :

1
2
3
4
5
t = [65, 31, 9, 32, 81, 82, 46, 12]
n= len(t)

for i in range(n-1):
    print(t[i] + t[i+1])
 6
 7
 8
 9
10
11
12
96
40
41
113
163
128
58
  • Ligne 4 : Le choix de (n-1) se justifie par le fait que le parcours de t s’arrête à l’avant-dernier terme de t puisqu’il faut à chaque étape du parcours considérer le terme suivant.
  • Ligne 5 : t[i+1] est le terme suivant du terme courant, ce qui suppose que \(i+1<n\) et donc que \(i< n-1\)
  • Ligne 4 : écrire range(n) au lieu de range(n-1) entraînerait une erreur (un débordement d’indice) lorsque \(i=n-1\) et que l’interpréteur calcule t[i+1].

La technique du drapeau dans une boucle for

Soit à définir un code Python qui partant d’une liste L d’entiers détermine si L ne contient que des entiers positifs. Plus précisément, le code doit définir un booléen nommé tousPositifs qui vaut True si L ne contient que des entiers positifs et False sinon.

Le code suivant réalise cette tâche sur deux exemples de liste :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1er exemple
L= [31, 82, -42, 32, -81]

tousPositifs = True
for z in L:
    if z < 0:
        tousPositifs = False
print(L, '->', tousPositifs)

print("---------------------")

# 2e exemple
L= [31, 82, 421, 32, 81]

tousPositifs = True
for z in L:
    if z < 0:
        tousPositifs = False
print(L, '->', tousPositifs)
20
21
22
[31, 82, -42, 32, -81] -> False
---------------------
[31, 82, 421, 32, 81] -> True
  • Le même code (lignes 4-8 et lignes 15-19) est testé avec deux listes différentes (lignes 2 et 13). Ci-dessous, les commentaires ne portent que sur le premier groupe de code.
  • Dans chaque cas, on affiche la liste L (lignes 8) et le booléen tousPositifs.
  • Lignes 4 : on définit au début du code un « drapeau », ici le booléen tousPositifs ; ce drapeau témoigne d’un état, l’état étant que la liste L contienne uniquement des nombres positifs (True) ou non (False). Au départ, le drapeau tousPositifs est placé sur True.
  • lignes 6-7 : La liste L est parcourue et si au cours du parcours de L, la présence dans la liste d’un entier strictement négatif est détectée (ligne 6), le drapeau change de couleur, ici il passe à False (ligne 7). Le drapeau ne change que si un entier négatif est détecté, autrement dit le if n’est pas couplé à un else.
  • Ligne 5-8 : si le drapeau change par rapport à son état initial, c’est qu’un entier négatif a été détecté dans la liste L et donc il est attendu que le drapeau tousPositifs vaille False, ce qui est bien le cas, cf. ligne 20.
  • Lignes 15-18 : si au contraire, la liste L est parcourue sans que le drapeau ait changé, c’est qu’aucun entier négatif n’est jamais rencontré et donc il est attendu que le drapeau tousPositifs vaille True, ce qui est bien le cas, cf. ligne 22.

Ainsi le code ci-dessus réalise bien la tâche attendue.

Initialisation du drapeau

Comment est déterminée la valeur initiale du drapeau ? Réponse : en fonction de ce que représente le booléen une fois toute la liste parcourue. Si le booléen True doit correspondre au fait que tous les entiers sont positifs, c’est que le booléen doit changer en False dans la boucle quand un entier négatif est détecté et donc, le booléen doit être initialisé à True. Il serait possible de réaliser le même objectif en utilisant un autre drapeau initialisé à False :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
L= [31, 82, -42, 32, -81]

contientNegatif = False
for z in L:
    if z < 0:
        contientNegatif = True
print(L, '->', contientNegatif)

print("=========================")

L= [31, 82, 421, 32, 81]

contientNegatif = False
for z in L:
    if z < 0:
        contientNegatif = True
print(L, '->', contientNegatif)
18
19
20
[31, 82, -42, 32, -81] -> True
=========================
[31, 82, 421, 32, 81] -> False
  • Ligne 3 : on définit un drapeau contientNegatif qui vaudra True si L contient au moins un entier strictement négatif et False sinon.
  • Lignes 5-6 : lorsqu’on parcourt la liste, le drapeau doit passer à True si un entier strictement négatif apparaît, donc le drapeau doit être initialisé à False.

Exercice type : Alternance de parité

On donne une liste L d’entiers et on demande de créer un booléen alterneParite valant True si les éléments de L se suivent en alternant de parité et False sinon. Voici des exemples de comportements attendus :

L Alternance de parité
[81, 32, 9, 12] True
[32, 9, 32, 65] True
[32, 9, 31, 82] False
[81] True

Solution

Il s’agit d’appliquer la technique du drapeau. Il faut parcourir la liste de L. Mais comme il faut à chaque étape comparer les parités de deux éléments consécutifs, la boucle de parcours a la forme avec indice suivante :

n=len(L)

for i in range(n-1):
    # Code à compléter

La parité d’un entier a est donnée par le reste a % 2. Donc, deux éléments consécutifs de L ont des parités différentes si L[i] % 2 != L[i+1] % 2. Dans le cas contraire, le drapeau de surveillance de parité doit changer. D’où le code suivant :

1
2
3
4
5
6
7
8
L = [81, 32, 9, 12]
alterneParite = True

for i in range(0,len(L)-1):
    if L[i] % 2 == L[i+1] % 2:
        alterneParite = False

print(alterneParite)
9
True

Testons plusieurs listes placées dans une liste tests :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
tests = [[81, 32, 9, 12],[32, 9, 32, 65], [32, 9, 31, 82],[81]]

for L in tests:

    alterneParite = True

    for i in range(0,len(L)-1):
        if L[i] % 2 == L[i+1] % 2:
            alterneParite = False

    print(L, "->", alterneParite)
12
13
14
15
[81, 32, 9, 12] -> True
[32, 9, 32, 65] -> True
[32, 9, 31, 82] -> False
[81] -> True

On pourra remarquer que le code

1
2
3
4
5
6
7
8
L = [81, 32, 9, 12]
alterneParite = True

for i in range(0,len(L)-1):
    if L[i] % 2 == L[i+1] % 2:
        alterneParite = False

print(alterneParite)

n’est pas parfaitement optimal, déjà parce que la technique du drapeau n’est pas optimale et aussi parce que la parité de chaque élément de la liste ayant un voisin est évaluée deux fois (cf. ligne 5).

Liste construite depuis la liste vide

Pour créer une liste d’éléments vérifiant une certaine propriété P, on procède souvent ainsi :

  • on crée une liste L, initialement vide,
  • on garnit successivement L d’éléments vérifiant P en utilisant la méthode append.

Exemple

Étant donné une liste t d’entiers, on veut extraire de t la liste L des entiers \(x\) tels que \(x\geq 42\).

Solution :

1
2
3
4
5
6
t = [65, 31, 9, 32, 81, 82, 46, 12]
L= []
for z in t:
    if z >= 42:
        L.append(z)
print(L)
7
[65, 81, 82, 46]
  • Ligne 2 : on crée une liste vide à laquelle on va adjoindre les éléments de L qui conviennent.
  • Lignes 3-5 : on parcourt la liste t.
  • Ligne 4 : on teste si l’élément courant vérifie la condition P.
  • Lignes 4-5 : si un élément la vérifie, l’élément est placé à la fin de la liste L.

Construction de petites listes

Pour construire de petites listes, ayant jusqu’à 3 ou 4 éléments, il est souvent plus simple de l’écrire directement avec ses éléments entre crochets plutôt que de la construire depuis la liste vide et après applications répétées de la méthode append.

Par exemple, on dispose de deux variables entières u et v et on veut regrouper dans une liste L ces éléments en sorte que l’élément ayant la plus petite valeur soit au début de la liste, ou ne mettre qu’un seul élément dans la liste si les deux éléments sont identiques. Par exemple,

  • si u = 42 et v = 42 alors L = [42] ;
  • si u = 81 et v = 42 alors L = [42, 81].

Le problème peut se résoudre ainsi :

u = 81
v = 42

if u == v:
    L = [u]
else:
    if u < v:
        L = [u, v]
    else:
        L = [v, u]

Il est maladroit d’utiliser append comme dans le code ci-dessous :

u = 81
v = 42

L = []

if u == v:
    L.append(u)
else:
    if u < v:
        L.append(u)
        L.append(v)
    else:
        L.append(v)
        L.append(u)

print(L)

Exercice type : Liste des \(N\) premiers multiples de \(d\)

Soient N et d deux entiers positifs. Créer la liste L contenant les N premiers multiples de d en commençant par d. Par exemple, si N = 7 et d = 10 alors L = [10, 20, 30, 40, 50, 60, 70].

Solution

1
2
3
4
5
6
N=7
d=10
L=[]
for i in range(1,N+1):
    L.append(i*d)
print(L)