Licence CC BY-NC-ND, Thierry Parmentelat

from IPython.display import HTML
HTML(filename="_static/style.html")

itér.. (1/3) - for, itertools#

  • la boucle for est la méthode préférée pour itérer sur un ensemble de valeurs

  • en général préférable au while en Python

    • on peut faire un for sur n’importe quel itérable

    • ce n’est pas le cas pour le while

    • avec for c’est l’itérable qui se charge de la logique

  • et aussi de nombreuses techniques pour itérer de manière optimisée

    • compréhensions

    • itérateurs

    • expressions génératrices

    • générateurs (encore appelées fonctions génératrices)

  • attention / rappel : avec numpy, pas de for, programmation vectorielle

la boucle for#

une instruction for ressemble à ceci :

for item in iterable:
    bloc
    aligné
    d_instructions

break et continue#

comme dans beaucoup d’autres langages, et comme pour le while :

  • break sort complètement de la boucle

  • continue termine abruptement
    l’itération courante et passe à la suivante

for .. else#

en fait la forme générale de la boucle for c’est

for item in iterable:
    bloc
    aligné
else:
    bloc     # exécuté lorsque la boucle sort "proprement"
    aligné   # c'est-à-dire pas avec un break

c’est assez rare

l’instruction else attachée à un for est d’un usage plutôt rare en pratique

bouh c’est vilain !#

dès que vous voyez for i in range(len(truc)) vous devez vous dire qu’il y a mieux à faire:

liste = [10, 20, 40, 80, 120]

# la bonne façon de faire un for

for item in liste:
    print(item, end=" ")
10 20 40 80 120 
# et **non pas** cette
# horrible périphrase !

for i in range(len(liste)):
    item = liste[i]
    print(item, end=" ")
10 20 40 80 120 

boucle for sur un dictionnaire#

  • rappel: on peut facilement itérer sur un dictionnaire

  • la plupart du temps, sur à la fois clés et valeurs for k, v in d.items():

  • pour itérer sur les clés, restons simple: for k in d:

  • enfin sur les valeurs for v in d.values():

remarque

on peut aussi itérer sur les clés avec for k in d.keys(), mais c’est moche..

agenda = {
    'paul': 12, 
    'pierre': 14,
    'jean': 16,
}
# l'unpacking permet d'écrire 
# un code élégant
for key, value in agenda.items():
    print(f"{key}{value}")
paul → 12
pierre → 14
jean → 16

boucles for : limite importante#

  • règle très importante: à l’intérieur d’une boucle

  • il ne faut pas modifier l’objet sur lequel on itère

s = {1, 2, 3}

# on essaie de modifier l'objet itéré
try:
    for x in s:
        if x == 1:
            s.remove(x)
except Exception as exc:
    print(f"OOPS {type(exc)} {exc}")
OOPS <class 'RuntimeError'> Set changed size during iteration

la technique usuelle consiste à utiliser une copie

s = {1, 2, 3}

# avec les listes on peut aussi utiliser [:]
# mais ici sur un ensemble ça ne fonctionnerait pas 
for x in s.copy():
    if x == 1:
        s.remove(x)

s
{2, 3}

question de style#

rappelez-vous qu’on peut unpack dans un for; ça permet souvent d’utiliser des noms de variables explicites

D = {'alice': 35, 'bob': 9, 'charlie': 6}
# pas pythonique (implicite)

for t in D.items():
    print(t[0], t[1])
alice 35
bob 9
charlie 6
# pythonique (explicite)

for nom, age in D.items():
    print(nom, age)
alice 35
bob 9
charlie 6

itérables et itérateurs#

c’est quoi un itérable ?#

  • par définition, c’est un objet .. sur lequel on peut faire un for

  • notamment avec les séquences natives : chaînes, listes, tuples, ensembles

  • et aussi dictionnaires, et des tas d’autres objets, mais patience

# une chaine est un itérable

chaine = "un été"
for char in chaine:
    print(char, end=" ")
