Du code, du communisme

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