Qu’est-ce qu’un callback ?


mettreOn nous a parfois reproché de ne pas faire assez de tutos pour débutant. C’est pas faux, d’autant que quand j’ai commencé j’étais bien content que le site du zéro ait choisi de se spécialiser là dedans. Les tutos pour débutant sont vraiment la pierre angulaire de l’attractivité d’une techno.

Donc, un jour vous vous baladez avec vos premiers succès en prog, vous vous chauffer à utiliser une library externe (ce qui fait toujours peur au début) et il y a un truc que vous ne savez pas faire. Vous posez la question sur un forum, et on vous répond: “mais c’est simple, il suffit de passer un callback“.

Doh.

Rappel: on peut passer des fonctions en argument

Une fonction, ce n’est pas juste un bout de code auquel on donne un nom. C’est une unité de programmation à elle toute seule, en tout cas dans les langages modernes, et on dit dans ce cas qu’elles sont des “first class citizen” (citoyen de première catégorie, quoi, du vrai, du dur, du pur).

En pratique, ça veut dire qu’on peut manipuler la fonction sans l’éxécuter. En python ça donne ça:

>>> def dis_bonjour():
...     print "bonjour"
...
>>> print dis_bonjour # afficher l'objet fonction
<function dis_bonjour at 0x7f8cc6fce578>
>>> dis_bonjour.func_name # afficher le nom de la fonction
'dis_bonjour'

Ca veut dire aussi qu’on peut passer une fonction comme argument:

>>> def fonction_qui_appelle_une_fonction(fonction_a_appeler):
...     fonction_a_appeler()
...
>>> fonction_qui_appelle_une_fonction(dis_bonjour)
bonjour

Mais Za Koi ça sert ?

Et bien à dire qu’on va exécuter du code, même si on ne sait pas encore à l’avance quel est ce code. C’est très utile quand on code soit-même une bibliothèque pour permettre aux utilisateurs de celle-ci d’exécuter du code durant le fonctionnement de notre algo, sans avoir à mettre la main dedans.

C’est exactement ce que font les callback (ou appel en retour, traduit grossièrement).

Un callback, c’est une fonction passée en paramètre, qui va être appelée à une condition. La condition est la plus souvent “quand ceci arrive” et “ceci” est le plus souvent “quand le traitement est terminé”. Donc la grande majorité des callbacks sont des fonctions qu’on passe à d’autres fonctions pour qu’elles soient exécutées quand le traitement est terminé.

Des exemples, des exemples !

Si vous faites une interface graphique, vous voulez qu’un clic sur un bouton déclenche une action. Cette action est souvent passée comme un callback.

Exemple avec ce petit programme Tkinter (la lib d’UI installée par défaut avec Python):

>> from Tkinter import * # import de tout TK
>>> root = Tk() # création de la fenêtre
>>> def crie_ta_joie(): # notre callback
...     print "Yo !"
...
>>> b = Button(root, text="Go", command=crie_ta_joie) # création d'un bouton
>>> b.pack() # placement du bouton
>>> root.mainloop() # mise en route de l'UI

crie_ta_joie est passée à la classe Button via le paramètre command. Quand on cliquera sur le bouton ‘Go’, le callback crie_ta_joie sera donc appelé. ‘Yo !’ sera affiché dans le terminal.

C’est ce qu’on appelle la programmation événementielle: on écrit des fonctions qui sont appelées quand des événements arrivent, ici le clic sur un bouton.

Et en javascript…

Si vous utilisez jQuery, vous utilisez déjà des callbacks partout.

Ainsi, si vous faites:

$.get('/arretez/ou/ma/mere/va/tirer', function(){
    alert('Bang !');
});

jQuery va faire une requête ajax sur l’url, et le programme va continuer de fonctionner car les appels réseaux sont non bloquant. Mais quand la réponse arrivera, le callabck sera appelé, et fera alert(Bang !).

Les callbacks sont donc très utilisés pour la programmation asynchrone, c’est à dire quand on ne connaît pas le temps que vont mettre des opérations à s’effectuer mais qu’on veut réagir une fois qu’elle sont terminées.

L’injection de dépendances

Les callbacks sont aussi très utilisés pour une technique de programmation appelée “injection de dépendances” qui consiste à permettre à ceux qui utilisent votre code de choisir ce que feront certains bouts de code.

Imaginez une fonction (ici assez simplifiée) de téléchargement qui permet d’afficher la progression de celui-ci:

