Algo & Prog 1 Python : CM séance 07
7. Compréhensions, générateurs et itérables
7.1. Expression if-else
On peut remplacer :
if condition :
valeur = expression_1
else :
valeur = expression_2
par une syntaxe plus courte :
valeur = expression_1 if condition else expression_2
Remarque : ceci est équivalent à l'opérateur ternaire du C/C++/Java :
valeur = condition ? expression_1 : expression_2
qui n'existe pas en Python.
Exemple :
res = liste[i] if i >= 0 and i < len(liste) else 0
On peut imbriquer :
if condition_1 :
valeur = expression_1
elif condition_2 :
valeur = expression_2
...
else :
valeur = expression_n
peut s'écrire :
valeur = expression_1 if condition_1 else \
expression_2 if condition_2 else \
... else \
expression_n
7.2. Compréhensions
Une compréhension est un procédé permettant de construite un objet en filtrant un autre objet itérable, avec une syntaxe très concise.
7.2.1. Listes
Les compréhensions de listes permettent de construire des listes avec la syntaxe suivante :
new_list = [ expression(item) for item in iterable ]
new_list = [ expression(item) for item in iterable if condition(item) ]
La deuxième ligne est équivalente à
new_list = []
for item in iterable :
if condition(item) :
new_list.append(expression(item))
Exemples :
-
liste des carrés de 1 à 10 :
res = [] for n in range(1,10+1) : res.append(n*n)
avec une compréhension :
res = [n*n for n in range(1,10+1)]
-
listes des nombres de 1 à 100 multiples de 3 ou 7 :
res = [] for n in range(1,100+1) : if n % 3 == 0 or n % 7 == 0 : res.append(n)
avec une compréhension :
res = [n for n in range(1,100+1) if n % 3 == 0 or n % 7 == 0]
-
produit cartésien de deux listes :
numéros = [1,2,3,4] lettres = ['A','B','C'] res = [] for n in numéros : for l in lettres : res.append((n,l))
avec une compréhension :
res = [(n,l) for n in numéros for l in lettres]
7.2.2. Dictionnaires
On peut aussi faire des compréhensions de dictionnaires :
new_dict = { key : val for item in iterable }
new_dict = { key : val for item in iterable if condition(item)}
où key
et val
sont des expressions dépendant de item
.
Exemples :
-
Dictionnaire des cubes de 1 à 10 :
res = {} for n in range(1,10+1) : res[n] = n**3
avec une compréhension :
res = { n : n**3 for n in range(1,10+1) }
-
Expression if-else : dictionnaire de parité des carrés de 1 à 10 :
res = {} for n in range(1,10+1) : if n*n % 2 == 0 : res[n*n] = 'pair' else : res[n*n] = 'impair'
avec une compréhension :
res = { n*n : ('pair' if n*n%2 == 0 else 'impair') for n in range(1,10+1)}
-
Dictionnaire inversé :
food = { 'banane' : 'fruit', 'salade' : 'légume', 'café' : 'boisson' } inv = {} for alim,categ in food.items() : inv[categ] = alim
avec une compréhension :
inv = { categ:alim for alim,categ in food.items() }
Remarque : ne fonctionne bien que si toutes les valeurs de food sont uniques !
7.2.3. Ensembles et tuples
Les compréhensions de set
sont possibles avec cette syntaxe :
carrés = { n*n % 10 for n in range(10) }
Les compréhension de tuples ne sont pas possible, car la syntaxe
( ... for ... )
est réservée pour les expressions génératrices,
voir plus loin.
7.2.4. Plus de listes
Les compréhensions peuvent être utiles pour effectuer des copies de listes ou créer des listes de listes :
-
Copie de liste : on avait vu qu'il est possible de faire une copie légère avec un slice :
>>> b = a[:]
On peut aussi recopier via une compréhension :
>>> b = [item for item in a]
-
Pour créer un liste de \(n\) éléments à 0 on peut écrire
n = ... l = [ 0 for i in range(n) ]
on peut écrire aussi
l = [0]*n
-
Pour créer une liste 2D de taille \(n \times m\) on peut écrire
n = ... ; m = ... l = [ [0 for i in range(n)] for j in range(m) ]
ou encore
l = [ [0]*n for j in range(m) ]
MAIS PAS
l = [ [0]*n ]*m
Explication :
>>> l = [[0]*3]*5 >>> l [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]] >>> l[1][2] = 5 >>> l [[0, 0, 5], [0, 0, 5], [0, 0, 5], [0, 0, 5], [0, 0, 5]] >>> id(l[0]) 139979814315016 >>> id(l[1]) 139979814315016
En fait Python créé la liste
[0,0,0]
, puis crée une liste de 5 fois la même référence sur la liste[0,0,0]
!
7.3. Générateurs
Un générateur est un objet itérable une seule fois.
Avantage sur une liste :
- pas besoin de stocker l'ensemble des valeurs en mémoire !
- chaque valeur est calculée au fur et à mesure.
7.3.1. Expression génératrice
Une expression génératrice est une sorte de générateur.
Syntaxe : la même que les compréhension mais entre ()
:
new_gen = ( expression(item) for item in iterable if condition(item) )
Exemple :
>>> g = (n*n for n in range(5))
>>> for i in g : print(i)
0 1 4 9 16
>>> for i in g : print(i) # deuxième essai
>>> # rien : ne sert qu'une seule fois
Remarque : une expression génératrice n'a pas de longueur :
>>> g = (n*n for n in range(5))
>>> len(g)
TypeError: object of type 'generator' has no len()
7.3.2. Fonction génératrice
C'est une fonction qui renvoie un générateur.
Dans la fonction on renvoie une ou plusieurs valeurs avec yield
.
def jours() :
print("début")
yield 'lundi'
yield 'mardi'
yield 'mercredi'
print("fin")
>>> jours()
<generator object jours at 0x7f4f9719ee60>
>>> for j in jours() : print("j =", j)
début
j = lundi
j = mardi
j = mercredi
fin
>>> for j in jours() : print("j =", j)
début
j = lundi
j = mardi
j = mercredi
fin
>>> list(jours()) # variante pour tester un itérable
début
fin
['lundi', 'mardi', 'mercredi']
→ envoie les valeurs pour chaque for
, car jours()
crée chaque fois un
autre générateur.
Mais le générateur renvoyé est à usage unique :
>>> x = jours()
>>> list(x)
début
fin
['lundi', 'mardi', 'mercredi']
>>> list(x) # deuxième essai
[] # rien
Comment ça marche ?
-
La fonction contient des
yield
→ ce n'est pas une fonction normale. -
Appel de la fonction → python renvoie un générateur.
-
lors du
for
sur le générateur :- première itération : python exécute la fonction jusqu'au 1er
yield
, sauve l'état dans le générateur et renvoie la valeur ; - itérations suivantes : python reprend l'exécution jusqu'au prochain
yield
, sauve l'état dans le générateur et renvoie la valeur ; - après la dernière itération : python termine la fonction puis
génère l'exception
StopIteration
→ c'est la fin dufor
.
- première itération : python exécute la fonction jusqu'au 1er
7.4. Objets itérables
Un objet est itérable si
-
il définit une méthode
__iter__()
qui renvoie un itérateur (qui peut être lui-même, ou un autre objet) ; -
l'itérateur renvoyé possède une methode
__next__()
qui- renvoie le prochain élément ;
- lève
StopItération
à la fin.
7.4.1. Générateurs
Les générateurs sont itérables :
-
expression génératrice :
>>> g = (i for i in range(3)) >>> g.__iter__() <generator object <genexpr> at 0x7f6e06a94d00> >>> id(g) 140110534888704 >>> id(g.__iter__()) 140110534888704 # __iter__() renvoie self >>> g.__next__() 0 >>> g.__next__() 1 >>> g.__next__() 2 >>> g.__next__() StopIteration
-
fonction génératrice :
def jours() : print("début") yield 'lundi' yield 'mardi' yield 'mercredi' print("fin") >>> jours().__iter__() <generator object jours at 0x7f6e06a94db0> >>> jours().__iter__() <generator object jours at 0x7f6e06a94e08>
→
jours()
est itérable et renvoie chaque fois un nouvel itérateur.>>> g = jours() >>> g.__iter__() <generator object <genexpr> at 0x7f6e06a94d00> >>> g.__iter__() <generator object <genexpr> at 0x7f6e06a94d00>
→ cette fois c'est le même, il est lié à l'instance
g
et ne servira donc qu'une seule fois.>>> g.__next__() début 'lundi' >>> g.__next__() 'mardi' >>> g.__next__() 'mercredi' >>> g.__next__() fin StopIteration
7.4.2. Les ranges
La fonction range()
est aussi itérable :
>>> r = range(2)
>>> list(r)
[0, 1]
>>> list(r)
[0, 1]
>>> g = r.__iter__() # itérateur à usage unique
>>> type(g)
<class 'range_iterator'>
>>> g.__next__()
0
>>> g.__next__()
1
>>> g.__next__()
StopIteration
La fonction iter()
appelle la méthode __iter__
, et next()
appelle __next__
.
On peut donc écrire :
>>> x = iter((3,5)) # ou x = (3,5).__iter__()
>>> next(x) # ou x.__next__()
3
>>> next(x)
5
>>> next(x)
StopIteration
7.4.3. Classe itérable
On peut écrire une classe toute-en-un, pour un usage unique ou multiple.
-
Usage unique :
class Mois : def __init__(self) : print("init Mois") self.liste = ['janvier','février','mars'] self.ind = 0 def __iter__(self) : return self def __next__(self) : if not self.ind < len(self.liste) : raise StopIteration res = self.liste[self.ind] self.ind += 1 return res >>> m = Mois() init Mois >>> list(m) ['janvier', 'février', 'mars'] >>> list(m) [] # -> usage unique >>>
-
Usage multiple :
class Saison : def __init__(self) : print ("init Saison") self.liste = ['printemps','été','automne','hiver'] def __iter__(self) : return SaisonIterateur(self) class SaisonIterateur : def __init__(self, saison) : print ("init SaisonIterator") self.saison = saison self.ind = 0 def __next__(self) : if not self.ind < len(self.saison.liste) : raise StopIteration res = self.saison.liste[self.ind] self.ind += 1 return res >>> s = Saison() init Saison >>> list(s) init SaisonIterator ['printemps', 'été', 'automne', 'hiver'] >>> list(s) init SaisonIterator ['printemps', 'été', 'automne', 'hiver'] >>> g = s.__iter__() init SaisonIterator >>> list(g) TypeError: 'SaisonIterateur' object is not iterable >>> g.__next__() 'printemps' >>> g.__next__() 'été' >>> g.__next__() 'automne' >>> g.__next__() 'hiver' >>> g.__next__() StopIteration