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
forest la méthode préférée pour itérer sur un ensemble de valeursen général préférable au
whileen Pythonon peut faire un
forsur n’importe quel itérablece n’est pas le cas pour le
whileavec
forc’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 :
breaksort complètement de la bouclecontinuetermine 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 vous devez vous dire qu’il y a mieux à faire:for i in range(len(truc))
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
fornotamment 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
formais plusieurs fonctions acceptent en argument des itérables
sum,max,minmap,filteretc…
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, etzip
dans un module dédié
itertools:chain,cycle,islice, …
range#
rangecrée un objet qui permet d’itèrer sur un intervalle de nombres entiersarguments : 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
rangen’est pas une listeau 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 ?
réponse
list(range(1, 11))
où le type list se comporte, (comme tous les types)
comme une usine à fabriquer des listes
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
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”:
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
zipfonctionne avec autant d’arguments qu’on veutelle 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 ?
réponse
# zip s'arrête dès que
# l'un de ses morceaux s'arrête
for index, item in zip(count(), L):
print(f"{index} {item}")
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 :
countpour énumérer les entiers (voir plus haut)chainpour chainer plusieurs itérablescyclepour rejouer un itérable en bouclerepeatpour énumérer plusieurs fois le même objetislicepour n’énumérer que certains morceauxzip_longestfonctionne commezipmais s’arrête au morceau le plus long
chain#
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#
# 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
itertools & combinatoires#
Le module itertools propose aussi quelques combinatoires usuelles:
product: produit cartésien de deux itérablespermutations: les permutations (\(n!\))combinations: p parmi net d’autres…
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..
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
comment marche la boucle
for?#lorsqu’on itère sur un itérable
sous le capot, la boucle
forva faire:créer un itérateur en appelant
iter(iterable)appeler
next()sur cet itérateurjusqu’à obtenir l’exception
StopIteration