import urllib2, sys
def download(truc_a_telecharger, fichier_de_destination):
 
    # on ouvre la connection
    u = urllib2.urlopen(truc_a_telecharger)
 
    taille_des_bouts = 8192
    total_telecharge = 0
 
    # on ouvre le fichier pour écrire ce qu'on télécharge
    with open(fichier_de_destination, 'w') as f:
 
        # while True / if : break est un palliatif Python
        # pour l'absence d'instruction "until"
        while True:
 
            # on télécharge un petit bout du fichier
            bout = u.read(taille_des_bouts)
            total_telecharge += taille_des_bouts
 
            if not bout: # plus rien à télécharge: on sort
                break
 
            # on écrit le bout de fichier téléchargé
            f.write(bout)
 
            # on écrit un point sur le terminal pour noter qu'un bout a été
            # téléchargé
            sys.stdout.write('.')

Qui s’utilise comme ça:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

On pourrait faire beaucoup mieux que juste afficher un point pour chaque bout de fichier téléchargé. On pourrait par exemple afficher un pourcentage d’avancement. Ou écrire dans un log. Ou ne rien faire, et supprimer tout affichage.

Mais on veut garder le comportement par défaut car on pense que la plupart des gens l’utiliseront ainsi, et qu’il n’y a pas de raison qu’ils le recodent.

En modifiant la fonction, et en permettant de passer un callback, on permet cette flexibilité:

def download(truc_a_telecharger, fichier_de_destination,
             # on attend un callback en paramètre
             # mais on en passe un par défaut
             afficher_le_progres=lambda *x, **y: sys.stdout.write('.')):
 
    u = urllib2.urlopen(truc_a_telecharger)
    # on chope la taille du fichier, ça permettra plus de choses
    taille_du_fichier = int(u.info().getheaders("Content-Length")[0])
    taille_de_bloc = 8192
    total_telecharge = 0
 
    with open(fichier_de_destination, 'w') as f:
 
        while True:
 
            # ici on appelle le callback en lui passant un maximum de paramètres
            # pour qu'il puisse faire le plus de chose possible
            afficher_le_progres(truc_a_telecharger, fichier_de_destination,
                                taille_du_fichier, total_telecharge)
 
            bout = u.read(taille_de_bloc)
            total_telecharge += taille_de_bloc
 
            if not bout:
                break
 
            f.write(bout)

Et on l’utilise comme avant:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

Ou avec plus de puissance:

def log(truc_a_telecharger, fichier_de_destination,
       taille_du_fichier, total_telecharge):
    with open('progress.log', 'w') as f:
        pourcentage = str(total_telecharge * 100 / taille_du_fichier)
        f.write(pourcentage)
 
download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', log)

Et si on veut supprimer tout affichage, on peut passe une fonction qui ne fait rien:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', lambda *x: None)

Il y a plusieurs choses importantes ici:

  • on accepte un callback en paramètre
  • le paramètre possède une valeur par défaut. Hé oui, on peut mettre une fonction en valeur par défaut !
  • la fonction est une fonction anonyme. Ce n’est pas obligatoire, mais c’est pratique.
  • la fonction par défaut utilise l’opérateur splat pour accepter un nombre illimité de paramètres, même si elle ne va pas les utiliser.
  • on délègue le comportement de l’algo lors de l’affichage du progrès à la fonction passée en paramètre.
  • on passe un max de paramètres à cette fonction pour lui donner le plus de libertés possible. C’est aussi pour ça que notre fonction accepte par défaut un nombre illimité de paramètres: sinon ça ferait une erreur

Ce système est l’injection de dépendance: on délègue une partie du travail à du code injecté depuis l’extérieur. Cela permet une extensibilité de notre fonction, sans sacrifier sa simplicité puisqu’on a une valeur par défaut.

On peut pousser l’injection très loin: en passant carrément des listes de fonctions, et toutes les appeler, ou des objets, et appeler plusieurs méthodes de l’objet (ce dernier point est une extension de l’injection de dépendance appelé le pattern strategy).

