Si le célèbre langage de pro­gram­ma­tion Python est plutôt connu pour la pro­gram­ma­tion orientée objet (OOP), il se prête aussi à la pro­gram­ma­tion fonc­tion­nelle. Dé­cou­vri­rez quelles sont les fonctions dis­po­nibles et comment les utiliser.

Qu’est-ce qui ca­rac­té­rise la pro­gram­ma­tion fonc­tion­nelle ?

Le terme « pro­gram­ma­tion fonc­tion­nelle » désigne un type de pro­gram­ma­tion qui utilise les fonctions comme unité de base du code. Il existe une gradation entre les langages purement fonc­tion­nels (comme Haskell ou Lisp) et les langages qui se basent sur plusieurs pa­ra­digmes, comme Python. La frontière entre les langages qui prennent en charge ou non la pro­gram­ma­tion fonc­tion­nelle est donc assez fluide.

Pour qu’un langage prenne en charge la pro­gram­ma­tion fonc­tion­nelle, il doit traiter les fonctions comme des objets de première classe (en anglais first-class citizens). C’est le cas dans Python, où les fonctions de­vien­nent des objets au même titre que les chaînes de ca­rac­tères, les nombres et les listes. Ces fonctions peuvent servir de pa­ra­mètres à d’autres fonctions, ou être renvoyées en tant que valeurs de retour d’autres fonctions.

La pro­gram­ma­tion fonc­tion­nelle est dé­cla­ra­tive

La pro­gram­ma­tion dite dé­cla­ra­tive consiste à décrire un problème et à laisser l’en­vi­ron­ne­ment de pro­gram­ma­tion trouver la solution. À l’opposé, la pro­gram­ma­tion dite im­pé­ra­tive consiste à décrire pas à pas le chemin à suivre vers la solution. La pro­gram­ma­tion fonc­tion­nelle constitue une partie de l’approche dite dé­cla­ra­tive, et Python permet de suivre les deux pa­ra­digmes.

Prenons un exemple concret en Python. En partant d’une liste de nombres, on cherche à calculer leurs carrés cor­res­pon­dants. Voici la méthode selon l’approche im­pé­ra­tive :

# Calculate squares from list of numbers
def squared(nums):
    # Start with empty list
    squares = []
    # Process each number individually
    for num in nums:
        squares.append(num ** 2)
    return squares
Python

Avec les List Com­pre­hen­sions, Python adopte une approche dé­cla­ra­tive qui se combine bien avec les tech­niques fonc­tion­nelles. Il est possible de créer la liste des carrés sans boucle explicite. Le code qui en résulte est nettement plus léger et sans in­den­ta­tions :

# Numbers 0–9
nums = range(10)
# Calculate squares using list expression
squares = [num ** 2 for num in nums]
# Show that both methods give the same result
assert squares == squared(nums)
Python

Primauté des fonctions pures sur les pro­cé­dures

Une Pure Function, ou « fonction pure », peut se comparer aux fonctions ma­thé­ma­tiques de base. Ce terme désigne une fonction qui présente les ca­rac­té­ris­tiques suivantes :

• La fonction arrive au même résultat pour les mêmes arguments ;

• La fonction n’a accès qu’à ses arguments ;

• La fonction ne déclenche pas d’effets se­con­daires.

Pour faire court, ces pro­prié­tés sig­ni­fient que l’appel à une fonction pure n’entraîne pas de chan­ge­ment dans l’en­vi­ron­ne­ment système. L’exemple classique de la fonction carré f(x) = x * x peut être fa­ci­le­ment mis en œuvre en tant que fonction pure dans Python :

def f(x):
    return x * x
# let’s test
assert f(9) == 81
Python

Les pro­cé­dures, très utilisées dans les anciens langages comme Pascal ou Basic, s’opposent aux fonctions pures. Tout comme la fonction, la procédure est un bloc de code avec un nom, qui peut être appelé plusieurs fois. Avec une dif­fé­rence cependant : une procédure ne renvoie aucune valeur. Au lieu de cela, la procédure accède di­rec­te­ment à des variables non locales pour les modifier au besoin.

En langage C et Java, les pro­cé­dures sont réalisées sous forme de fonction avec le type de retour void. Avec Python, les fonctions renvoient toujours une valeur : s’il n’y a pas d’ins­truc­tion return, le programme renvoie la valeur spéciale « None ». Quand on parle de procédure dans Python, on parle de fonction sans ins­truc­tion return.

Voici quelques exemples de fonctions pures et impures dans Python. La fonction suivante est impure car elle renvoie un résultat différent à chaque appel :

# Function without arguments
def get_date():
    from datetime import datetime
    return datetime.now()
Python

La procédure suivante est elle aussi impure car elle accède à des données définies en dehors de la fonction :

# Function using non-local value
name = 'John'
def greetings_from_outside():
    return(f"Greetings from {name}")
Python

