Posts Tagged ‘Python 3’

Les fonctions anonymes lambda en Python : print, expressions conditionnelles et récursivité

samedi, décembre 24th, 2016

Si Python n’est pas un langage de programmation fonctionnelle, il possède cependant des fonctions anonymes lambda qui sont typiques de cette famille de langages. Ces fonctions sont réputées peu puissantes en Python car elle ont été volontairement limitées syntaxiquement à une expression, sans possibilité d’utiliser des instructions. Pourtant, nous allons voir qu’elles ont dans ce langage quelques particularités intéressantes.

Print

L’instruction print est devenue une fonction print() – et donc une expression – dans Python 3 suite à la PEP 3105. Ainsi, si vous utilisez print dans une fonction lambda, Python 2 lèvera une exception SyntaxError: invalid syntax alors que Python 3 l’acceptera :

>>> pr = lambda x : print(x)
>>> pr('OK en Python 3')
OK en Python 3

Expressions conditionnelles

Introduites (tardivement) dans Python 2.5 suite à la PEP 308, les expressions conditionnelles sont une manière simplifiée de réaliser grâce à l’opérateur ternaire true_value if condition else false_value la suite d’instructions suivante :

if condition:
    x = true_value
else:
    x = false_value

Comme leur nom l’indique, les expressions conditionnelles sont bien des expressions et elles permettent donc de mettre de la logique dans les fonctions lambda. Cela était déjà possible précédemment, en abusant un peu les opérateurs logiques classiques avec condition and true_value or false_value, mais c’est une méthode que je déconseille car elle n’est pas totalement fiable pour certaines valeurs de condition.

Dans l’exemple suivant, j’utilise une expression conditionnelle avec la fonction print() et donc Python 3, ce dernier me permettant d’utiliser un nom de variable avec le caractère non-ASCII é (PEP 3131) :

>>> majorité = lambda x : print("mineur") if x < 18 else print("majeur")
>>> majorité(15)
mineur
>>> majorité(25)
majeur

Récursivité

Le principe des fonctions anonymes étant de ne pas être nommées, il est donc logiquement difficile de les appeler. Ainsi, les fonctions anonymes de certains langages fonctionnels ne peuvent pas s’appeler, et donc ne peuvent pas être récursives. En Python, les lambda sont un sucre syntaxique limité des fonctions normales mais elles leur sont sémantiquement équivalentes, et elles peuvent donc parfaitement s’appeler récursivement.

En utilisant une expression conditionnelle et la récursivité on peut ainsi facilement implémenter l’algorithme récursif naïf de la très classique suite de Fibonacci :

>>> fib = lambda x : x if x < 2 else fib(x - 1) + fib(x - 2)
>>> fib(1)
1
>>> fib(10)
55
>>> fib(25)
75025

N’essayez pas d’aller beaucoup plus haut pour tester cet algorithme de complexité exponentielle, mais il démontre bien la puissance quelque peu surprenante des fonctions lambda en Python.

Compréhension de liste

On peut enfin ajouter que l’usage de compréhension de liste permet aisément de faire une boucle dans une fonction lambda :

>>> incr = lambda liste : [i + 1 for i in liste]
>>> incr([1, 45, 340])
[2, 46, 341]

La compréhension de dictionnaire en Python

samedi, octobre 5th, 2013

La compréhension de liste est une syntaxe moderne remplaçant les classiques fonctions map() et filter() de la programmation fonctionnelle. Ce sucre syntaxique est devenu un standard de la programmation moderne présent dans quasiment tous les langages.

La PEP 202, en intégrant cette fonctionnalité dans Python 2.0, n’était donc pas d’une originalité folle. Mais les développeurs de Python ne se sont pas arrêtés en si bon chemin, et ils ont eu l’intelligence de pousser le concept un peu plus loin. Ainsi, ils ont cherché à étendre la syntaxe de compréhension aux autres types conteneurs que les listes, c’est-à-dire en premier lieu aux dictionnaires.

