Les fonctions de première classe en Python

Les fonctions de première classe en Python

By BUSSER Arthur

15 avr. 2019

Catégories : Hack, Formation | Tags : Programmation, Python

J’ai récemment regardé une conférence de Dave Cheney sur les fonctions de première classe en Go. Sachant que Python est également capable de les prendre en charge, sont-elles utilisables de la même manière ? Absolument.

J’utilise Python depuis un moment maintenant. C’est un bon complément à Bash lorsqu’il est question d’intégrer une logique assez complexe dans des scripts. Le code en ressort plus propre et lisible lors de la manipulation de données simples.

Lorsqu’on apprend à utiliser un langage, il est important de garder à l’esprit ce qui ne peut uniquement être fait en utilisant ses fonctionnalités innées. Par exemple, apprendre à utiliser – et non pas abuser – les listes, pour cela une chose importante est de comprendre à quelles problématiques elles répondent. C’est une des règles crucial pour maîtriser Python, et plus globalement la programmation objet. Cet article explore ce qui peut être fait avec des fonctions de première classe en Python tout en conservant un code propre.

Tout d’abord, que signifie “Python supporte les fonctions de première classe” ? En termes simples, cela veut dire que les fonctions peuvent être utilisées comme s’il s’agissait de valeurs. Vous pouvez les stocker dans des variables, les passer à d’autres fonctions en tant qu’arguments, etc.

Dave Cheney a utilisé une calculatrice simple à titre d’exemple, commençons par reproduire celui-ci. En voici une implémentation possible :

class Calculator:

def __init__(self):
    self.num = 0.0

def get(self):
    return self.num

def add(self, n):
    self.num += n

def subtract(self, n):
    self.num -= n

def multiply(self, n):
    self.num *= n

Quelques connaissances en Python – plus généralement de n’importe quel langage orienté objet – suffisent à comprendre le code ci-dessus. Il peut être utilisé comme suit :

calc = Calculator()

calc.add(2)
calc.multiply(5)
calc.subtract(3)

print(calc.get())  # 7.0

Maintenant que la calculatrice fonctionne, ce code est importable dans n’importe quel script qui pourrait en avoir besoin. Mais qu’en est-il de la division sachant que la celle-ci n’est pas encore implémentée ? Quid du calcul d’un logarithme ? de factorielles ? Ces opérations pourraient facilement être ajoutées en écrivant de nouvelles méthodes dans la classe Calculator. Idéalement, cette calculatrice devrait être capable de résoudre n’importe quel type de calcul sans avoir à modifier son code.

C’est ici que les fonctions de première classe entrent en jeu. Souvenez-vous de la définition précédente : les fonctions peuvent être stockées dans des variables et passées sous forme d’arguments à d’autres fonctions. Voici un code pour montrer comment les fonctions peuvent être stockées dans des variables :

def rand():
    return 4  # chosen by fair dice roll (https://xkcd.com/221/)

print(rand())  # 4
print(rand)    # <function rand at 0x7fe0d1a37048>

also_rand = rand
print(also_rand())  # 4
print(also_rand)    # <function rand at 0x7fe0d1a37048>

"""My suggestion just do
def rand():
    return 4  # chosen by fair dice roll (https://xkcd.com/221/)
type(rand) # function
also_rand = rand
also_rand() == rand() # True
id(rand) == id(randd) # True
"""

Dans le code ci-dessus, rand est une variable de type “fonction”. Notez que also_rand a exactement la même valeur que rand.

Une fonction peut également être passée en tant qu’argument à d’autres fonctions, comme ceci :

def hello():
    print("Hi there!")

def call_three_times(any_func):
    any_func()
    any_func()
    any_func()

call_three_times(hello)  # Hi there!
                         # Hi there!
                         # Hi there!

Cela rappellera à de nombreux développeurs Python des fonctions telles que map et filter, prenant pour arguments des fonctions, tout comme callthreetimes ci-dessus. C’est ce qu’on appelle la programmation fonctionnelle.