La fonction suivante est impure car elle modifie un argument mutable à son appel et affecte donc l’en­vi­ron­ne­ment système :

# Function modifying argument
def greetings_from(person):
    print(f"Greetings from {person['name']}")
    # Changing 'person' defined somewhere else
    person['greeted'] = True
    return person
# Let’s test
person = {'name': "John"}
# Prints 'John'
greetings_from(person)
# Data was changed from inside function
assert person['greeted']
Python

La fonction suivante est pure car elle donne le même résultat pour le même argument sans effet se­con­daire :

# Pure function
def squared(num):
    return num * num
Python

La récursion comme al­ter­na­tive à l’itération

Dans la pro­gram­ma­tion fonc­tion­nelle, la ré­cur­si­vité est l’équi­valent de l’itération. Une fonction récursive s’appelle elle-même de manière répétée jusqu’à obtention du résultat. Pour que cela fonc­tionne sans que la fonction ne boucle à l’infini, deux con­di­tions doivent être remplies :

  1. La récursion doit avoir une condition d’arrêt ;
  2. L’exécution récursive de la fonction doit mener à réduire le problème.

Python prend en charge les fonctions ré­cur­sives. Voici en exemple célèbre le calcul de la séquence de Fibonacci par une approche dite « naïve », peu per­for­mante pour les grandes valeurs de n mais qui peut être optimisée par la mise en cache :

def fib(n):
    if n == 0 or n == 1:
        return n
    else:
        return fib(n - 2) + fib(n - 1)
Python

Python et Func­tio­nal Pro­gram­ming : vont-ils bien ensemble ?

Python est un langage multi-pa­ra­digmes, c’est-à-dire que l’écriture du code peut suivre dif­fé­rents pa­ra­digmes de pro­gram­ma­tion. En plus de la pro­gram­ma­tion fonc­tion­nelle, il est aussi possible d’utiliser sans problème la pro­gram­ma­tion orientée objet en Python.

Python dispose de nombreux outils dédiés à la pro­gram­ma­tion fonc­tion­nelle. Cependant, con­trai­re­ment aux langages purement fonc­tion­nels comme Haskell, leur étendue reste limitée. Le degré de pro­gram­ma­tion fonc­tion­nelle d’un programme Python dépend en premier lieu de la personne chargée du dé­ve­lop­pe­ment. Voici un aperçu des prin­ci­pales ca­rac­té­ris­tiques fonc­tion­nelles de Python.

Dans Python, les fonctions sont des objets de première classe

La règle de Python : « Eve­ry­thing is an object » (tra­duc­tion : tout est objet). C’est aussi le cas pour les fonctions. Elles peuvent être utilisées partout dans le langage, tout du moins là où les objets sont autorisés. Voyons un exemple concret : la pro­gram­ma­tion d’une cal­cu­la­trice qui prend en charge dif­fé­rentes opé­ra­tions ma­thé­ma­tiques.

En premier lieu, voici l’approche im­pé­ra­tive. Celle-ci utilise les outils clas­siques de la pro­gram­ma­tion struc­tu­rée, tels que les bran­che­ments con­di­tion­nels et les ins­truc­tions d’af­fec­ta­tion :

def calculate(a, b, op='+'):
    if op == '+':
        result = a + b
    elif op == '-':
        result = a - b
    elif op == '*':
        result = a * b
    elif op == '/':
        result = a / b
    return result
Python

Con­si­dé­rons main­te­nant une approche dé­cla­ra­tive pour résoudre ce même problème. Au lieu de la branche if, nous re­pré­sen­tons les opé­ra­tions sous forme de dict Python. Les symboles des opé­ra­tions sont des clés qui renvoient aux objets de fonction cor­res­pon­dants, que nous importons du module operator. Le code qui en résulte est plus clair et ne nécessite pas de bran­che­ment :

def calculate(a, b, op='+'):
    # Import operator functions
    import operator
    # Map operation symbols to functions
    operations = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
    }
    # Choose operation to carry out
    operation = operations[op]
    # Run operation and return results
    return operation(a, b)
Python

Il ne reste plus qu’à tester la fonction dé­cla­ra­tive calculate. Les ins­truc­tions assert montrent que le code fonc­tionne :

# Let’s test
a, b = 42, 51
assert calculate(a, b, '+') == a + b
assert calculate(a, b, '-') == a - b
assert calculate(a, b, '*') == a* b
assert calculate(a, b, '/') == a / b
Python

Sous Python, les lambdas sont des fonctions anonymes

Si les fonctions sous Python se dé­fi­nis­sent par le mot-clé def, le langage reconnaît aussi les « lambdas ». Il s’agit de fonctions courtes et anonymes qui dé­fi­nis­sent une ex­pres­sion avec des pa­ra­mètres. Les lambdas peuvent être utilisées partout où une fonction est attendue, ou être liées à un nom par af­fec­ta­tion :