La PEP 274 visait donc a intégrer la compréhension de dictionnaire à Python 2.3, mais elle a été abandonnée car considérée comme inutile au vu de l’utilisation conjointe des expressions de générateur et du constructeur dict().

Cependant, l’intérêt pour ce sucre syntaxique est resté présent, et la compréhension de dictionnaire a finalement été intégrée au langage à l’occasion de la grande réforme du passage à Python 3.0, et backportée dans Python 2.7.

On obtient ainsi plus de lisibilité :

>>> dict((i,i*2) for i in range(5))
{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}
>>> {i: i*2 for i in range(5)}
{0: 0, 1: 2, 2: 4, 3: 6, 4: 8}

et de flexibilité :

>>> k_and_v = [[1, 'a'], [2, 'b']]
>>> dict(k_and_v)
{1: 'a', 2: 'b'}
>>> {k: v for k, v in k_and_v}
{1: 'a', 2: 'b'}
>>> {k+1: v*2 for k, v in k_and_v}
{2: 'aa', 3: 'bb'}

Et puisque Python 3.0 a doté les sets d’une nouvelle syntaxe, il en a aussi profité pour les doter de la compréhension :

>>> [i%2 for i in range(8)] # liste
[0, 1, 0, 1, 0, 1, 0, 1]
>>> {i%2 for i in range(8)} # set
{0, 1}

Il n’est pas étonnant que la syntaxe de compréhension de liste se soit diffusée dans tous les langages de programmation modernes. Il n’est pas étonnant non plus que Python, un langage doté d’une philosophie très sensible à la lisibilité, soit allé plus loin en la généralisant à tous ses types conteneurs, en faisant un élément central du langage.

La syntaxe des sets en Python

jeudi, mars 14th, 2013

Pour comprendre la génèse de la syntaxe des sets, il faut étudier celle des types conteneurs historiques de Python qui sont :

  • les listes, délimitées par les crochets [ et ]
  • les tuples, délimités par les parenthèses ( et )
  • les dictionnaires, délimités par les accolades { et }

Ces types conteneurs sont donc dotés d’une syntaxe légère et facilement utilisable :

>>> l = [1, 2, 3]
>>> l
[1, 2, 3]
>>> t = (1, 2, 3)
>>> t
(1, 2, 3)
>>> d = {1: '1', 2: '2', 3: '3'}
>>> d
{1: '1', 2: '2', 3: '3'}

Les tuples, qui sont en fait des listes immuables et que l’on ne peut donc pas modifier, sont souvent oubliés car assez peu utilisés, en tout cas de manière consciente. En effet, une simple énumération sans syntaxe spécifique est en fait un tuple :

>>> e = 1, 2, 3
>>> e
(1, 2, 3)

Si l’on peut donc dire que globalement, les listes et les dictionnaires répondent à la grande majorité des besoins des programmeurs, un quatrième type conteneur s’est lentement mais sûrement fait une place au soleil des pythonistes : les sets.

Un set est un ensemble, c’est-à-dire une collection non ordonnée d’éléments uniques, ce qui se révèle très pratique dans beaucoup d’usages courants. Ce nouveau type conteneur étant fourni en deux saveurs, l’une mutable (comme les listes), et l’autre immuable (comme les tuples).

Suite à la PEP 218, il fut d’abord introduit dans Python 2.3 sous la forme d’un nouveau module sets ajouté à la bibliothèque standard, avant de devenir un type built-in dans Python 2.4. Cela représentait une amélioration syntaxique non négligeable, plus besoin d’écrire :

>>> import sets
>>> s_mutable = sets.Set([1, 2, 3])
>>> s_immutable = sets.ImmutableSet([1, 2, 3])

On pouvait dorénavant se contenter de :

>>> s_mutable = set([1,2,3])
>>> s_immutable = frozenset([1,2,3])

Et de bénéficier en plus d’une implémentation du type set en C et non plus en Python, avec la belle augmentation de performance qui va avec.

