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).
“On nous a parfois reproché de parfois pas”
N’y aurait-il pas parfois un “parfois” de trop?
Ca arrive parfois.
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.
Aaaah, l’injection de dépendances… J’ai découvert ça il y a pas longtemps, j’en ai encore mal aux fesses…
À 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
Indentation fail. Désolé.
f.write préfère un tampon ou une chaîne à un entier :
Juste.
+1 avec Goldy. Merci :)
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 :-)
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)
C’est une erreur de ma part, la lambda doit accepter tous les arguments, par juste les keywords arguments, donc faire :
et NON :
AYBABTU! J’te déface ton orthographe!
Fixed.
Une petit mot en trop:
Bien vu ! C’est corrigé.
Euh je vois pas le callback dans le bout de tcl :
pour moi il s’agit juste d’un appel de function classique, non ? ou alors mais souvenir de Tcl/Tk sont trop lointain ?
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.