Licence CC BY-NC-ND, Thierry Parmentelat

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

itér.. (2/3) - compr. et genexpr#

compréhensions#

très fréquemment on veut construire un mapping

map#

pour appliquer une fonction à un ensemble de valeurs

_images/iter-map.svg

map + filter#

idem en excluant certaines entrées

_images/iter-map-filter.svg

compréhension de liste#

c’est le propos de la compréhension (de liste):

[expr(x) for x in iterable]

qui est équivalent à

result = []
for x in iterable:
    result.append(expr(x))

compréhension de liste avec filtre#

si nécessaire on peut ajouter un test de filtre:

[expr(x) for x in iterable 
     if condition(x)]

qui est équivalent à

result = []
for x in iterable:
    if condition(x):
        result.append(expr(x))

compréhensions de liste - exemple 1#

sans filtre

# la liste des carrés 
# des entiers entre 0 et 5

[x**2 for x in range(6)]
[0, 1, 4, 9, 16, 25]
# si on décortique

result = []

for x in range(6):
    result.append(x**2)

result
[0, 1, 4, 9, 16, 25]

compréhensions de liste - exemple 2#

avec filtre

# la liste des cubes
# des entiers pairs entre 0 et 5

[x**2 for x in range(6) if x % 2 == 0]
[0, 4, 16]
# si on décortique

result = []

for x in range(6):
    if x % 2 == 0:
        result.append(x**3)

result
[0, 8, 64]