L’intégration des sets mutables (les plus utiles) dans le core du langage Python, au même niveau que les tuples, les listes et les dictionnaires, se heurtait encore à une limitation syntaxique : il fallait écrire set([1, 2, 3]). En effet, il n’y a que trois signes de ponctuation ASCII 7 bits fonctionnant par paire et facilement accessibles sur un clavier, les parenthèse, les crochets et les accolades, qui comme on l’a vu sont déjà utilisés respectivement par les tuples, les listes et les dictionnaires.

Mais que pouvait-on alors faire pour mieux intégrer syntaxiquement les sets à Python ? C’est dans Python 3.0 que la solution a été trouvée : si les tuples et les listes sont des énumérations que l’on ne pourrait distinguer des sets, les dictionnaires sont eux bien différents, et l’on peut donc utiliser les accolades { et } pour une énumération sans risque de confusion :

>>> s = {1, 2, 3}
>>> s
{1, 2, 3}

Il reste cependant une petite exception qui rend cette solution syntaxique imparfaite, et potentiellement génératrice d’erreurs ou d’incompréhensions, c’est le cas de l’ensemble vide. En effet, {} ne peut pas représenter à la fois le dictionnaire vide et le set vide :

>>> s = {}
>>> s
{}
>>> isinstance(s, set)
False
>>> isinstance(s, dict)
True

De manière logique et rétrocompatible, {} représente donc toujours le dictionnaire vide, et c’est set() qui permet de créer un set vide :

>>> s = set()
>>> s
set()
>>> s.add(1)
>>> s
{1}

Enfin, ce sucre syntaxique de Python 3 a été backporté dans Python 2.7.

 

Head et tail de liste en Python 3

samedi, avril 23rd, 2011

Bien que Python ne soit pas un langage de programmation fonctionnelle, il incorpore un certain nombre de fonctionnalités typiques de ces langages. Ainsi les fonctions anonymes lambda, la compréhension de liste, ou encore les fonctions built-in map(), filter() et reduce() (cette dernière ayant été supprimée dans Python 3).

La programmation fonctionnelle est caractérisée par une utilisation intensive des listes, et en particulier par de nombreuses opérations récursives sur ces dernières. On est donc souvent amené à vouloir récupérer le premier élément d’une liste, sa tête ou head, et l’ensemble des éléments restants, sa queue ou tail. Ce qui s’écrit donc trivialement avec la fonction f() retournant une liste :

head, tail = f()[0], f()[1:]

Le premier problème ici est le double appel de la fonction f(), que l’on peut facilement corriger grâce à l’utilisation d’une variable intermédiaire result :

result = f()
head, tail = result[0], result[1:]

Le second problème est la lourdeur syntaxique de l’ensemble, avec la variable intermédiaire et le double usage du [] (le __getitem__ des listes en Python). On aimerait bien à la place avoir une syntaxe plus adaptée, c’est-à-dire plus proche de celles utilisées par les langages de programmation fonctionnelle, comme par exemple Erlang :

[Head|Tail] = Result

Et comme toujours avec Python, le langage se bonifie grâce à une PEP (Python Enhancement Proposal), en l’occurence la PEP 3132 intitulée Extended Iterable Unpacking. Ainsi, depuis Python 3.0, on peut utiliser une nouvelle syntaxe qui reprend celle des star-args (*args) des fonctions Python :

head, *tail = result

Ceci se teste facilement dans un shell Python 3 :

>>> result = [1, 2, 3, 4]
>>> head, *tail = result
>>> head
>>> 1
>>> tail
>>> [2, 3, 4]

On a donc maintenant une syntaxe tout à fait satisfaisante, très lisible et facilitant un style de programmation fonctionnel. Ce qui est encore plus intéressant avec cette notation, c’est qu’elle ne se limite pas au couple head et tail, et peut ainsi être déclinée de bien des manières tout à fait intéressantes comme par exemple :

>>> result = [1, 2, 3, 4]
>>> head, *body, tail = result
>>> head
>>> 1
>>> body
>>> [2, 3]
>>> tail
>>> 4

Python 3 est sorti depuis plus de deux ans et, sans être révolutionnaire, il apporte de nombreuses améliorations à Python 2 qui valent le coup d’être découvertes et utilisées !