duck typing – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Qu’est-ce que le duck typing et à quoi ça sert ? http://sametmax.com/quest-ce-que-le-duck-typing-et-a-quoi-ca-sert/ http://sametmax.com/quest-ce-que-le-duck-typing-et-a-quoi-ca-sert/#comments Mon, 12 Jan 2015 02:47:00 +0000 http://sametmax.com/?p=15712 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 "", line 1, in 
    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.

]]>
http://sametmax.com/quest-ce-que-le-duck-typing-et-a-quoi-ca-sert/feed/ 5 15712