Les fonctions d'ordre supérieur : des fonctions qui reçoivent et renvoient des fonctions

Objectifs de la leçon
  • Expliquer ce qu’est une fonction d’ordre supérieur (Higher-Order Function) et pourquoi c’est possible en Python
  • Passer une fonction en paramètre — sans parenthèses — et l’appeler depuis une autre fonction
  • Utiliser map, filter et le paramètre key de sorted, max et min
  • Écrire une fonction qui renvoie une fonction

Qu’allons-nous apprendre ?

  • Ce qu’est une fonction d’ordre supérieur (Higher-Order Function).
  • Comment passer une fonction en paramètre — et pourquoi sans parenthèses.
  • Les fonctions intégrées map et filter.
  • Le paramètre key de sorted, max et min.
  • Une fonction qui renvoie une fonction.

Pourquoi en avons-nous besoin ?

Deux fonctions presque identiques :

def double_all(numbers):
    result = []
    for n in numbers:
        result.append(n * 2)
    return result

def square_all(numbers):
    result = []
    for n in numbers:
        result.append(n * n)
    return result

La boucle est identique — seule l’action sur chaque élément diffère. Chaque nouvelle action (moitié, plus 10, arrondi…) demande une fonction entière de plus, et c’est exactement la duplication que DRY interdit.

L’idée : écrire la boucle une seule fois, et passer l’action elle-même en paramètre.

Explication à partir des bases

Une fonction est un objet

Nous avons déjà vu dans la leçon « les fonctions en profondeur » qu’en Python une fonction est un objet — comme un nombre ou une liste :

  • double (sans parenthèses) — la fonction elle-même. On peut la stocker dans une variable ou la transmettre.
  • double(5) (avec parenthèses) — un appel à la fonction. Le résultat est ce qu’elle renvoie.

La fonction d’ordre supérieur

Une fonction d’ordre supérieur est une fonction qui :

  • Reçoit une autre fonction en paramètre, ou
  • Renvoie une fonction.

Vous en avez déjà utilisé une : sorted(products, key=lambda p: p[1]) — sorted reçoit une fonction dans le paramètre key.

Analogie : une machine à laver avec des programmes

Une machine à laver sait « passer sur tous les vêtements ». Ce qu’il faut faire à chaque pièce — vous le choisissez avec le programme : lavage délicat, normal ou séchage. On n’achète pas une nouvelle machine pour chaque programme — on lui donne le programme. La fonction d’ordre supérieur est la machine, et la fonction que vous passez est le programme.

Syntaxe

def apply_to_all(numbers, action):     # action is a function
    result = []
    for n in numbers:
        result.append(action(n))       # call it for each item
    return result

apply_to_all([1, 2, 3], double)        # pass it: no parentheses
list(map(double, [1, 2, 3]))           # built-in: map
list(filter(is_even, [1, 2, 3, 4]))    # built-in: filter
sorted(words, key=len)                 # key: sort by the result of len

Premier exemple de code

week-2/apply_to_all.py :

def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result

def double(n):
    return n * 2

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

Sortie :

[2, 4, 6]
[1, 4, 9]
[2, 2]

Analyse du code ligne par ligne

LigneExplication
def apply_to_all(numbers, action):action est un paramètre ordinaire — simplement, nous attendons qu’il contienne une fonction.
result.append(action(n))Nous appelons la fonction reçue, sur chaque élément.
apply_to_all([1, 2, 3], double)Nous passons double sans parenthèses — la fonction, pas son résultat.
lambda n: n * nUne courte fonction sans nom, créée sur place.
roundOn peut aussi passer une fonction intégrée.

Un autre exemple

map, filter et key. week-2/hof_builtins.py :

prices = [100, 250, 40, 80]

with_fee = list(map(lambda p: p + 10, prices))
cheap = list(filter(lambda p: p < 100, prices))
print(with_fee)
print(cheap)

words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words, key=len))
print(max(words, key=len))

students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
print(max(students, key=lambda s: s[1]))

Sortie :

[110, 260, 50, 90]
[40, 80]
['fig', 'kiwi', 'banana', 'cherry']
banana
('Dana', 92)
  • map(func, items) — exécute func sur chaque élément. list(...) transforme le résultat en liste.
  • filter(func, items) — ne garde que les éléments pour lesquels func renvoie True.
  • key=len — sorted et max comparent selon le résultat de len sur chaque mot, et non selon le mot lui-même.

Une fonction qui renvoie une fonction. week-2/make_multiplier.py :

def make_multiplier(factor):
    def multiply(n):
        return n * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))
print(triple(5))

Sortie :

10
15

