19. Les bonus▲
L'un de mes objectifs dans ce livre a été de vous en apprendre aussi peu que possible sur Python. Lorsqu'il y avait deux façons différentes de faire quelque chose, j'en ai choisi une et évité de parler de l'autre. Ou alors, parfois, j'ai mentionné la seconde dans un exercice.
Je voudrais maintenant revenir à quelques-unes des pépites que j'ai laissées de côté. Python offre un bon nombre de fonctionnalités qui ne sont pas réellement nécessaires - vous pouvez écrire de bons programmes sans elles -, mais elles peuvent parfois vous permettre d'écrire du code plus concis, ou plus lisible, ou plus efficace, et parfois même les trois en même temps.
19-1. Expressions conditionnelles▲
Nous avons étudié les instructions conditionnelles au chapitre 5.4Exécution conditionnelle. Ces conditions sont souvent utilisées pour choisir entre deux valeurs. Par exemple :
if
x >
0
:
y =
math.log
(
x)
else
:
y =
float(
'nan'
)
Ce fragment de code vérifie si x est un nombre positif. Si c'est le cas, il calcule le logarithme de ce nombre. Sinon, la fonction math.log générerait une exception entraînant l'arrêt du programme. Pour éviter cet arrêt intempestif du programme, nous générons un « NaN » (Not a Number), une valeur spéciale en virgule flottante qui représente autre chose qu'un nombre.
Nous pouvons écrire ces instructions de façon plus concise en utilisant une expression conditionnelle :
y =
math.log
(
x) if
x >
0
else
float(
'nan'
)
Que l'on peut lire comme suit : « y prend la valeur log(x) si x est plus grand que 0 ; sinon, ce n'est pas un nombre, il prend la valeur spéciale NaN. »
Les fonctions récursives se prêtent parfois à l'utilisation d'expressions conditionnelles. Par exemple, voici une version récursive de la fonction factorielle :
def
factorielle
(
n):
if
n ==
0
:
return
1
else
:
return
n *
factorielle
(
n-
1
)
Nous pouvons la réécrire comme suit :
def
factorielle
(
n):
return
1
if
n ==
0
else
n *
factorielle
(
n-
1
)
Une autre utilisation des expressions conditionnelles est la gestion des arguments optionnels. Par exemple, voici la méthode init de la solution GoodKangaroo (voir exercice 2 du chapitre 17) :
def
__init__
(
self, name, contents=
None
):
self.name =
name
if
contents ==
None
:
contents =
[]
self.pouch_contents =
contents
Nous pouvons la réécrire ainsi :
def
__init__
(
self, name, contents=
None
):
self.name =
name
self.pouch_contents =
[] if
contents ==
None
else
contents
D'une façon générale, vous pouvez remplacer une instruction conditionnelle par une expression conditionnelle si les deux branches contiennent des expressions simples qui sont soit renvoyées à une fonction appelante, soit affectées à la même variable.
19-2. Les listes en compréhension▲
Nous avons abordé dans la section 10.7Mapper, filtrer et réduire les mécanismes de mappage et de filtration. Par exemple, la fonction suivante prend en entrée une liste de chaînes de caractères, applique la méthode de chaîne capitalize à chacun des éléments et renvoie une nouvelle liste de chaînes :
def
mettre_tout_en_capitales
(
t):
res =
[]
for
s in
t:
res.append
(
s.capitalize
(
))
return
res
Nous pouvons réécrire cette fonction de façon plus concise en utilisant une liste en compréhension :
def
mettre_tout_en_capitales
(
t):
return
[s.capitalize
(
) for
s in
t]
Les opérateurs crochets indiquent que nous construisons une nouvelle liste. L'expression à l'intérieur des crochets spécifie les éléments de la liste, et la clause for indique quelle séquence nous parcourons.
La syntaxe d'une liste en compréhension est un peu étrange parce que la variable de boucle, s dans notre exemple, apparaît dans l'expression (s.capitalize) avant qu'elle ne soit définie (for s in t).
Les listes en compréhension s'appellent parfois aussi listes en intension (avec un s et non un t, car le mot, issu de la philosophie et des mathématiques, s'oppose ici à extension).
Les listes en compréhension permettent également de filtrer des données. Par exemple, cette fonction sélectionne les éléments de t qui sont en lettres capitales et renvoie la nouvelle liste :
def
capitales_seulement
(
t):
res =
[]
for
s in
t:
if
s.isupper
(
):
res.append
(
s)
return
res
Nous pouvons la réécrire en utilisant une liste en compréhension :
def
capitales_seulement
(
t):
return
[s for
s in
t if
s.isupper
(
)]
Les listes en compréhension sont concises et faciles à lire, tout au moins pour les expressions simples. Et elles sont généralement plus rapides que des boucles for équivalentes, parfois beaucoup plus rapides.
Donc, si vous m'en voulez de ne pas les avoir mentionnées plus tôt, je vous comprends. Pour ma défense, je signalerais cependant que les listes en compréhension sont plus difficiles à déboguer parce que vous ne pouvez pas ajouter une instruction d'impression à l'intérieur de la boucle. Et je vous recommanderais de ne les utiliser que si le calcul est suffisamment simple pour que vous ayez une bonne chance de l'écrire correctement du premier coup. Ce qui, pour de purs débutants, signifie jamais.
19-3. Les générateurs▲
Les générateurs ressemblent aux listes en compréhension, mais avec des parenthèses à la place des crochets :
Le résultat est un objet générateur qui sait comment itérer sur une séquence de valeurs. Mais, à la différence d'une liste en compréhension, il ne s'empresse pas de calculer toutes les valeurs immédiatement : il attend qu'on lui demande d'en calculer une. La fonction interne next obtient et renvoie la valeur suivante du générateur :
>>>
next
(
g)
0
>>>
next
(
g)
1
Quand vous arrivez à la fin de la séquence, next signale une exception StoptIteration. Vous pouvez également utiliser une boucle for pour itérer sur les valeurs :
>>>
for
valeur in
g:
... print
(
valeur)
4
9
16
Le générateur sait où il était arrivé dans la séquence, c'est pourquoi la boucle for ci-dessus a repris là où le dernier next était arrivé. La boucle for s'arrête silencieusement à la fin de la séquence, mais, si on l'appelle une nouvelle fois, il générera à nouveau une exception :
>>>
next
(
g)
StopIteration
Les générateurs sont fréquemment utilisés avec des fonctions comme sum, max et min :
19-4. Les fonctions any et all▲
La fonction interne any de Python prend en entrée une série de valeurs booléennes et renvoie True si l'une au moins des valeurs est vraie. Cela fonctionne sur des listes :
>>>
any(
[False
, False
, True
])
True
Mais on s'en sert souvent avec des générateurs :
>>>
any(
lettre ==
't'
for
lettre in
'monty'
)
True
Cet exemple ne paraît pas très utile, car il fait la même chose que l'opérateur in. Mais nous pourrions utiliser any pour réécrire certaines des fonctions de recherche que nous avons écrites dans la section 9.3Recherche.
Par exemple, la fonction evite :
def
evite
(
mot, interdites):
for
lettre in
mot:
if
lettre in
interdites:
return
False
return
True
peut se réécrire comme suit :
def
evite
(
mot, interdites):
return
not
any(
lettre in
interdites for
lettre in
mot)
Ce qui se lit en presque bon français : « mot évite interdites s'il n'y a pas any lettre interdite dans mot ».
Utiliser any avec un générateur est efficace parce que fonction s'arrête (sans aller jusqu'au bout) dès qu'elle a trouvé une valeur satisfaisant la condition recherchée.
Il y a également une fonction interne, all, qui renvoie True si tous les éléments de la séquence sont vrais. À titre d'exercice, utilisez all pour réécrire la fonction utilise_toutes de la section 9.3Recherche.
19-5. Les ensembles (sets)▲
Dans la section 13.6Soustraction de dictionnaire, j'ai utilisé des dictionnaires pour trouver des mots figurant dans un document, mais pas dans une liste de mots. La fonction soustraire utilisait d1, contenant les mots du document sous la forme de clés, et d2, contenant la liste de mots. Elle renvoyait un dictionnaire contenant les clés de d1 ne figurant pas dans d2.
def
soustraire
(
d1, d2):
resultat =
dict(
)
for
clef in
d1:
if
clef not
in
d2:
resultat[clef] =
None
return
resultat
Dans chacun de ces trois dictionnaires, les valeurs associées à toutes les clés étaient None, car nous n'utilisions jamais ces valeurs. Ceci a pour effet de gaspiller l'espace mémoire.
Il existe en Python un autre type de donnée interne, l'ensemble (set), qui fonctionne comme une collection de clés d'un dictionnaire sans valeurs. Il est rapide d'ajouter un élément à un ensemble, de même que de vérifier l'appartenance d'un élément à un ensemble. Et les ensembles fournissent des méthodes et des opérateurs pour effectuer les opérations ensemblistes communes.
Par exemple, la soustraction d'ensembles réalisée ci-dessus peut se faire grâce à une méthode de différence ensembliste, difference, ou à l'opérateur - entre deux ensembles. Nous pouvons donc réécrire soustraire comme suit :
Le résultat est un ensemble au lieu d'un dictionnaire, mais pour les opérations qui nous intéressent, comme l'itération ou le test d'appartenance, le comportement est identique.
Quelques-uns des exercices de ce livre peuvent se faire de façon concise et efficace en utilisant des ensembles. Voici par exemple une solution utilisant un dictionnaire de l'exercice 7 du chapitre 10 :
def
has_duplicates
(
t):
d =
{}
for
x in
t:
if
x in
d:
return
True
d[x] =
True
return
False
Quand un élément apparaît pour la première fois, il est ajouté au dictionnaire. Si le même élément se présente à nouveau, la fonction renvoie True. À la fin, si aucun doublon n'a été rencontré, la fonction renvoie False.
En utilisant les ensembles, nous pouvons réécrire cette fonction ainsi :
Dans un ensemble, un élément ne peut être présent qu'une seule fois. Par conséquent, si un élément existe plusieurs fois dans t, l'ensemble sera plus petit que la liste d'origine. Inversement, s'il n'y a pas de doublon, l'ensemble et la liste d'origine auront la même taille.
Nous pouvons aussi utiliser des ensembles pour résoudre certains des exercices du chapitre 9Étude de cas : jouer avec les mots. Par exemple, voici notre version de utilise_uniquement avec une boucle :
def
utilise_uniquement
(
mot, disponibles):
for
lettre in
mot:
if
lettre not
in
disponibles:
return
False
return
True
La fonction utilise_uniquement vérifie si toutes les lettres du mot sont disponibles. Nous pouvons la réécrire en employant un ensemble :
L'opérateur <=, qui s'écrirait mathématiquement ⊆ (est inclus dans ou est égal), vérifie si un ensemble est un sous-ensemble d'un autre (ou est égal à l'autre), ce qui se vérifie si toutes les lettres de mot appartiennent à disponibles.
À titre d'exercice, réécrivez la fonction evite du chapitre 9Étude de cas : jouer avec les mots en employant des ensembles.
19-6. Les compteurs (Counter)▲
Un compteur (Counter) ressemble à un ensemble, sauf que si un élément est ajouté plus d'une fois, il n'est pas dédoublonné, mais le compteur enregistre le nombre de ses occurrences. Si le concept mathématique de multiensemble (parfois également appelé « sac ») vous est familier, alors sachez qu'un compteur est une façon naturelle de représenter un multiensemble.
Les compteurs ne sont pas un type de donnée interne de Python, mais sont définis dans un module standard nommé collections, que vous devez donc importer pour pouvoir utiliser les compteurs. Vous pouvez initialiser un compteur avec une chaîne de caractères, une liste ou toute autre chose supportant l'itération :
>>>
from
collections import
Counter
>>>
decompte =
Counter
(
'perroquet'
)
>>>
decompte
Counter
(
{'e'
: 2
, 'r'
: 2
, 'o'
: 1
, 'q'
: 1
, 'p'
: 1
, 'u'
: 1
, 't'
: 1
})
Les compteurs se comportent à bien des égards comme des dictionnaires. Ils établissent une correspondance entre chaque clé et le nombre d'occurrences de cette clé. Comme les dictionnaires, il faut que les clés soient hachables.
À la différence des dictionnaires, les compteurs ne déclenchent aucune exception si on tente d'accéder à un élément qui ne s'y trouve pas : dans ce cas, ils renvoient simplement 0 :
>>>
decompte['d'
]
0
Nous pouvons utiliser des compteurs pour réécrire la fonction is_anagram de l'exercice 6 du chapitre 10 :
def
is_anagram
(
mot1, mot2):
return
Counter
(
mot1) ==
Counter
(
mot2)
Si deux mots sont des anagrammes, alors ils contiennent les mêmes lettres et chaque lettre le même nombre de fois, si bien que leurs compteurs sont équivalents.
Les compteurs fournissent des méthodes et opérateurs pour effectuer des opérations de type ensembliste, en particulier l'addition, la soustraction, l'union et l'intersection. Une méthode souvent utile est most_common, qui renvoie une liste de paires valeur-fréquence, triées de la plus fréquente à la plus rare :
>>>
decompte =
Counter
(
'perroquet'
)
>>>
for
val, freq in
decompte.most_common
(
3
):
... print
(
val, freq)
...
e 2
r 2
o 1
Le nombre passé en entrée à la fonction most_common (ici, 3) indique le nombre maximal de paires clé-valeur à parcourir. Si l'argument est manquant, la fonction most_common itérera sur l'ensemble des paires :
>>>
for
val, freq in
count.most_common
(
):
... print
(
val, freq)
...
e 2
r 2
o 1
q 1
p 1
u 1
t 1
19-7. Dictionnaire de type defaultdict▲
Le module collections propose aussi la fonctionnalité defaultdict, qui ressemble à un dictionnaire si ce n'est que, si vous accédez à une clé qui n'existe pas, il peut générer une nouvelle valeur à la volée.
Quand vous créez un dictionnaire defaultdict, vous fournissez une fonction qui est utilisée pour créer de nouvelles valeurs. Une fonction utilisée pour créer des objets s'appelle parfois une usine (factory). Les fonctions internes de création de listes, d'ensembles et d'autres types peuvent servir d'usines :
>>>
from
collections import
defaultdict
>>>
d =
defaultdict
(
list)
Notez que l'argument est list, qui est un objet de classe, et non list() qui serait une nouvelle liste. La fonction que vous fournissez n'est pas appelée tant que vous ne cherchez pas à accéder à une clé qui n'existe pas.
>>>
t =
d['nouvelle clé'
]
>>>
t
[]
La nouvelle liste, que nous appelons ici t, est également ajoutée au dictionnaire. Il en résulte que si nous modifions t, le changement se répercutera dans le dictionnaire d :
>>>
t.append
(
'nouvelle valeur'
)
>>>
d
defaultdict
(<
class
'list'
>
, {'nouvelle clé'
: ['nouvelle valeur'
]})
Si vous constituez un dictionnaire de listes, vous pouvez souvent écrire du code plus simple en utilisant des dictionnaires de type defaultdict. Dans ma solution à l'exercice 2 du chapitre 12, que vous pouvez télécharger à l'adresse anagram_sets.py, j'ai construit un dictionnaire établissant une correspondance entre une chaîne de caractères triés par ordre alphabétique et une liste de mots pouvant être écrits avec ces lettres. Par exemple, la chaîne de caractères 'ADEIR' pointe sur la liste de mots ['AIDER', 'ARIDE', 'RADIE', 'RAIDE']. Voici le code d'origine :
def
all_anagrams
(
nomfichier):
d =
{}
for
ligne in
open(
nomfichier):
mot =
ligne.strip
(
).lower
(
)
t =
signature
(
mot)
if
t not
in
d:
d[t] =
[mot]
else
:
d[t].append
(
mot)
return
d
Il est possible de simplifier cette fonction avec la fonctionnalité setdefault que vous avez peut-être utilisée dans l'exercice 2 du chapitre 11 :
def
all_anagrams
(
nomfichier):
d =
{}
for
ligne in
open(
nomfichier):
mot =
ligne.strip
(
).lower
(
)
t =
signature
(
mot)
d.setdefault
(
t, []).append
(
mot)
return
d
Cette solution présente cependant l'inconvénient de créer une nouvelle liste à chaque fois, même si elle n'est pas nécessaire. Pour des listes, ce n'est pas trop gênant. Mais si la fonction usine est complexe, ça peut le devenir.
Nous pouvons éviter cet écueil et simplifier le code en utilisant un defaultdict :
def
all_anagrams
(
nomfichier):
d =
defaultdict
(
list)
for
ligne in
open(
nomfichier):
mot =
ligne.strip
(
).lower
(
)
t =
signature
(
mot)
d[t].append
(
mot)
return
d
Ma solution à l'exercice 3 du chapitre 18, que vous pouvez télécharger à l'adresse PokerHandSoln.py utilise setdefault dans la fonction has_straightflush. Cette solution a le désavantage de créer un objet de type Hand à chaque itération dans la boucle, qu'il soit utile ou non. À titre d'exercice, réécrivez cette fonction en utilisant un defaultdict.
19-8. Tuples nommés▲
Beaucoup d'objets simples sont essentiellement des collections de valeurs apparentées. Par exemple, l'objet Point défini au chapitre 15Classes et objets contient deux nombres, x et y. Quand vous définissez une classe de ce genre, vous commencez souvent avec une méthode init et une méthode str :
class
Point:
def
__init__
(
self, x=
0
, y=
0
):
self.x =
x
self.y =
y
def
__str__
(
self):
return
'(
%g
,
%g
)'
%
(
self.x, self.y)
Cela représente beaucoup de code pour ne pas dire grand-chose. Il existe en Python une façon plus concise de dire la même chose :
from
collections import
namedtuple
Point =
namedtuple
(
'Point'
, ['x'
, 'y'
])
Le premier argument est le nom de la classe que vous désirez créer et le second une liste d'attributs dont les objets Point ont besoin, sous la forme de chaînes de caractères. La valeur de retour de la fonction namedtuple est un objet classe :
>>>
Point
<
class
'__main__.Point'
>
Point possède automatiquement des méthodes comme __init__ et __str__, si bien que vous n'avez pas besoin de les écrire.
Pour créer un objet Point, il suffit d'utiliser la classe Point comme une fonction :
>>>
p =
Point
(
1
, 2
)
>>>
p
Point
(
x =
1
, y =
2
)
La méthode init affecte les arguments aux attributs en utilisant les noms que vous avez fournis. La méthode str affiche une représentation de l'objet de type Point et de ses attributs.
Vous pouvez également accéder aux éléments du tuple nommé par leur nom :
>>>
p.x, p.y
(
1
, 2
)
Mais vous pouvez aussi manipuler un tuple nommé comme un tuple :
>>>
p[0
], p[1
]
(
1
, 2
)
>>>
x, y =
p
>>>
x, y
(
1
, 2
)
Les tuples nommés fournissent une façon rapide de définir des classes simples. Le problème est que les classes simples ne restent pas toujours simples. Il se peut que vous vouliez ultérieurement ajouter des méthodes à un tuple nommé. Dans ce cas, vous pourriez définir une nouvelle classe héritant du tuple nommé :
class
Point_plus_riche
(
Point):
# ajoutez de nouvelles méthodes ici
Ou alors vous pourriez décider de passer à une définition de classe conventionnelle.
19-9. Assembler des arguments avec mot-clé▲
Nous avons vu à la section 12.4Arguments tuples à longueur variable comment écrire une fonction qui assemble ses arguments dans un tuple :
def
affiche_tout
(*
args):
print
(
args)
Vous pouvez appeler cette fonction avec un nombre quelconque d'arguments positionnels (c'est-à-dire d'arguments n'utilisant pas de mots-clés).
>>>
affiche_tout
(
1
, 2.0
, '3'
)
(
1
, 2.0
, '3'
)
Mais l'opérateur * ne peut pas assembler des arguments dotés de mots-clés :
>>>
affiche_tout
(
1
, 2.0
, troisieme=
'3'
)
TypeError
: printall
(
) got an unexpected keyword argument 'troisieme'
Pour assembler des arguments avec mots-clés, vous pouvez utiliser l'opérateur ** :
def
affiche_tout
(*
args, **
kwargs):
print
(
args, kwargs)
Vous pouvez choisir le nom qu'il vous plaira pour le paramètre d'assemblage des arguments avec mots-clés, mais il est usuel de l'appeler kwargs. Le résultat est un dictionnaire établissant une correspondance entre les mots-clés et les valeurs.
>>>
affiche_tout
(
1
, 2.0
, troisieme=
'3'
)
(
1
, 2.0
) {'troisieme'
: '3'
}
Si vous avez un dictionnaire de mots-clés et de valeurs, vous pouvez utiliser l'opérateur de dispersion ** pour appeler une fonction :
>>>
d =
dict(
x=
1
, y=
2
)
>>>
Point
(**
d)
Point
(
x=
1
, y=
2
)
Sans cet opérateur, la fonction traiterait d comme un argument positionnel unique, et affecterait donc d à x, et se plaindrait ensuite qu'il n'y ait rien à affecter à y :
>>>
d =
dict(
x=
1
, y=
2
)
>>>
Point
(
d)
Traceback (
most recent call last):
File "<stdin>"
, line 1
, in
<
module>
TypeError
: __new__
(
) missing 1
required positional argument: 'y'
Quand vous utilisez des fonctions qui ont un nombre élevé de paramètres, il est souvent utile de créer et de passer en paramètre des dictionnaires qui spécifient les options fréquemment utilisées.
19-10. Glossaire▲
- expression conditionnelle : une expression qui peut prendre une valeur parmi deux possibles, en fonction d'une condition.
- liste en compréhension : une expression avec une boucle for entre crochets qui produit une nouvelle liste.
- générateur : une expression avec une boucle for entre parenthèses qui produit un objet générateur.
- multiensemble : une entité mathématique qui établit une correspondance entre les éléments d'un ensemble et le nombre d'occurrences de chacun de ces éléments.
- usine : une fonction, habituellement passée en paramètre, utilisée pour créer des objets.
19-11. Exercices▲
Exercice 1
La fonction suivante calcule récursivement les coefficients binomiaux :
def
coeff_binomial
(
n, k):
"""Calcule le coefficient binomial "k parmi n".
n: nombre d'essais
k: nombre de succès
renvoie : int
"""
if
k ==
0
:
return
1
if
n ==
0
:
return
0
resultat =
coeff_binomial
(
n-
1
, k) +
coeff_binomial
(
n-
1
, k-
1
)
return
resultat
Réécrivez le corps de la fonction en utilisant des expressions conditionnelles imbriquées.
Remarque : cette fonction n'est pas très efficace parce qu'elle finit par recalculer encore et encore les mêmes valeurs. Vous pourriez la rendre plus efficace en la mémoïsant (mise en cache de ces valeurs, voir section 11.6Mémos). Mais vous découvrirez sans doute qu'elle est plus difficile à mémoïser si vous l'écrivez avec des expressions conditionnelles.