Retour à la calculatrice. Les utilisateurs devraient pouvoir passer n’importe quelle fonction à la calculatrice et la laisser l’exécuter. Voici comment cela pourrait être codé :

class Calculator:

def __init__(self):
    self.num = 0.0

def get(self):
    return self.num

def do(self, operation):
    self.num = operation(self.num)

Le code en ressort plus court, ce qui est normal : la calculatrice peut tout faire sans rien implémenter en plus. Voici quelques exemples d’opérations :

def add_two(n):
    return n + 2

def multiply_by_three(n):
    return n * 3

calc = Calculator()

calc.do(add_two)
calc.do(multiply_by_three)

print(calc.get())  # 6.0

Tout cela fonctionne, mais le code n’est pas très flexible. Il faut une fonction entière pour ajouter 2 et une autre si on veut ajouter 5. Ce sera vite ingérable.

De ce fait, on a içi un bon cas d’usage des fonctions lambdas. Elles permettent de définir des fonctions anonymes courtes, c’est-à-dire des fonctions qui ne sont pas explicitement définies avec def. En les utilisant, le code ci-dessus devient :

calc = Calculator()

calc.do(lambda x: x + 2)
calc.do(lambda x: x + 5)
calc.do(lambda x: x * 3)

print(calc.get())  # 21.0

Bien que ne pas avoir à définir des fonctions pour chaque opération soit une bonne chose, il y a trop de lambda. Le code ci-dessus n’est pas facile à lire. Il doit être rendu plus lisible.

Pourquoi ne pas remplacer ces lambdas par des fonctions aux noms explicites ? Inspirons nous de add_two défini plus haut mais avec plus de ré-utilisabilité.

def add(n):
    return lambda x: x + n

def multiply(n):
    return lambda x: x * n

calc = Calculator()

calc.do(add(2))
calc.do(multiply(5))
calc.do(add(3))

print(calc.get())  # 13.0

La lisibilité du code calc.do(add(2)) est difficilement rivalisable. Ci-dessus, add et multiply sont des fonctions retournant une fonction, ensuite appelée par la calculatrice. La programmation fonctionnelle permet un code très lisible.

Rappelons que les lambdas ne peuvent définir qu’une seule expression, ce qui les rend idéales pour des choses simples comme une addition, mais insuffisantes pour une logique plus complexe. Les lambdas ne sont pas les seules fonctions à notre disposition. Toute fonction peut être retournée.

def add(n):
    def anonymous(x):
        return x + n
    return anonymous

def gcd(n):
    def helper(a, b):
        if b == 0:
            return a
        r = a % b
        return helper(b, r)
    def anonymous(x):
        return helper(x, n)
    return anonymous

calc = Calculator()

calc.do(add(56))
calc.do(gcd(42))

print(calc.get())  # 14.0

La calculatrice peut maintenant appliquer n’importe quel comportement décrit par une fonction. La seule condition préalable est que la fonction transmise à la méthode do de la calculatrice prenne une valeur en argument et renvoie une valeur. Ainsi, toute fonction correspondant à ces critères peut être utilisée, y compris les fonctions d’autres bibliothèques.

Par exemple :

import math

def add(n):
    return lambda x: x + n

calc = Calculator()

calc.do(add(15))
calc.do(math.sqrt)

print(calc.get())  # 3.872983346207417

La prise en charge des fonctions de première classe n’est pas une fonctionnalité superficielle. Bien que l’utilisation de fonctions de cette manière puisse sembler intimidante au début, on s’y habitue assez rapidement.

Un élément à retenir de cet article est que les fonctions peuvent être utilisées pour créer d’autres fonctions. Bien que cela puisse paraître compliqué, le code en ressortira plus lisible et donc plus compréhensible. Dès lors, une logique complexe pourra être écrite et configurable, le tout grâce à des règles simples.

La communauté Go utilise abondamment les fonctions de première classe. Notre exemple illustre le fait que cette physionomie est également implémentable en Python.

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnées par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Sciencem le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.