squared = lambda x: x * x
assert squared(9) == 81
Python

À l’aide de lambdas, nous pouvons améliorer la fonction calculate. Au lieu de coder en dur les opé­ra­tions dis­po­nibles dans la fonction, nous passons un dict avec des fonctions lambda comme valeurs. Cela permet d’ajouter fa­ci­le­ment de nouvelles opé­ra­tions par la suite :

def calculate(a, b, op, ops={}):
    # Get operation from dict and define noop for non-existing key
    operation = ops.get(op, lambda a, b: None)
    return operation(a, b)
# Define operations
operations = {
    '+': lambda a, b: a + b,
    '-': lambda a, b: a - b,
}
# Let’s test
a, b, = 42, 51
assert calculate(a, b, '+', operations) == a + b
assert calculate(a, b, '-', operations) == a - b
# Non-existing key handled gracefully
assert calculate(a, b, '**', operations) == None
# Add a new operation
operations[‘**’] = lambda a, b: a** b
assert calculate(a, b, '**', operations) == a** b
Python

Fonction d’ordre supérieur dans Python

Les lambdas sont beaucoup utilisés pour des fonctions d’ordre supérieur comme map() et filter(). Ainsi, les éléments d’un itérable peuvent être trans­for­més sans passer par des boucles. La fonction map() utilise comme pa­ra­mètres une fonction et un itérable, et exécute la fonction pour chaque élément de l’itérable. Voyons comment cela fonc­tionne avec la gé­né­ra­tion de nombres carrés :

nums = [3, 5, 7]
squares = map(lambda x: x ** 2, nums)
assert list(squares) == [9, 25, 49]
Python
Note

Les fonctions d’ordre supérieur (en anglais higher-order functions) sont des fonctions qui se basent sur des paramètre fonctions ou qui renvoient comme valeur une fonction.

La fonction filter() permet de filtrer les éléments d’un itérable. Reprenons l’exemple pour générer uni­que­ment les carrés pairs :

nums = [1, 2, 3, 4]
squares = list(map(lambda num: num ** 2, nums))
even_squares = filter(lambda square: square % 2 == 0, squares)
assert list(even_squares) == [4, 16]
Python

Itérables, com­pré­hen­sions et gé­né­ra­teurs

Les itérables cons­ti­tuent un concept-clé de Python : il s’agit d’une abs­trac­tion de col­lec­tions dont les éléments peuvent être affichés in­di­vi­duel­le­ment. Il peut s’agir de chaînes de ca­rac­tères (strings), de tuples, de listes et de dicts qui suivent tous les mêmes règles. Par exemple, la fonction len() permet d’obtenir la taille d’un itérable :

name = 'Walter White'
assert len(name) == 12
people = ['Jim', 'Jack', 'John']
assert len(people) == 3
Python

Bâties à partir des itérables, on peut aussi utiliser les com­pré­hen­sions. Celles-ci se prêtent bien à la pro­gram­ma­tion fonc­tion­nelle et ont largement remplacé l’uti­li­sa­tion des lambdas avec map() et filter().

# List comprehension to create first ten squares
squares = [num ** 2 for num in range(10)]
Python

Comme pour les langages purement fonc­tion­nels, Python offre une approche d’éva­lua­tion pa­res­seuse avec les gé­né­ra­teurs. Cela signifie que la gé­né­ra­tion de données n’a lieu qu’au moment de l’accès, ce qui permet notamment d’éco­no­mi­ser beaucoup de mémoire. Voici une ex­pres­sion de gé­né­ra­teur qui calcule chaque nombre au carré lors de l’accès :

# Generator expression to create first ten squares
squares = (num ** 2 for num in range(10))
Python

L’ins­truc­tion yield permet de gérer des éva­lua­tions pa­res­seuses dans Python. Voici une fonction qui retourne les nombres positifs jusqu’à une limite donnée :

def N(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1
Python

Al­ter­na­tives à Python pour le Func­tio­nal Pro­gram­ming

Très populaire, la pro­gram­ma­tion fonc­tion­nelle s’est établie comme le plus important contre-courant face à la pro­gram­ma­tion orientée objet. La com­bi­nai­son de struc­tures de données immuables (« immutable») avec des fonctions pures donne un code facile à pa­ral­lé­li­ser. La pro­gram­ma­tion fonc­tion­nelle est ainsi très in­té­res­sante pour la trans­for­ma­tion de données en pipelines de données.

Parmi les plus appréciés, on retrouve les langages purement fonc­tion­nels fortement typés comme Haskell ou Clojure, le dialecte de Lisp. Ja­vaS­cript est, lui aussi, considéré comme un langage fonc­tion­nel par essence. Ty­peS­cript constitue une al­ter­na­tive moderne avec un typage fort.

Conseil

Vous souhaitez tra­vail­ler en ligne avec Python ? Profitez d’un hé­ber­ge­ment Web top niveau pour votre projet !

Aller au menu principal