u n   é t é 
# un ensemble aussi

ensemble = {10, 40, 80} 
for element in ensemble:
    print(element, end=" ")
40 10 80 

la boucle for, mais pas que#

  • on a défini les itérables par rapport à la boucle for

  • mais plusieurs fonctions acceptent en argument des itérables

  • sum, max, min

  • map, filter

  • etc…

L = [20, 34, 57, 2, 25]

min(L), sum(L)
(2, 138)
# ceci retourne un itérateur
map(lambda x: x**2, L)
<map at 0x7f4a7a390310>
# pour voir "ce qu'il y a dedans"
list(map(lambda x: x**2, L))
[400, 1156, 3249, 4, 625]

itérateurs#

  • les itérateurs sont une sous-famille des itérables

  • qui présentent la particularité de consommer peu de mémoire

  • en fait un objet itérateur capture uniquement la logique de l’itération, mais pas les données

  • c’est-à-dire où on en est, et comment passer au suivant

import sys

L = list(range(1000))
sys.getsizeof(L)
8056
# avec iter() on fabrique 
# un itérateur
I = iter(L)

sys.getsizeof(I)
48

cette boucle Python

for i in range(100_000):
    # do stuff

est comparable à ceci en C

for (int i=0; 
     i<100000; 
     i++) {
    /* do stuff */
}

ce qui montre qu’on peut s’en sortir avec seulement un entier comme mémoire
et donc on ne veut pas devoir allouer une liste de 100.000 éléments juste pour pouvoir faire cette boucle !

combinaisons d’itérations#

Python propose des outils pour créer et combiner les itérables:

  • fonctions natives builtin qui créent des itérateurs:

    • range, enumerate, et zip

  • dans un module dédié itertools:

    • chain, cycle, islice, …

range#

  • range crée un objet qui permet d’itèrer sur un intervalle de nombres entiers

  • arguments : même logique que le slicing

    • début (inclus), fin (exclus), pas

    • sauf (curiosité) : si un seul argument, c’est la fin

# les nombres pairs de 10 à 20
for i in range(10, 21, 2):
    print(i, end=" ")
10 12 14 16 18 20 
# le début par défaut est 0
for i in range(5):
    print(i, end=" ")
0 1 2 3 4 

un range n’est pas une liste#

  • l’objet retourné par range n’est pas une liste

  • au contraire il crée un objet tout petit, un itérateur (*)

  • qui contient seulement la logique de l’itération

  • la preuve:

# 10**20 c'est 100 millions de Tera

# un range est presque un iterateur
iterator = range(10**20)
iterator
range(0, 100000000000000000000)
for item in iterator:
    if item >= 5:
        break
    print(item, end=" ")
0 1 2 3 4 

je chipote, mais…

en réalité un range() n’est pas techniquement un itérateur; mais bon ça y ressemble beaucoup…

exercice

comment créer une vraie liste des entiers de 1 à 10 ?

count : un itérateur infini#

du coup un itérateur peut même .. ne jamais terminer :

# count fait partie du module itertools

from itertools import count
count?
# si on n'arrête pas la boucle nous mêmes
# ce fragment va boucler sans fin

for i in count():
    print(i, end=" ")
    if i >= 10:
        break
0 1 2 3 4 5 6 7 8 9 10 
# on peut changer les réglages
# ici en partant de 2 avec un step de 5

for i in count(2, 5):
    print(i, end=" ")
    if i >= 32:
        break
2 7 12 17 22 27 32 

enumerate#

on a dit qu’on ne faisait jamais

for i in range(len(liste)):
    item = liste[i]
    print(item, end=" ")

mais comment faire alors si on a vraiment besoin de l’index i ?
→ il suffit d’utiliser la builtin enumerate()

L = [1, 10, 100]

for i, item in enumerate(L):
    print(f"{i}: {item}")
0: 1
1: 10
2: 100
_images/iter-enumerate.svg

enumerate est typiquement utile sur un fichier, pour avoir le numéro de ligne
remarquez le deuxième argument de enumerate, ici pour commencer à 1