compréhension de liste - imbrications#

  • on peut imbriquer plusieurs niveaux de boucle

  • la profondeur du résultat dépend du nombre de [
    et pas du nombre de for

# une liste toute plate comme résultat
# malgré deux boucles for imbriquées
[10*x + y for x in (1, 2) for y in (1, 2)]
[11, 12, 21, 22]

compréhensions imbriquées - exemple#

l’ordre dans lequel se lisent les compréhensions imbriquées:
il faut imaginer des for imbriqués dans le même ordre

[10*x + y for x in range(1, 5) 
     if x % 2 == 0 
         for y in range(1, 5)
             if y % 2 == 1]
[21, 23, 41, 43]
# est équivalent à
# (dans le même ordre)
L = []
for x in range(1, 5):
    if x % 2 == 0:
        for y in range(1, 5):
            if y % 2 == 1:
                L.append(10*x + y)
L
[21, 23, 41, 43]

compréhension d’ensemble#

même principe exactement, mais avec des {} au lieu des []

# en délimitant avec des {} 
# on construit une
# compréhension d'ensemble
{x**2 for x in range(-6, 7) 
    if x % 2 == 0}
{0, 4, 16, 36}
# rappelez-vous que {} est un dict
result = set()

for x in range(-6, 7):
    if x % 2 == 0:
        result.add(x**2)
        
result
{0, 4, 16, 36}

compréhension de dictionnaire#

syntaxe voisine, avec un : pour associer clé et valeur

# sans filtre
 
{x : x**2 for x in range(4)}
{0: 0, 1: 1, 2: 4, 3: 9}
# avec filtre

{x : x**2 for x in range(4) if x%2 == 0}
{0: 0, 2: 4}

exemple : créer un index par une compréhension#

un idiome classique :

  • on a une liste d’éléments - beaucoup, genre \(10^6\)

  • on veut pouvoir accéder en temps constant
    à un élément à partir d’un id

  • solution: créer un dictionnaire - qu’on appelle un index
    (comme dans les bases de données)

# créer un dict qui permet un accès direct à partir du nom
personnes = [
    {'nom': 'Martin', 'prenom': 'Julie', 'age': 18},
    {'nom': 'Dupont', 'prenom': 'Jean', 'age': 32},
    {'nom': 'Durand', 'prenom': 'Pierre', 'age': 25},  
]

# l'idiome pour créer un index
index = {personne['nom']: personne for personne in personnes}

index
{'Martin': {'nom': 'Martin', 'prenom': 'Julie', 'age': 18},
 'Dupont': {'nom': 'Dupont', 'prenom': 'Jean', 'age': 32},
 'Durand': {'nom': 'Durand', 'prenom': 'Pierre', 'age': 25}}
# le concept est le même que dans une base de données
# en termes d'accès rapide à partir du nom qui jour le rôle d'id

index['Martin']
{'nom': 'Martin', 'prenom': 'Julie', 'age': 18}

expression génératrice#

performance des compréhensions#

la compréhension n’est pas l’arme absolue ! elle a un gros défaut, c’est qu’on va toujours :

  • parcourir tout le domaine

  • et allouer de la mémoire

  • au moment où on évalue la compréhension,
    i.e. avant même de faire quoi que ce soit d’autre

note

finalement c’est exactement la même discussion que itérateur vs itérable
ou quand on avait comparé range() avec une liste

expression génératrice#

pour éviter ces problèmes: utiliser une genexpr

  • ça se présente un peu comme une compréhension de liste

  • mais avec des () à la place des []

  • supporte les if et les imbrications
    exactement comme les compréhensions

data = [0, 1]
# compréhension

C = [10*x + y for x in data for y in data]

for y in C:
    print(y)
0
1
10
11
# genexpr

G = (10*x + y for x in data for y in data)

for y in G:
    print(y)
0
1
10
11

les genexprs sont des itérateurs#

  • même “contenu” que la compréhension

  • mais pas la même implémentation: une genexpr est de type generator (en particulier c’est un itérateur)

# compréhension

C = [x**2 for x in range(4)]

type(C), C
(list, [0, 1, 4, 9])
# genexpr

G = (x**2 for x in range(4))

type(G), G
(generator, <generator object <genexpr> at 0x7f5bf5f07920>)
# une compréhension est une vraie liste

C2 = [x**2 for x in range(100_000)]

import sys
sys.getsizeof(C2)
800984
# les genexprs sont des itérateurs
# et donc sont tout petits

G2 = (x**2 for x in range(100_000))

sys.getsizeof(G2)
200

compréhension ou genexpr ?#

  • les compréhensions de dictionnaire et d’ensemble sont souvent justifiées

  • par contre, pour les listes: toujours bien se demander si on a vraiment besoin de construire la liste

  • ou si au contraire on a juste besoin d’itérer dessus (souvent une seule fois d’ailleurs)

  • si on a vraiment besoin de cette liste, alors la compréhension est OK

  • mais dans le cas contraire il faut préférer un itérateur, c’est le propos de l’expression génératrice

  • qui souvent revient à remplacer [] par () - ou même juste enlever les []

apprenez à bien choisir entre compréhension et genexpr (les deux sont utiles)

# remplissons une classe imaginaire
from random import randint

matieres = ('maths', 'français', 'philo')

def notes_eleve_aleatoires():
    return {matiere: randint(0, 20) for matiere in matieres}
# ici je crée une compréhension; pourquoi ?
notes_classe = [notes_eleve_aleatoires() for _ in range(4)]
notes_classe
[{'maths': 12, 'français': 13, 'philo': 8},
 {'maths': 13, 'français': 4, 'philo': 13},
 {'maths': 5, 'français': 13, 'philo': 16},
 {'maths': 17, 'français': 18, 'philo': 12}]
# pour calculer la moyenne de la classe en maths
# pas besoin de garder les résultats intermédiaires
# du coup, on fabrique une genexpr
# en toute rigueur il aurait fallu écrire ceci
sum((notes_eleve['maths'] for notes_eleve in notes_classe)) / len(notes_classe)
11.75
# mais la syntaxe nous permet de nous en affranchir
# (remarquez une seul niveau de parenthèses, et l'absence de [])
sum(notes_eleve['maths'] for notes_eleve in notes_classe) / len(notes_classe)
11.75