Aller au contenu

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)}

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 du for.

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