make_multiplier est une « usine » qui produit des fonctions : chaque appel renvoie une nouvelle fonction, qui se souvient de son propre factor. Comment elle s’en souvient exactement — nous le verrons dans la leçon suivante, sur les fonctions imbriquées.

Erreur courante

L’erreur : appeler la fonction au lieu de la passer.

apply_to_all([1, 2], double())
TypeError: double() missing 1 required positional argument: 'n'

Pourquoi ? double() avec des parenthèses appelle la fonction immédiatement — sans paramètre — elle échoue donc avant même que apply_to_all ne commence. Passez la fonction sans parenthèses : apply_to_all([1, 2], double). C’est la fonction qui la reçoit qui l’appellera.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: Une fonction en paramètre

Écrivez une fonction apply_to_all(numbers, action) qui exécute action sur chaque nombre et renvoie une nouvelle liste. Utilisez-la avec une fonction ordinaire double, avec une lambda qui met au carré, et avec round.

print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))

Résultat attendu :

[2, 4, 6]
[1, 4, 9]
[2, 2]
Afficher la solution
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2, 3], double))
print(apply_to_all([1, 2, 3], lambda n: n * n))
print(apply_to_all([1.6, 2.4], round))
[2, 4, 6]
[1, 4, 9]
[2, 2]

Passez la fonction sans parenthèses (double, round). C’est apply_to_all qui l’appelle.

Exercice 2: map et filter

prices = [20, 75, 120, 45, 90]
  1. Ne gardez que les prix supérieurs à 50, avec filter.
  2. Ajoutez 10 à chaque prix restant, avec map.

Résultat attendu :

[75, 120, 90]
[85, 130, 100]
Afficher la solution
prices = [20, 75, 120, 45, 90]

expensive = list(filter(lambda p: p > 50, prices))
raised = list(map(lambda p: p + 10, expensive))
print(expensive)
print(raised)
[75, 120, 90]
[85, 130, 100]

filter ne garde que ce pour quoi la lambda renvoie True. map exécute la lambda sur chaque élément. list(...) transforme le résultat en liste.

Exercice 3: key — selon quoi trier

words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]
  1. Triez les mots par longueur.
  2. Trouvez le mot le plus long.
  3. Triez les étudiants par note, de la plus haute à la plus basse, et n’affichez que les noms.

Résultat attendu :

['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi
Afficher la solution
words = ["banana", "fig", "cherry", "kiwi"]
students = [("Dana", 92), ("Avi", 78), ("Noa", 85)]

print(sorted(words, key=len))
print(max(words, key=len))

for name, grade in sorted(students, key=lambda s: s[1], reverse=True):
    print(name)
['fig', 'kiwi', 'banana', 'cherry']
banana
Dana
Noa
Avi

key=len — comparer selon le résultat de len sur chaque mot, et non selon le mot lui-même.

Exercice 4: Une fonction qui renvoie une fonction

Écrivez une fonction make_discount(percent) qui renvoie une fonction, laquelle reçoit un prix et le renvoie après la remise.

ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))

Résultat attendu :

180.0
100.0
Afficher la solution
def make_discount(percent):
    def apply(price):
        return price - price * percent / 100
    return apply


ten_off = make_discount(10)
half_price = make_discount(50)
print(ten_off(200))
print(half_price(200))
180.0
100.0

make_discount est une « usine » — chaque appel produit une nouvelle fonction qui se souvient de son propre percent.

Exercice 5: Appeler au lieu de passer

def double(n):
    return n * 2

print(apply_to_all([1, 2], double()))
  1. Quelle erreur obtiendrez-vous, et pourquoi ?
  2. Quelle est la correction ?
Afficher la solution
  1. TypeError: double() missing 1 required positional argument: 'n'. Les parenthèses de double() appellent la fonction immédiatement — sans paramètre — avant même que apply_to_all ne commence.
  2. Passez la fonction elle-même, sans parenthèses :
def apply_to_all(numbers, action):
    result = []
    for n in numbers:
        result.append(action(n))
    return result


def double(n):
    return n * 2


print(apply_to_all([1, 2], double))
[2, 4]

Résumé

  • Fonction d’ordre supérieur = une fonction qui reçoit une fonction ou renvoie une fonction. C’est possible parce qu’une fonction est un objet.
  • Passez une fonction sans parenthèses — c’est la fonction qui la reçoit qui l’appelle.
  • map — une action sur chaque élément. filter — garde ce qui remplit la condition.
  • key= dans sorted, max, min — définit selon quoi comparer.
  • Une fonction peut renvoyer une fonction — une « usine » qui produit des fonctions sur mesure.

Documentation officielle