Algo & Prog 1 Python : CM séance 08
8. Lambdas, tris, compléments
8.1 Les lambda-fonctions
Les lambda-fonctions sont des fonction anonymes, généralement très courtes, que l'on peut créer à l'exécution.
Le terme fait référence au \(\lambda\)-calcul, un système formel de 1930 où tout est fonction.
Elles sont très pratique lorsqu'on a besoin d'exprimer par exemple un critère lors d'un tri, voir plus loin.
Syntaxe :
f = lambda paramètres : expression donnant le résultat
Usage classique, comme tout appel de fonction :
résultat = f(paramètres)
Exemple : soit la fonction somme
def somme (a, b) :
return a+b
Sous la forme d'une lambda on pourra écrire
somme = lambda a, b : a+b
et pour l'appel, dans les deux cas :
s = somme (3, 5)
On peut aussi évaluer directement une lambda :
>>> (lambda x : x*x*x)(5) # 125
Une lambda peut avoir des paramètres optionnels :
somme = lambda a, b=0 : a+b
>>> somme (3,5) # 8
>>> somme (3) # 3
Ce mécanisme permet de créer une fermeture (uk : closure) d'une fonction, c'est-à-dire de mémoriser un paramètre :
somme = lambda a, b : a+b
incr = lambda c : lambda a, b=c : somme(a,b)
>>> f = incr(2)
>>> f(5) # 7
Dans cet exemple, la valeur de l'incrément c
est stockée comme valeur par
défaut dans le paramètre b
de la lambda renvoyée.
8.2. Les tris
8.2.1. Tri simple
Les listes ont une méthode sort()
qui modifie la liste :
jours = ['lundi', 'jeudi', 'mardi']
jours.sort()
>>> jours # ['jeudi', 'lundi', 'mardi']
Il existe aussi une fonction sorted()
qui renvoie une nouvelle liste :
jours = ['lundi', 'jeudi', 'mardi']
alpha = sorted(jours)
>>> alpha # ['jeudi', 'lundi', 'mardi']
>>> jours # ['lundi', 'jeudi', 'mardi']
La méthode sort()
n'est définie que pour les listes ;
la fonction sorted()
accepte n'importe quel itérable :
>>> sorted("salut") # ['a', 'l', 's', 't', 'u']
>>> sorted((6,5,0,2)) # [0, 2, 5, 6]
>>> sorted({'ga':0, 'bu':1, 'zo':2, 'meu':3}) # ['bu', 'ga', 'meu', 'zo']
une compréhension :
>>> sorted([i*7 % 11 for i in range(5)]) # [0, 3, 6, 7, 10]
>>> sorted((i*7 % 11 for i in range(5))) # [0, 3, 6, 7, 10]
Tri dans l'ordre décroissant : avec le paramètre reverse
jours = ['lundi', 'jeudi', 'mardi']
jours.sort(reverse=True)
>>> jours # ['mardi', 'lundi', 'jeudi']
>>> sorted("salut", reverse=True) # ['u', 't', 's', 'l', 'a']
8.2.2. Critère de tri
Le paramètre key
permet de définir un critère de tri, sous la forme
d'une fonction prenant un seul paramètre.
>>> sorted("PoWer") # ['P', 'W', 'e', 'o', 'r']
>>> sorted("PoWer", key=str.lower) # ['e', 'o', 'P', 'r', 'W']
>>> sorted([-2, 5, -7, 3]) # [-7, -2, 3, 5]
>>> sorted([-2, 5, -7, 3], key=abs) # [-2, 3, 5, -7]
La fonction critère sera appelée exactement une fois pour chaque élément à trier.
Les lambda fonctions sont particulièrement utiles pour définir un critère :
mois = { 'janvier':1, 'juin':6, 'octobre': 10, 'mai':5 }
>>> sorted(mois) # ['janvier', 'juin', 'mai', 'octobre']
>>> sorted(mois, key = lambda m: len(m))
# ['mai', 'juin', 'janvier', 'octobre']
>>> sorted(mois.items(), key=lambda m : m[1])
# [('janvier', 1), ('mai', 5), ('juin', 6), ('octobre', 10)]
On peut trier des objets sur des attributs :
-
soit avec une lambda :
class Pied : def __init__(self, pointure) : self.p = pointure def __repr__(self) : # pour rendre l'affichage lisible return str(self.p) >>> collec = [Pied(42), Pied(37), Pied(39), Pied(41)] >>> sorted(collec, key=lambda item : item.p) # [37, 39, 41, 42]
-
ou encore, en définissant
__lt__
:class Pied : def __init__(self, pointure) : self.p = pointure def __lt__(self, other) : return self.p < other.p def __repr__(self) : return str(self.p) >>> collec = [Pied(42), Pied(37), Pied(39), Pied(41)] >>> sorted(collec) # [37, 39, 41, 42]
8.2.3. Propriété de stabilité
Si on trie sur un critère 1, puis sur un critère 2, dans le résultat parmi les valeurs égales pour le critère 2, l'ordre du critère 1 sera conservé.
Exemple :
>>> l1 = [('a',1), ('c',3), ('a',6), ('b',3)]
>>> l2 = sorted(l1, key=lambda m:m[1])
# [('a', 1), ('c', 3), ('b', 3), ('a', 6)]
>>> l3 = sorted(l2, key=lambda m:m[0])
# [('a', 1), ('a', 6), ('b', 3), ('c', 3)]
→ l'ordre ('a', 1), ('a', 6)
sur les chiffres est conservé (stabilité).
Ceci donne un moyen de faire un tri multiple :
8.2.4. Critère multiple
On souhaite trier un itérable selon les critères \(C_1\), \(C_2\), ... \(C_n\).
- Première méthode :
- appeler
sorted
pour \(C_n\), puis \(C_{n-1}\), ..., enfin pour \(C_1\).
Exemple : on veut trier sur année, mois, jour
d = [ { 'annee' : 2024, 'mois' : 10, 'jour' : 7},
{ 'annee' : 2023, 'mois' : 10, 'jour' : 5},
{ 'annee' : 2020, 'mois' : 8, 'jour' : 11},
{ 'annee' : 2024, 'mois' : 8, 'jour' : 2},
{ 'annee' : 2024, 'mois' : 10, 'jour' : 9} ]
d = sorted (d, key = lambda item : item['jour'])
d = sorted (d, key = lambda item : item['mois'])
d = sorted (d, key = lambda item : item['annee'])
>>> d
[{'annee': 2020, 'mois': 8, 'jour': 11},
{'annee': 2023, 'mois': 10, 'jour': 5},
{'annee': 2024, 'mois': 8, 'jour': 2},
{'annee': 2024, 'mois': 10, 'jour': 7},
{'annee': 2024, 'mois': 10, 'jour': 9}]
- Deuxième méthode :
- exprimer le critère sous forme d'un tuple.
Exemple :
d = [ { 'annee' : 2024, 'mois' : 10, 'jour' : 7},
{ 'annee' : 2023, 'mois' : 10, 'jour' : 5},
{ 'annee' : 2020, 'mois' : 8, 'jour' : 11},
{ 'annee' : 2024, 'mois' : 8, 'jour' : 2},
{ 'annee' : 2024, 'mois' : 10, 'jour' : 9} ]
d = sorted (d, key = lambda item : (item['annee'], item['mois'],item['jour']))
>>> d
[{'annee': 2020, 'mois': 8, 'jour': 11},
{'annee': 2023, 'mois': 10, 'jour': 5},
{'annee': 2024, 'mois': 8, 'jour': 2},
{'annee': 2024, 'mois': 10, 'jour': 7},
{'annee': 2024, 'mois': 10, 'jour': 9}]
Avantages :
- pas besoin d'inverser l'ordre des critères ;
- plus lisible ;
- plus efficace (un seul tri).
8.3. Compléments
8.3.1. Itérer avec zip
La fonction zip
permet d'itérer en parallèle sur plusieurs itérables ;
la boucle s'arrête dès que la fin d'un des itérables est atteinte :
>>> for i, j, k in zip ([5,7,9], range(5), "abcd") :
print (i, j, k)
5 0 a
7 1 b
9 2 c
Pour comprendre comment ça marche on peut le simuler :
def myzip (*args) :
iterateurs = [ iter(arg) for arg in args ]
try :
while True :
yield [ i.__next__() for i in iterateurs ]
except StopIteration :
pass
On obtient bien la même chose :
>>> for i, j, k in myzip ([5,7,9], range(5), "abcd") :
print(i, j, k)
5 0 a
7 1 b
9 2 c
Dans la même veine, voici un itérateur intéressant, qui donne à chaque itération les valeurs de l'élément précédent, courant et suivant :
def prev_item_next (iterable):
iterateur = iter(iterable)
prev = None
try :
item = iterateur.__next__()
except StopIteration :
return
for next in iterateur:
yield (prev,item,next)
prev, item = item, next
yield (prev,item,None)
Usage :
>>> for prev, item, next in prev_item_next(["ga", "bu", "zo", "meu"]) :
print ("valeurs :", prev, item, next)
valeurs : None ga bu
valeurs : ga bu zo
valeurs : bu zo meu
valeurs : zo meu None
8.3.2. Annotations
Les annotations consistent à préciser les types attendus et renvoyés pour une fonction :
def saluer (nom: str) -> str :
return f"Bonjour {nom}"
On peut définir des alias de types :
Vecteur = list[float]
def scale(scalaire: float, vecteur: Vecteur) -> Vecteur:
return [scalaire * num for num in vecteur]
Les versions récentes de Python fournissent toutes sortes d'outils pour gérer
les annotations de types, dont l'introspection (dictionnaire __annotations__
à partir de Python 3.10).
Toutefois, l'interpréteur de Python ne vérifie rien, par exemple
>>> saluer (123)
'Bonjour 123'
ne déclenchera pas d'erreur alors qu'un str
était attendu.
En réalité, les annotations sont utiles dans certains IDE (environnements de développement intégrés), par exemple PyCharm ou Vscode, ou des outils de vérification tels que mypy.
8.3.3 Décorateurs
Les décorateurs sont un concept de programmation avancé. Le nom vient du patron de conception décorateur.
Un dédorateur permet de modifier une fonction ou un objet, en se substituant à lui ;
on reconnait son utilisation au symbole @
.
Exemple de définition d'un décorateur :
def mon_decorateur (f) :
def nouvelle_fonction (*args, **kwargs) :
print ("Début", f.__name__)
res = f (*args, **kwargs)
print ("Fin", f.__name__)
return res
return nouvelle_fonction
Exemple d'utilisation du décorateur :
@mon_decorateur
def somme (a, b) :
print (f"calcul de {a}+{b} ...")
return a+b
>>> somme (3, 5)
Début somme
calcul de 3+5 ...
Fin somme
8
@mon_decorateur
def incrementer (x, dx=1) :
print (f"incrémente {x} de {dx} ...")
>>> incrementer (5)
Début incrementer
incrémente 5 de 1 ...
Fin incrementer
>>> incrementer (4, 3)
Début incrementer
incrémente 4 de 3 ...
Fin incrementer
Il est également possible d'écrire des décorateurs avec des paramètres.