Aller au contenu

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]
ou un générateur :

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