Comprendre et utiliser Python et le Functional Programming

Si le célèbre langage de programmation Python est plutôt connu pour la programmation orientée objet (OOP), il se prête aussi à la programmation fonctionnelle. Découvrirez quelles sont les fonctions disponibles et comment les utiliser.

Qu’est-ce qui caractérise la programmation fonctionnelle ?

Le terme « programmation fonctionnelle » désigne un type de programmation qui utilise les fonctions comme unité de base du code. Il existe une gradation entre les langages purement fonctionnels (comme Haskell ou Lisp) et les langages qui se basent sur plusieurs paradigmes, comme Python. La frontière entre les langages qui prennent en charge ou non la programmation fonctionnelle est donc assez fluide.

Pour qu’un langage prenne en charge la programmation fonctionnelle, 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 deviennent des objets au même titre que les chaînes de caractères, les nombres et les listes. Ces fonctions peuvent servir de paramètres à d’autres fonctions, ou être renvoyées en tant que valeurs de retour d’autres fonctions.

La programmation fonctionnelle est déclarative

La programmation dite déclarative consiste à décrire un problème et à laisser l’environnement de programmation trouver la solution. À l’opposé, la programmation dite impérative consiste à décrire pas à pas le chemin à suivre vers la solution. La programmation fonctionnelle constitue une partie de l’approche dite déclarative, et Python permet de suivre les deux paradigmes.

Prenons un exemple concret en Python. En partant d’une liste de nombres, on cherche à calculer leurs carrés correspondants. Voici la méthode selon l’approche impérative :

# 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 Comprehensions, Python adopte une approche déclarative qui se combine bien avec les techniques fonctionnelles. 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 indentations :

# 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 procédures

Une Pure Function, ou « fonction pure », peut se comparer aux fonctions mathématiques de base. Ce terme désigne une fonction qui présente les caractéristiques 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 secondaires.

Pour faire court, ces propriétés signifient que l’appel à une fonction pure n’entraîne pas de changement dans l’environnement système. L’exemple classique de la fonction carré f(x) = x * x peut être facilement 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 procé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 différence cependant : une procédure ne renvoie aucune valeur. Au lieu de cela, la procédure accède directement à des variables non locales pour les modifier au besoin.

En langage C et Java, les procé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’instruction return, le programme renvoie la valeur spéciale « None ». Quand on parle de procédure dans Python, on parle de fonction sans instruction 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’environnement 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 secondaire :

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

La récursion comme alternative à l’itération

Dans la programmation fonctionnelle, la récursivité est l’équivalent 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 fonctionne sans que la fonction ne boucle à l’infini, deux conditions 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écursives. Voici en exemple célèbre le calcul de la séquence de Fibonacci par une approche dite « naïve », peu performante 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 Functional Programming : vont-ils bien ensemble ?

Python est un langage multi-paradigmes, c’est-à-dire que l’écriture du code peut suivre différents paradigmes de programmation. En plus de la programmation fonctionnelle, il est aussi possible d’utiliser sans problème la programmation orientée objet en Python.

Python dispose de nombreux outils dédiés à la programmation fonctionnelle. Cependant, contrairement aux langages purement fonctionnels comme Haskell, leur étendue reste limitée. Le degré de programmation fonctionnelle d’un programme Python dépend en premier lieu de la personne chargée du développement. Voici un aperçu des principales caractéristiques fonctionnelles de Python.

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

La règle de Python : « Everything is an object » (traduction : 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 programmation d’une calculatrice qui prend en charge différentes opérations mathématiques.

En premier lieu, voici l’approche impérative. Celle-ci utilise les outils classiques de la programmation structurée, tels que les branchements conditionnels et les instructions d’affectation :

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

Considérons maintenant une approche déclarative pour résoudre ce même problème. Au lieu de la branche if, nous représentons les opérations sous forme de dict Python. Les symboles des opérations sont des clés qui renvoient aux objets de fonction correspondants, que nous importons du module operator. Le code qui en résulte est plus clair et ne nécessite pas de branchement :

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éclarative calculate. Les instructions assert montrent que le code fonctionne :

# 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éfinissent par le mot-clé def, le langage reconnaît aussi les « lambdas ». Il s’agit de fonctions courtes et anonymes qui définissent une expression avec des paramètres. Les lambdas peuvent être utilisées partout où une fonction est attendue, ou être liées à un nom par affectation :

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érations disponibles dans la fonction, nous passons un dict avec des fonctions lambda comme valeurs. Cela permet d’ajouter facilement de nouvelles opérations 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 transformés sans passer par des boucles. La fonction map() utilise comme paramètres une fonction et un itérable, et exécute la fonction pour chaque élément de l’itérable. Voyons comment cela fonctionne avec la génération 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 uniquement 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, compréhensions et générateurs

Les itérables constituent un concept-clé de Python : il s’agit d’une abstraction de collections dont les éléments peuvent être affichés individuellement. Il peut s’agir de chaînes de caractè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 compréhensions. Celles-ci se prêtent bien à la programmation fonctionnelle et ont largement remplacé l’utilisation 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 fonctionnels, Python offre une approche d’évaluation paresseuse avec les générateurs. Cela signifie que la génération de données n’a lieu qu’au moment de l’accès, ce qui permet notamment d’économiser beaucoup de mémoire. Voici une expression de générateur 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’instruction yield permet de gérer des évaluations paresseuses 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

Alternatives à Python pour le Functional Programming

Très populaire, la programmation fonctionnelle s’est établie comme le plus important contre-courant face à la programmation orientée objet. La combinaison de structures de données immuables (« immutable») avec des fonctions pures donne un code facile à paralléliser. La programmation fonctionnelle est ainsi très intéressante pour la transformation de données en pipelines de données.

Parmi les plus appréciés, on retrouve les langages purement fonctionnels fortement typés comme Haskell ou Clojure, le dialecte de Lisp. JavaScript est, lui aussi, considéré comme un langage fonctionnel par essence. TypeScript constitue une alternative moderne avec un typage fort.

Conseil

Vous souhaitez travailler en ligne avec Python ? Profitez d’un hébergement Web top niveau pour votre projet !