Fonctions anonymes en Python (ou lambda)


Python a des capacités de programmation fonctionnelle honorables, mais loin de ce que peuvent offrir des langages spécialisés comme Javascript ou Lisp.

Notamment, en JS, on peut créer des fonctions anonymes (appelée aussi ‘lambdas’), c’est à dire un bloc de code réutilisable comme une fonction, appelable comme une fonction, mais sans nom:

function(){
    alert('Le Chateau de Aaarrrgh')
}

A quoi ça sert ?

Concrètement, à rien. Il n’existe aucune opération qu’on fasse avec une fonction anonyme qu’on ne puisse faire avec une fonction normale. On utilise les fonctions anonymes par goût, pour des raisons de style.

En effet, les lambdas sont très pratiques pour créer des fonctions jetables: quand on a besoin d’une fonction, mais que l’on ne va l’utiliser qu’une seule fois. Car on peut définir et utiliser une fonction anonyme presque d’une traite, ce qui évite l’écriture en deux temps.

Par exemple en JS, avec jQuery vous allez faire ça:

// quand on clic sur une lien, faire un alert
$('a').click(function(){
    alert("C'est chiant hein ?")
})

Ici le bloc:

function(){
    alert("C'est chiant hein ?")
}

Est une fonction anonyme. On ne compte pas la réutiliser, donc inutile de la mettre à part: on la définit et on la passe en callback tout de suite.

Les lambdas en Python

Python possède ce genre de fonctionnalité, à l’aide du mot clé lambda. Une fonction:

def gratter(sujet):
    return "Je me gratte %s" % sujet

Peut aussi s’écrire:

gratter = lambda sujet: "Je me gratte %s" % sujet

C’est exactement la même chose, seule la syntaxe change:

  • lambda au lieu de def;
  • pas de paranthèses;
  • pas de mot clé return.

Ce qui est pratique, c’est qu’on peut définir la fonction à la volée. Par exemple, supposons que vous souhaitiez créer un mapping de fonctions de décompression:

import bz2
import zlib
from base64 import decodestring
from collections import defaultdict
 
def ne_fait_rien(x):
    return x
 
def decompresse_bz(x):
    return bz2.decompress(decodestring(x)).decode('utf8')
 
def decompresse_zip(x):
    return zlib.decompress(decodestring(x)).decode('utf8')
 
def retourne_ne_fait_rien():
    return ne_fait_rien
 
decompresseur = defaultdict(retourne_ne_fait_rien)
decompresseur['bz'] = decompresse_bz
decompresseur['zip'] = decompresse_zip

Et ça s’utilise comme ça:

resultat = decompresseur[format](data)

Pratique si vous avez un script qui va décompresser un max de données venues de l’extérieur et qui annoncent leur format sous forme de string. Au pire des cas, si vous ne connaissez pas le format, ça ne fait rien.

Maintenant voyons la même chose avec des fonctions anonymes:

import bz2
import zlib
from base64 import decodestring
from collections import defaultdict
 
decompresseur = defaultdict(lambda: ne_fait_rien)
decompresseur['bz'] = lambda x: bz2.decompress(decodestring(x)).decode('utf8')
decompresseur['zip'] = lambda x: zlib.decompress(decodestring(x)).decode('utf8')

Comme les lambdas peuvent être définies n’importe où, le code est beaucoup plus court. On peut décrire la logique de notre code directement là où on en a besoin, et en l’occurrence, on se fiche d’avoir la fonction sous son état ordinaire.

Les limites des lambdas

Guido a bridé volontairement les lambdas en Python:

  • On ne peut les écrire que sur une ligne.
  • On ne peut pas avoir plus d’une instruction dans la fonction.

Difficile, donc, de se la jouer full nested pendant tout le script comme on ferait en Haskell.

Une autre limite vient du fait que le système de portée de Python est lexical. Ainsi:

ajouteurs= range(4)
for i in range(4):
   ajouteurs[i] = lambda a: i + a

Qui devrait produire:

>>> print ajouteurs[3](3)
6
>>> print ajouteurs[2](3)
5

Produit en fait:

>>> print ajouteurs[3](3)
6
>>> print ajouteurs[2](3)
6

Pour contourner cela, il faut forcer Python a recréer un scope à chaque création de lambda:

>>> for i in range(4):
...    ajouteurs [i] = lambda a, i=i: i + a  
...
>>> print( ajouteurs[2](3))
5

Cela fonctionne en passant i en tant que valeur par défaut d’un paramètre du même nom.

12 thoughts on “Fonctions anonymes en Python (ou lambda)

    • Sam Post author

      Un exemple, un exemple… C’est clair que ça manque de map, filter et tout le toutim, mais underscore.js permet de remplacer tout ça.

      J’ai pris javascript, non pas parcequ’il arrive à tenir tête à Lisp ou Haskell sur ce terrain, mais parceque c’est le langage fonctionnel que le plus de gens connaissent. Et je me vois mal donner le même exemple en C.

  • Claudy

    Bijour, l’exemple de la fin avec ‘ajouteurs’ ne fonctionne pas :(

    >IndexError: list assignment index out of range<

    Je voulais tester etant donne que je n'ai pas tres bien saisi la notion de portee lexicale dans ce contexte !

  • Sam Post author

    J’ai oublié d’initialiser la liste d’ajouteurs. Ca marche maintenant :-)

  • Sam Post author

    J’ai trouvé ça dans les stats:

    python fonctions anonymes sans lambda

    A tout ceux qui cherchent ça, il n’y a PAS de moyen de faire des fonctions anonymes en Python sans lambda.

  • CactusLibidineux

    Les fonctions anonymes en Javascript peuvent en au moins un cas être nécessaires : les bookmarklets.
    En effet, pour ne pas risquer de rentrer en conflit avec le JS de la page (nom de fonction déjà utilisée), utiliser une fonction anonyme est la seule solution 100% fiable.

  • bipede

    “Il n’existe aucune opération qu’on fasse avec une fonction anonyme qu’on ne puisse faire avec une fonction normale”

    Pas tout à fait d’accord…

    Une fonction lambda est irremplacable dans certaines opérations, comme par exemple l’argument cmp des fonctions sorted() et list.sort() .

  • Sam Post author

    L’argument cmp est deprecated et n’existe plus en Python 3.

    Mais même pour l’argument cmp en Python 2, on peut utiliser sans problème une fonction normale. Par ailleurs, le module operator fournie des fonctions toutes faites pour la plupart des use cases.

  • Bruno Flament

    Bonjour Sam ou Max ou les deux

    Dans ton exemple il y a ca :

    decompresseur = defaultdict(lambda x: x)

    Si je test cette fonction j’ai une l’erreur suiviante

    >>>      d = defaultdict(lambda x:x)
     
    >>>      d['gz']
     
          Traceback (most recent call last):
     
          File "", line 1, in 
     
          TypeError: () takes exactly 1 argument (0 given)

    Ne faut-il pas plutot utiliser lambda: None ?

    decompresseur = defaultdict(lambda: None)

    merci Sam ou Max ou les deux

  • Sam Post author

    En effet il y a une erreur. J’ai corrigé pour : d = defaultdict(lambda: ne_fait_rien)

  • EtienneEtienneH

    Article très intéressant… j’ai presque tout compris.

    J’avais oublié depuis mes cours à la fac ce qu’étaient les fonctions lambda. Et je m’aperçoit que j’en utilise parfois sans le savoir.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.