Définition de liste
Le deuxième point d'attention pour un code « pythonique » concerne la définition de listes. La façon la plus simple de définir une liste consiste à partir d'une liste vide et d'y ajouter les éléments désirés à l'aide de la méthode append
. Les éléments sont, par exemple, ajoutés les uns après les autres grâce à une boucle, comme dans le code suivant qui remplit la liste data
avec les premières puissances de deux :
data = []
for i in range(n):
data.append(2 ** i)
Deux techniques peuvent être utilisées pour améliorer le temps d'exécution de programmes créant des listes. Outre les performances, ces techniques améliorent également la lisibilité des programmes.
Fonctions map et filter
Lorsqu'il s'agit de construire une liste à partir d'une autre, en appliquant une opération sur chaque élément de cette dernière, on peut utiliser la fonction prédéfinie map
. Celle-ci prend comme premier paramètre une fonction décrivant l'opération à appliquer à chaque élément de la liste passée en second paramètre. Lorsque la fonction à appliquer est simple, il est préférable de la définir à l'aide d'une fonction lambda, pour que le code soit le plus « pythonique » possible. Voici deux fonctions qui créent une liste avec les premières puissances de deux :
def map_1(n):
result = []
for i in range(n):
result.append(2 ** i)
return result
def map_2(n):
data = map(lambda i: 2**i, range(n))
return list(data)
La seconde fonction est plus « pythonique » que la première et il faut privilégier ce style, même si au niveau des performances, elle n'est qu'un peu plus rapide que la première. En effet, on passe de 97 ms à 94 ms pour créer une liste avec les dix-mille premières puissances de 2, soit une diminution de temps d'à peine 3%.
On peut aussi vouloir construire une liste à partir d'une autre en ne gardant que certains éléments, ce que l'on peut faire avec la fonction prédéfinie filter
. Celle-ci prend comme premier paramètre une fonction booléenne décrivant la condition que doivent satisfaire les éléments qu'il faut garder dans la liste passée en second paramètre. De nouveau, si la fonction est simple, mieux vaut utiliser une fonction lambda. Voici deux fonctions qui créent une liste avec les entiers compris entre 0 et qui sont divisibles soit par deux, soit par trois :
def filter_1(n):
result = []
for i in range(n):
if i % 2 == 0 or i % 3 == 0:
result.append(i)
return result
def filter_2(n):
data = filter(lambda i: i % 2 == 0 or i % 3 == 0, range(n))
return list(data)
Au niveau du style, mieux vaut utiliser la seconde fonction, même si les performances au niveau du temps d'exécution ne sont pas forcément meilleures. Pour cet exemple, la seconde fonction est même plus lente que la première. En effet, on passe de 193 ms à 228 ms pour créer une liste avec les entiers compris entre 0 et un million qui sont divisibles par deux ou par trois, soit une augmentation de temps de 25%.
En réalité, les fonctions map
et filter
n'améliorent pas forcément le temps d'exécution. On préfère néanmoins les utiliser pour deux autres raisons. Tout d'abord, la lisibilité est améliorée en évitant d'avoir des codes avec des niveaux d'indentation trop élevés en raison de l'imbrication d'une instruction if
dans une boucle for
.
Ensuite, comme décrit dans la section 1.3 plus loin dans le chapitre, ces deux fonctions améliorent l'occupation mémoire. En effet, ce qui est couteux au niveau du temps d'exécution, c'est l'instruction list(data)
faite avant le return
pour obtenir un objet de type list
.
Liste en compréhension
La manière plus « pythonique » de créer une liste consiste à utiliser une définition en compréhension, inspirée de la définition mathématique des séquences. On préfère cette notation, plus lisible et concise que la fonction map
, car il ne faut pas définir et passer une fonction en paramètre. Voici comment l'exemple précédent, qui calcule les dix-mille premières puissances de deux, se réécrit :
def map_3(n):
return [2**i for i in range(n)]
Au niveau du temps d'exécution, cette nouvelle manière de définir la fonction prend plus ou moins le même temps que les deux versions précédentes. Son avantage est d'offrir un gain en lisibilité de code.
On peut également remplacer la fonction filter
par une définition en compréhension en ajoutant une clause if
. Voici comment l'exemple précédent, qui crée une liste avec les entiers compris entre 0 et un million qui sont divisibles par deux ou par trois, se réécrit :
def filter_3(n):
return [i for i in range(n) if i % 2 == 0 or i % 3 == 0]
Concernant le temps d'exécution, cette nouvelle manière de définir la liste est plus efficace. On passe en effet à 156 ms, c'est-à-dire une diminution de 19% par rapport à la première version.
La définition de listes en compréhension est, en fait, plus qu'un simple sucre syntaxique. Outre produire un code plus « pythonique », plus concis, lisible et explicite, elle permet d'améliorer le temps d'exécution, par rapport à l'utilisation de append
dans une boucle.
Concaténation de chaines de caractères
Comme mentionné dans la section précédente, la définition de liste en compréhension peut être utilisée pour améliorer les deux fonctions utilisant de la concaténation de chaines de caractères. Voici deux nouvelles versions des fonctions présentées précédemment :
def concat_3(n):
return ''.join(['Nom' for i in range(n)])
def format_3(n):
return ''.join(['{} x 10 = {}\n'.format(i, i*10) for i in range(n)])
Les temps d'exécution sont respectivement passés à 58 ms et 51 ms, soit des améliorations de 66% et de 52%, par rapport aux versions les plus lentes utilisant l'opérateur de concaténation.