1Boucles bornees et non bornees
Repeter un traitement est le second mecanisme de controle. Python offre deux boucles de natures differentes, et savoir laquelle choisir est deja la moitie du travail.
- La boucle
forparcourt les elements d'un objet iterable. Le nombre d'iterations est connu avant d'entrer dans la boucle : on parle de boucle bornee. - La boucle
whilerepete tant qu'une condition reste vraie. Le nombre d'iterations depend des calculs effectues : boucle non bornee. Elle peut ne jamais s'arreter.
Si l'on peut repondre a la question « combien de tours ? » avant de commencer, on
ecrit un for. Sinon (recherche d'un seuil, convergence, saisie utilisateur,
dichotomie), on ecrit un while. Utiliser un while la ou un
for convient est une faute de style ; l'inverse est souvent un bug.
La fonction range
range(n) # 0, 1, ..., n-1 (n valeurs)
range(a, b) # a, a+1, ..., b-1 (b-a valeurs si b > a)
range(a, b, p) # a, a+p, a+2p, ... tant que l'on reste avant b
La borne superieure est toujours exclue. Le pas p peut etre
negatif.
list(range(5)) # [0, 1, 2, 3, 4]
list(range(2, 9, 3)) # [2, 5, 8]
list(range(10, 0, -2)) # [10, 8, 6, 4, 2]
list(range(5, 5)) # [] borne atteinte : aucun tour
list(range(5, 0)) # [] pas de tour, et surtout pas d'erreur
\(\texttt{range}(a, b, p)\) avec \(p > 0\) contient
\(\max\!\left(0, \left\lceil \dfrac{b-a}{p} \right\rceil\right)\) valeurs. En particulier
range(a, b) en contient \(b - a\) si \(b \ge a\). C'est le calcul a faire pour
determiner le nombre d'iterations d'une boucle for.
range ne construit pas une liste : c'est un objet paresseux qui produit les
valeurs a la demande. range(10**12) est instantane et ne consomme aucune memoire,
alors que list(range(10**12)) saturerait la machine. Ne convertir en liste que si
l'on a vraiment besoin de la liste.
La boucle for
for i in range(1, 6):
print(i, i*i)
for c in "MPSI": # une chaine est iterable
print(c)
for x in [3, 1, 4, 1, 5]: # une liste aussi
print(x)
Trois motifs de parcours a connaitre par coeur :
t = [10, 20, 30]
for x in t: # par ELEMENT : quand l'indice est inutile
print(x)
for i in range(len(t)): # par INDICE : quand on doit modifier t
t[i] = t[i] * 2
for i, x in enumerate(t): # les DEUX a la fois
print(f"t[{i}] = {x}")
Parcourir par element quand on ne fait que lire, par indice quand on doit ecrire dans la
sequence. for i in range(len(t)) alors qu'on ne se sert que de t[i]
est maladroit ; enumerate evite d'avoir a choisir.
La boucle while
n = 100
compteur = 0
while n > 1: # condition testee AVANT chaque tour
n = n // 2
compteur += 1
print(compteur) # 6 : nombre de divisions par 2 pour atteindre 1
Le squelette d'une boucle while correcte comporte toujours trois elements :
- une initialisation avant la boucle ;
- une condition portant sur les variables initialisees ;
- une progression dans le corps, qui fait avancer vers l'arret.
Oublier la progression donne une boucle qui ne s'arrete jamais. Le programme semble fige ; Ctrl+C l'interrompt.
i = 0
while i < 10:
print(i) # i n'est jamais modifie : boucle infinie
Un while dont le corps ne modifie aucune variable de la condition est
toujours un bug.
2Motifs algorithmiques et controle de boucle
Accumulation
Le motif le plus frequent : une variable est initialisee a l'element neutre, puis enrichie a chaque tour.
def somme_carres(n):
"""Somme des carres des entiers de 1 a n."""
s = 0 # element neutre de l'addition
for k in range(1, n+1):
s += k*k
return s
def factorielle(n):
"""n! pour n >= 0."""
p = 1 # element neutre de la multiplication
for k in range(2, n+1):
p *= k
return p
assert somme_carres(3) == 14
assert factorielle(0) == 1 and factorielle(5) == 120
Verification : \(\sum_{k=1}^{n} k^2 = \frac{n(n+1)(2n+1)}{6}\), soit 14 pour \(n=3\).
Recherche d'un extremum
def maximum(t):
"""Plus grand element de la liste non vide t, sans utiliser max."""
m = t[0] # initialiser avec un element de la liste !
for x in t[1:]:
if x > m:
m = x
return m
def indice_du_max(t):
"""Indice du premier maximum de la liste non vide t."""
imax = 0
for i in range(1, len(t)):
if t[i] > t[imax]:
imax = i
return imax
Initialiser m = 0 est faux si tous les elements sont negatifs. On initialise
toujours avec t[0] (et l'on documente la precondition « liste non vide »), ou avec
float('-inf').
Comptage et test d'existence
def compte(t, v):
"""Nombre d'occurrences de v dans t."""
c = 0
for x in t:
if x == v:
c += 1
return c
def appartient(t, v):
"""v figure-t-il dans t ? Sortie anticipee."""
for x in t:
if x == v:
return True # inutile de continuer
return False # atteint seulement si la boucle est finie
Le return dans la boucle est la facon la plus lisible de s'arreter des que la
reponse est connue. Le return False final doit etre hors de la boucle :
place a l'interieur, il ferait echouer la fonction des le premier element different.
break, continue et la clause else
| Mot-cle | Effet |
|---|---|
break | quitte immediatement la boucle la plus interne |
continue | abandonne le tour courant et passe au suivant |
pass | ne fait rien (bloc syntaxiquement necessaire) |
for k in range(2, 20):
if k % 3 == 0:
continue # on saute les multiples de 3
if k > 10:
break # on arrete completement
print(k, end=" ") # 2 4 5 7 8 10
else d'une boucle
Le bloc else d'une boucle s'execute si et seulement si la boucle s'est
terminee normalement, c'est-a-dire sans break. C'est exactement ce qu'il
faut pour un test de primalite.
def est_premier(n):
"""n >= 2 est-il premier ?"""
if n < 2:
return False
for d in range(2, int(n**0.5) + 1):
if n % d == 0:
return False # diviseur trouve
return True # aucun diviseur : n est premier
On s'arrete a \(\lfloor\sqrt n\rfloor\) car si \(n = ab\) avec \(1 < a \le b\), alors \(a^2 \le ab = n\) donc \(a \le \sqrt n\) : le plus petit diviseur non trivial est toujours \(\le \sqrt n\). Cela fait passer la complexite de \(\Theta(n)\) a \(\Theta(\sqrt n)\).
Boucles imbriquees
for i in range(1, 4):
for j in range(1, 4):
print(f"{i*j:3}", end="")
print() # retour a la ligne apres chaque ligne du tableau
1 2 3
2 4 6
3 6 9
Le corps interne est execute \(3 \times 3 = 9\) fois. En general, deux boucles imbriquees de \(n\) tours chacune donnent \(n^2\) executions : c'est l'origine des complexites quadratiques des tris elementaires (chapitre 12).
# triangle de Pascal, motif de boucle "triangulaire"
n = 5
for i in range(n):
for j in range(i + 1): # borne dependant de i
print("*", end="")
print()
Ici le nombre total d'executions vaut \(1+2+\cdots+n = \frac{n(n+1)}{2}\), toujours quadratique en \(n\), mais avec une constante deux fois plus petite.
3Variant, invariant et trace interactive
Une boucle while n'est pas evidemment correcte : il faut prouver qu'elle
s'arrete et qu'elle calcule ce qu'on veut. Ces deux notions, developpees au
chapitre 15, s'introduisent naturellement ici.
Un variant est une quantite a valeurs dans \(\mathbb{N}\), calculee a partir des variables du programme, qui decroit strictement a chaque tour. Comme il n'existe pas de suite d'entiers naturels infinie strictement decroissante, son existence prouve la terminaison.
Un invariant est une propriete vraie avant l'entree dans la boucle et preservee par chaque tour. A la sortie, invariant et negation de la condition sont vrais simultanement : c'est ce qui donne la correction.
def division(a, b):
"""Quotient et reste de la division euclidienne de a >= 0 par b > 0."""
q, r = 0, a
while r >= b:
r = r - b
q = q + 1
return q, r
Terminaison. On prend \(v = r\). C'est un entier, positif tant que la boucle tourne (puisque \(r \ge b > 0\) a l'entree du tour). Il decroit strictement car \(r_{\text{nouveau}} = r - b \le r - 1 < r\). Donc la boucle termine.
Correction. Invariant : \(a = bq + r \ \wedge\ r \ge 0\).
- Initialisation : \(q=0\), \(r=a\) donne \(a = b\cdot 0 + a\) et \(a \ge 0\). Vrai.
- Conservation : si \(a = bq+r\), alors apres le tour \(bq' + r' = b(q+1) + (r-b) = bq + r = a\), et \(r' = r-b \ge 0\) car la condition \(r \ge b\) etait vraie. L'invariant est preserve.
- Terminaison de la preuve : a la sortie, l'invariant donne \(a = bq+r\) avec \(r \ge 0\), et la negation de la condition donne \(r < b\). C'est exactement la caracterisation du couple (quotient, reste). La fonction est correcte.
Choisir un algorithme, puis derouler les iterations une par une. L'etat des variables est affiche a chaque etape.
L'essentiel du chapitre
forquand le nombre de tours est connu d'avance,whilesinon.- La borne superieure de
rangeest exclue ;rangeest paresseux. - Trois motifs a maitriser : accumulation, recherche d'extremum, sortie anticipee.
- La clause
elsed'une boucle s'execute si aucunbreakn'a eu lieu. - Un test de primalite s'arrete a \(\sqrt n\) : complexite \(\Theta(\sqrt n)\).
- Une boucle
whilese justifie par un variant (terminaison) et un invariant (correction).