Partie I - Fondamentaux Python · Chapitre 3

Structures répétitives

3 sections de cours · fiche d'exercices corrigee

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.

Definition — Boucle bornee / non bornee
  • La boucle for parcourt 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 while repete tant qu'une condition reste vraie. Le nombre d'iterations depend des calculs effectues : boucle non bornee. Elle peut ne jamais s'arreter.
Methode — laquelle choisir ?

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
Nombre de termes

\(\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.

Piege

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}")
A retenir

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 :

  1. une initialisation avant la boucle ;
  2. une condition portant sur les variables initialisees ;
  3. une progression dans le corps, qui fait avancer vers l'arret.
Boucle infinie

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
Piege classique

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
Sortie anticipee

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-cleEffet
breakquitte immediatement la boucle la plus interne
continueabandonne le tour courant et passe au suivant
passne 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
La clause 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.

Definition — Variant de boucle

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.

Definition — Invariant de boucle

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.

Exemple complet — division euclidienne
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.
Trace pas a pas d'une boucle

Choisir un algorithme, puis derouler les iterations une par une. L'etat des variables est affiche a chaque etape.

tour 0
Choisir un algorithme puis cliquer sur « Initialiser ».

L'essentiel du chapitre

  • for quand le nombre de tours est connu d'avance, while sinon.
  • La borne superieure de range est exclue ; range est paresseux.
  • Trois motifs a maitriser : accumulation, recherche d'extremum, sortie anticipee.
  • La clause else d'une boucle s'execute si aucun break n'a eu lieu.
  • Un test de primalite s'arrete a \(\sqrt n\) : complexite \(\Theta(\sqrt n)\).
  • Une boucle while se justifie par un variant (terminaison) et un invariant (correction).
Fiche d'exercices — chapitre 3Enonces gradues, du calcul d'application au niveau concours, avec corriges detailles.
Ouvrir la fiche →