24 thoughts on “Qu’est-ce qu’un callback ?

  • c0da

    “On nous a parfois reproché de parfois pas”

    N’y aurait-il pas parfois un “parfois” de trop?

  • Goldy

    Merci pour l’article. Effectivement, j’ai recherché y’a pas longtemps une définition clair de ce qu’était un callback, bien que sachant qu’il s’agissait de passer une fonction en argument d’une autre fonction, je n’avais pas trouvé d’explication clair sur la façon dont on pouvait les utiliser.

  • H3bus

    Aaaah, l’injection de dépendances… J’ai découvert ça il y a pas longtemps, j’en ai encore mal aux fesses…

  • Réchèr

    À noter également que le fait de pouvoir utiliser les fonctions comme des objets manipulables, permet d’implémenter des espèces de “switch case”. (Alors que cette structure syntaxique n’est pas présente dans le python)

    Il suffit d’utiliser un dictionnaire avec, comme clé : les possibilités du switch, et comme valeur : les fonctions à exécuter. Pour le “default”, on peut utiliser dict.get en indiquant la fonction par défaut. On peut aussi utiliser un defaultDict, qui a déjà été présenté dans un autre article.

    Exemple

    dictSwitch = {
        humain : saluer,
        chien : caresser,
        lapin : manger,
        poney : sodomiser,
    }
     
    # Supposons qu'on ait une variable intitulée "raceEtreVivant", qui vaut : humain, chien, ... ou n'importe quoi d'autre.
     
    # On récupère l'action à faire. Si aucune action de prévue, on récupère la fonction par défaut : regarder.
    actionAFaire = dictSwitch.get(raceEtreVivant, regarder)
    # On exécute l'action.
    actionAFaire()
  • Encolpe

    f.write préfère un tampon ou une chaîne à un entier :

    def log(truc_a_telecharger, fichier_de_destination,
           taille_du_fichier, total_telecharge):
        with open('progress.log', 'w') as f:
            pourcentage = str(total_telecharge * 100 / taille_du_fichier)
            f.write(pourcentage)
  • Sam Post author

    Histoire d’orienter un peu les recherches sur le site, pour ceux qui cherchent:

    callback ca veut dire quoi??

    Ben, c’est ici qu’il y a la réponse :-)

  • Bast

    Je fais certainement un truc qui va pas mais chez moi le code cité en exemple donne l’erreur suivante:

    >>> download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')
    Traceback (most recent call last):
    File "", line 1, in
    File "", line 19, in download
    TypeError: () takes exactly 0 arguments (4 given)

  • Sam Post author

    C’est une erreur de ma part, la lambda doit accepter tous les arguments, par juste les keywords arguments, donc faire :

                 afficher_le_progres=lambda *x, **y: sys.stdout.write('.')):

    et NON :

                 afficher_le_progres=lambda **x: sys.stdout.write('.')):
  • Marcel Mauss

    # met on en passe un par défaut

    AYBABTU! J’te déface ton orthographe!

  • Ludo

    Euh je vois pas le callback dans le bout de tcl :

    b = Button(root, text=”Go”, command=crie_ta_joie) # création d’un bouton

    pour moi il s’agit juste d’un appel de function classique, non ? ou alors mais souvenir de Tcl/Tk sont trop lointain ?

  • Sam Post author

    Si tu regarde une ligne plus haut, tu verras qu’on définit la fonction crie_ta_joie avec “def crie_ta_joie”. Mais ici, on n’appelle pas la fonction (on ne fait pas crie_ta_joie(), y a pas de parenthèses), on passe crie_ta_joie en tant qu’argument ‘command’. Cette fonction sera appelée plus tard, si l’utilisateur clic sur le bouton, c’est donc bien un callback.

  • isaac kunka

    Bonjour ,

    Je voulais savoir comment travaillez vous avec partenaire ,si ce serais possible je pourrais devenir votre agent si le voulait bien.

    Comment le tarif de la republique democratique du congo,la guinë ainsi de suite

  • Benz

    Bonjour, je ne comprends pas trop l’exemple :

    b = Button(root, text=”Go”, command=crie_ta_joie)

    tu dis : “Cette fonction sera appelée plus tard, si l’utilisateur clic sur le bouton, c’est donc bien un callback.”

    b = Button(root, text=”Go”, command=crie_ta_joie())

    Mais si j’écris la code ci-desus : la fonction aussi sera appelée au moment du clic sur le bouton, non ?

  • Sam Post author

    Pour le code suivant:

    b = Button(root, text=”Go”, command=crie_ta_joie())

    Tu ne passe pas la fonction en tant que callback, tu appelles la fonction (car tu utilises les parenthèses). La fonction est donc exécuté exactement à cette ligne. Cette ligne est exécutée au tout début du lancement du programme, au moment de la création du bouton.

    Cela ne fait donc pas ce que l’ont veut: au lieu de dire “appelle moi cette fonction plus tard”, ça dit “appelle moi cette fonction tout de suite”

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.