Le duck typing, qu’on pourrait traduire par “typage canard” mais on ne le fera pas parce que c’est très moche, est une manière de créer des APIs basée sur la philosophie que l’aspect pratique est plus important que la pureté du code.
L’idée est de créer des signatures de callable qui acceptent des paramètres en fonction de leur comportement, pas leur type :
Si ça marche comme un canard et que ça fait le bruit d’un canard, alors ça ressemble assez à un canard pour le traiter comme un canard
Imaginez que vous ayez un objet avec un interface ICanard:
class ICanar: def coin(): pass
Une fonction qui est programmée selon le duck typing acceptera comme argument un objet qui possède la méthode coin
, peut importe si il implémente cette interface ou non.
En gros, si un paramètre possède une interface suffisante pour nous, ou peut être casté en un objet avec une interface suffisante pour nous, on l’accepte. Cela rend un callable plus générique.
Ok, trève de bavardage, qu’est-ce que ça implique, dans la vraie vie vivante ?
Si je fais une fonction qui retourne le premier élément d’une liste ou un élément par défaut :
def getfirst(lst, default=None): try: return lst[0] except IndexError: return default |
Pratique, et ça marche sur d’autres itérables :
>>> getfirst([1, 2, 3]) 1 >>> getfirst('abcde') 'a' |
On a une forme de duck typing : si on peut récupérer le premier élément, alors ça suffit pour nous. Peut importe qu’il s’agit d’une liste ou d’un tuple.
On peut néanmoins améliorer la généricité de cette fonction:
def getfirst(iterable, default=None): for x in iterable: return x return default |
Ici, le comportement recherché est qu’on puisse faire une une boucle for
dessus, pas qu’on puisse récupérer un élément par son index.
Cela rend la fonction encore plus flexible, ainsi elle marche sur les générateurs, les flux, les fichiers:
>>> getfirst(open('/etc/fstab')) '# /etc/fstab: static file system information.\n'
Un autre exemple ? La fonction Python sum
par exemple, accepte tout types de nombres :
>>> sum((1, 2, 3)) # integers 6 >>> sum((1.3, 2.4, 3.5)) # floats 7.2 >>> sum((1j, 2j, 3j)) # complexes 6j |
Sympas, mais l’addition en Python supporte bien plus que les nombres :
>>> [1] + [2] [1, 2] >>> (1, 2) + (2, 4) (1, 2, 2, 4) >>> "a" + "b" 'ab' |
Mais sum
ne les accepte pas :
>>> sum("a", "b") Traceback (most recent call last): File "<ipython-input-24-1e5baeda1183>", line 1, in <module> sum("a", "b") TypeError: sum() can't sum strings [use ''.join(seq) instead] |
Il est possible de faire un sum
plus générique :
def sumum(*iterable, start=None, default=None): # On donne à l'utilisateur la possibilité # de passer un premier élément if start is None: # on récupère le premier élément try: start, *iterable = iterable except ValueError: # Il n'y a aucun élément dans l'itérable # donc on retourne la valeur par default return default # on additionne for x in iterable: start += x return start |
Le duck typing, à son maximum :
>>> sumum('a', 'b', 'c') 'abc' >>> sumum([1, 2], [3, 4]) [1, 2, 3, 4] |
Le duck typing implique aussi une prise de décision. Qu’est-ce qui serait le plus pratique ? De pouvoir additionner tous les types additionnables ? Ou de pouvoir additionner n’importe quoi qui ressemble à un nombre ?
Imaginons que la plupart de nos libs, plutôt que de fournir la possibilité d’additionner, propose la possibilité de caster vers un float
:
class Temperature: def __init__(self, value, unit='C'): self.value = float(value) self.unit = unit def __float__(self): if self.unit == 'C': return self.value if self.unit == 'K': return self.value - 273.15 if self.unit == 'F': return (self.value - 32) * 5/9 def __repr__(self): return '%s %s' % (self.value, (self.unit != 'K')*'°'+self.unit) t1 = Temperature(5) t2 = Temperature(3, 'K') t3 = Temperature(30, 'F') t1, t2, t3 ## (5.0 °C, 3.0 K, 30.0 °F) |
Dans ce cas notre fonction pourrait convertir tous les éléments d’un itérable avant addition :
def sumcast(*iterable, start=None, default=None): # On donne à l'utilisateur la possibilité # de passer un premier élément if start is None: # on récupère le premier élément try: start, *iterable = iterable except ValueError: # Il n'y a aucun élément dans l'itérable # donc on retourne la valeur par default return default # on additionne en convertissant tout en float start = float(start) for x in iterable: start += float(x) return start >>> sumcast(1, "3", t1, t2, t3) -262.26111111111106 |
Dans tous les cas, on se fiche complètement que nos objets soient d’un type précis ou qu’ils implémentent une interface précise à partir du moment où leur API est suffisamment proche du ce type ou de l’interface dont on a besoin.
Le duck typing a beau être une pratique vouée à simplifier la vie au prix du formalisme, il ne dispense pas de documenter votre code à propos de cette subtilité afin que l’utilisateur final n’ait pas de mauvaise surprise.
Il convient de ne pas abuser du duck typing, qui est là pour rendre service uniquement. Si vous ajoutez des cas farfelus dans votre code pour supporter des situations rares, vous le rendez plus compliqué et moins robuste. Visez la généricité pour les situations les plus courantes, pas toutes les situations possibles.
Et souvenez-vous que plus on est dynamique sur les types, plus on perd en performance. Il faut savoir quelle part de compromis on est prêt à faire.
Merci beaucoup pour ce post.
Quelques typos:
Une fonction qui est programmée selon le duck typing acceptera en comme argument un objet -> en OU comme
dans def sumum/except ValueError:/return empty -> return default
idem dans sumcast
Dans tous les cas, on se fichr -> fiche
Le duck typing à beau être -> a beau être
afin que l’utilisateur final n’ai pas de mauvaise surprise ->n’ait pas
vous le rendez plus compliqué et moins robustes -> robuste
Pour faire mon chieur: on dit °C, °F (car ce sont des échelles à 2 points fixes) mais K (et non °K) car c’est une échelle à un point fixe (le point triple de l’eau). Mais l’erreur est extrêmement fréquente. [En plus ça alourdirait le code de class Temperature]
Égalament de la typo: des cas farfelus votre code => des cas farfelus dans votre code
Et maintenant un peu de duck fucking: https://www.youtube.com/watch?v=6k01DIVDJlY
Merci beaucoup pour ces corrections. Comme d’habitude, ça apporte une vraie valeur ajoutée aux articles, et c’est très bienvenue.
Pour les kelvins, on devrait pouvoir s’arranger :)
Maarci bôcou pour se paust !
Cependant, pour continuer à l’améliorer :
dans la phrase « …acceptera en comme argument un objet qui possède la méthode
quack
, … »le “en” me semble de trop et le “quack” n’est pas cohérent avec l’exemple qui le précède (“coin”).
Sinon, pour faire encore plus mon chieur que francoisb : le °C est également une échelle à un point fixe (dérivée par décalage du kelvin). C’est l’échelle centigrade qui est à deux points fixes. [/chieur]
Je vous aime, mais là le °, il bougera plus. Il va rester à sa place, tranquile, fumer sa clope, mater un porno, et osef.