# on peut aussi commencer 
# à autre chose que 0

with open("some-file.txt") as f:
    for lineno, line in enumerate(f, 1):
        print(f"{lineno}:{line}", end="")
1:some text written
2:on a few lines

zip#

zip permet d’itérer sur plusieurs itérables “en même temps”:

_images/iter-zip.svg
liste1 = [10, 20, 30]
liste2 = [100, 200, 300]
for a, b in zip(liste1, liste2):
    print(f"{a}x{b}", end=" ")
10x100 20x200 30x300 

les arguments

  • zip fonctionne avec autant d’arguments qu’on veut

  • elle s’arrête dès que l’entrée la plus courte est épuisée

exercice: enumerate = zip + count

aucun intérêt pratique, mais juste pour le fun :
voyez-vous un moyen d’écrire enumerate à base de zip et count ?

un itérateur s’épuise#

ATTENTION il y a toutefois une limite lorsqu’on utilise un itérateur

  • une fois que l’itérateur est arrivé à sa fin

  • il est “épuisé” et on ne peut plus boucler dessus

note

à cet égard, les range() sont spéciaux

# avec une liste, pas de souci
L = [100, 200]

print('pass 1')
for i in L:
    print(i)

print('pass 2')
for i in L:
    print(i)
pass 1
100
200
pass 2
100
200
# iter() permet de construire
# un itérateur sur un itérable

R = iter(L)

print('pass 1')
for i in R:
    print(i)

print('pass 2')
for i in R:
    print(i)    
pass 1
100
200
pass 2

du coup par exemple,
ne pas essayer d’itérer deux fois sur un zip() ou un enumerate(), vous observeriez le même phénomène

le module itertools - assemblage d’itérables#

on trouve dans le module itertools plusieurs utilitaires très pratiques :

  • count pour énumérer les entiers (voir plus haut)

  • chain pour chainer plusieurs itérables

  • cycle pour rejouer un itérable en boucle

  • repeat pour énumérer plusieurs fois le même objet

  • islice pour n’énumérer que certains morceaux

  • zip_longest fonctionne comme zip mais s’arrête au morceau le plus long

chain#

_images/iter-chain.svg
from itertools import chain
data1 = (10, 20, 30)
data2 = (100, 200, 300)
# chain()
for d in chain(data1, data2):
    print(f"{d}", end=" ")
10 20 30 100 200 300 
# c'est comme un lego, on peut combiner toutes ces fonctions
for i, d in enumerate(chain(data1, data2)):
    print(f"{i}x{d}", end=" ")
0x10 1x20 2x30 3x100 4x200 5x300 

cycle#

_images/iter-cycle.svg
# cycle() ne termine jamais non plus

from itertools import cycle
data1 = (10, 20, 30)

for i, d in enumerate(cycle(data1)):
    print(f"{i}x{d}", end=" ")
    if i >= 10:
        break
0x10 1x20 2x30 3x10 4x20 5x30 6x10 7x20 8x30 9x10 10x20 

repeat#

# repeat()
from itertools import repeat
data1 = (10, 20, 30)
data2 = (100, 200, 300)

# pour peut répéter le même élément plusieurs fois
padding = repeat(1000, 3)

for i, d in enumerate(chain(data1, padding, data2)):
    print(f"{i}x{d}", end=" ")
0x10 1x20 2x30 3x1000 4x1000 5x1000 6x100 7x200 8x300 

islice#

fonctionne comme le slicing, mais sur n’importe quel itérable

# avec islice on peut par exemple 
# sauter une ligne sur deux dans un fichier
from pathlib import Path

# on crée un fichier 
with Path('islice.txt').open('w') as f:
    for i in range(6):
        f.write(f"{i}**2 = {i**2}\n")
# pour ne relire qu'une ligne sur deux

from itertools import islice

with Path('islice.txt').open() as f:
    for line in islice(f, 0, None, 2):
        print(line, end="")
0**2 = 0
2**2 = 4
4**2 = 16
# ou zapper les 3 premières

from itertools import islice

with Path('islice.txt').open() as f:
    for line in islice(f, 3, None):
        print(line, end="")
3**2 = 9
4**2 = 16
5**2 = 25
# ou ne garder que les 3 premières

from itertools import islice

with Path('islice.txt').open() as f:
    for line in islice(f, 3):
        print(line, end="")
0**2 = 0
1**2 = 1
2**2 = 4

zip_longest()#

comme zip, mais s’arrête à l’entrée la plus longue
du coup il faut dire par quoi remplacer les données manquantes

from itertools import zip_longest
for i, d in zip_longest(
        range(6), L, fillvalue='X'):
    print(f"{i} {d}")
0 100
1 200
2 X
3 X
4 X
5 X
_images/iter-zip-longest.svg

itertools & combinatoires#

Le module itertools propose aussi quelques combinatoires usuelles:

exemple avec product#

from itertools import product

dim1 = (1, 2, 3)
dim2 = '♡♢♤'

for i, (d1, d2) in enumerate(product(dim1, dim2), 1):
    print(f"i={i}, d1={d1} d2={d2}")
i=1, d1=1 d2=♡
i=2, d1=1 d2=♢
i=3, d1=1 d2=♤
i=4, d1=2 d2=♡
i=5, d1=2 d2=♢
i=6, d1=2 d2=♤
i=7, d1=3 d2=♡
i=8, d1=3 d2=♢
i=9, d1=3 d2=♤

exercice

le code de Vigenere se prête particulièrement bien à ces outils d’assembage d’itérabes (voir notebook séparé)

sous le capot#

pour les curieux..

comment marche la boucle for ?#

lorsqu’on itère sur un itérable

iterable = [10, 20, 30]

sous le capot, la boucle for va faire:

  • créer un itérateur en appelant iter(iterable)

  • appeler next() sur cet itérateur

  • jusqu’à obtenir l’exception StopIteration

iter() et next()#

voici un équivalent approximatif

iterable = [10, 20, 30]

# cette boucle for 

for item in iterable:
    print(item)
10
20
30
# est en gros équivalente
# à ce fragment

iterateur = iter(iterable)
while True:
    try:
        item = next(iterateur)
        print(item)
    except StopIteration:
        # print("fin")
        break
10
20
30

iter() et next()

il peut être parfois pratique d’utiliser iter() et next()
par exemple, comment prendre un élément - n’importe lequel - dans un ensemble ?

quel objet est itérable ?#

  • il existe beaucoup d’objets itérables en python

    • tous les objets séquence: listes, tuples, chaînes, etc.

    • les sets, les dictionnaires

    • les vues (dict.keys(), dict.values()), etc.

    • les fichiers

    • les générateurs

  • il faut penser à les utiliser, c’est le plus rapide et le plus lisible

quel objet est un itérateur ? (avancé)#

pour savoir si un objet est un itérateur
tester si
iter(obj) is obj

def is_iterator(obj):
    return iter(obj) is obj

par exemple#

  • une liste n’est pas son propre itérateur

  • un fichier est son propre itérateur

# créons un fichier
with open("tmp.txt", 'w') as F:
    for i in range(6):
        print(f"{i=} {i**2=}", file=F)

# pour voir qu'un fichier ouvert en
# lecture est son propre itérateur
with open("tmp.txt") as F:
    print(f"{is_iterator(F)=}")
is_iterator(F)=True
# la liste non
L = list(range(5))
print(f"{is_iterator(L)=}")
is_iterator(L)=False
# cycle en est un
C = cycle(L)
print(f"{is_iterator(C)=}")
is_iterator(C)=True
# un zip() est un itérateur
Z = zip(L, L)
print(f"{is_iterator(Z)=}")
is_iterator(Z)=True

bien se souvenir

un itérateur s’épuise, et donc un objet qui est un itérateur ne peut être itéré qu’une seule fois