type – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +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
Faire des enums en python http://sametmax.com/faire-des-enums-en-python/ http://sametmax.com/faire-des-enums-en-python/#comments Wed, 19 Dec 2012 17:41:10 +0000 http://sametmax.com/?p=3741

Ceci est un post invité de Réchèr posté sous licence creative common 3.0 unported.

“Moi, je viens du C++”

Voilà un propos qui fait classe. Ça donne un air de vieux de la vieille, à qui on ne la fait pas, et qui connaît des langages que c’est pas des langages de taffioles. “De mon temps, fallait savoir ce qu’on faisait, sinon tout plantait. On ciselait notre code à la main dans des blocs de granit. Vous les gosses de maintenant, vous connaissez plus rien. Eh ! Sale jeune, au lieu d’importer des lib de feignasses, vient donc me changer ma poche à urine.

Ça aurait été encore plus classe de dire que je viens de l’Assembleur, mais je suis pas assez vieux pour ça. Et de toutes façons, en vrai, je viens du Pascal. Bref, passons.

Tout ça pour dire que dans le C++, il existe quelque chose que j’avais trouvé bien sympa : les enums.

Comment ça marche ?

Un enum est un type de variables ayant un domaine de valeur fixe et prédéfini, que l’on spécifie lors de la déclaration du type.

typedef enum
{
     Pique,
     Coeur,
     Trefle,
     Carreau
} ECouleursCartes;

Ensuite, on peut déclarer une variable de ce type, et lui affecter l’une des valeurs autorisées.

ECouleursCartes vMaCouleurCarte;
vMaCouleurCarte = Trefle;

Concrètement, chaque valeur correspond à un entier. Le compilateur fait les correspondances tout seul comme un grand.

À quoi ça sert ?

À avoir du code plus parlant quand on veut représenter des notions concrètes de la vraie vie, ou quand on implémente une machine à état.

Par exemple, vous créez une classe FeuCirculation. Vous avez besoin d’une variable renseignant sa couleur. Vous pouvez utiliser un int à l’arrache, et noter en commentaire quelque part : “0 c’est rouge, 1 c’est vert, 2 c’est orange.”. Mais c’est chiant. Il faut toujours garder en tête les correspondances. On risque d’attribuer des valeurs non documentées. Bref, y’a n’importe quoi dedans et ça pue, (un peu comme une poche à urine).

Avec un enum, tout est plus clair, aussi bien dans le code définissant la classe FeuCirculation, que dans le code qui l’utilise. Et c’est plus robuste.

L’exemple qui est donné très souvent dans les tutoriaux, c’est un enum Animal, contenant les valeurs chien, chat, bœuf musqué, ... Ça fonctionne, mais personnellement cet exemple me gêne un peu. Chaque animal est un objet à part entière. Dans ce cas, il vaudrait mieux créer une classe générique Animal, et la faire hériter. Ce qui permettra ensuite de créer des méthodes telles que manger(), crier(), péter(), ...

If it quacks like a duck, it may have noticed you are fucking it.

Pour décrire plusieurs objets, utilisez des classes. Pour décrire les différents états d’un même objet, utilisez un enum. Pour décrire plusieurs objets simples, utilisez l’un ou l’autre, c’est à vous de choisir.

Comment faire un enum en python ?

Il n’y a, à ma connaissance, pas de structure syntaxique dédiée. Mais c’est pas grave, on va bien trouver quelque chose.

Le plus simple, pour commencer, c’est de garder l’idée de la correspondance avec des entiers. Ça offre plein d’avantages :

  • On sait exactement comment ça marche à l’intérieur.
  • On peut très facilement enregistrer des enums dans un fichier ou une base de donnée, puisque ce ne sont que des nombres.
  • On peut ordonner les valeurs. À utiliser avec prudence, car ça n’a pas toujours de sens. (Dire que l’état “feu rouge” est inférieur à l’état “feu vert”, qu’est-ce que cela signifie exactement ?)

La première idée qui vient à l’esprit, c’est de créer une classe avec les valeurs dedans, et de ne plus jamais les changer par la suite.

# On l'appelle FeuCirculation et non pas FeuRouge. 
# Parce qu'un feu n'est pas forcément rouge.
# (Putain de langue française à la con qui fait n'importe quoi).
class FeuCirculation:
    ROUGE = 0
    ORANGE = 1
    VERT = 2
    ORANGE_CLIGNOTANT = 3
    TOUT_ETEINT = 4

feuActuel = FeuCirculation.ORANGE

C’est bien, mais chiant à maintenir. Si on veut ajouter un autre état (par exemple, FLECHE_DROITE_ORANGE_CLIGNOTANTE), et qu’on se plante dans les valeurs numériques, il y a un risque d’avoir des doublons, et ça fiche tout en l’air.

Voici un petit peu mieux :

class FeuCirculation:
    (ROUGE,
     ORANGE,
     VERT,
     ORANGE_CLIGNOTANT,
     TOUT_ETEINT,
    ) = range(5)

range(5) renvoie un tuple (0, 1, 2, 3, 4). Des entiers différents sont attribués aux valeurs de l’enum. Si on veut en rajouter une et qu’on oublie de remplacer range(5) par range(6), ça va balancer une exception. Tout va bien.

Au cas où vous demanderiez : “qu’est-ce qui m’empêche de redéfinir l’une des valeurs, à un autre endroit du code, et de foutre le bordel ?”, je répondrais “We are all consenting adults here”. (Se référer à la philosophie du python. Si besoin, ce sera l’occasion d’un autre article).

Some consenting adults. (Je peux venir ?)

Le code est beau, mais pour débugger ?

C’est un peu lourdingue. Lorsqu’on fait print feuActuel (ou lorsqu’on tape directement feuActuel dans une console pdb), c’est une valeur numérique pas claire qui va s’afficher, au lieu d’un joli “ORANGE”, “ROUGE”, …

Un petit dictionnaire de correspondance “valeur numérique” -> “nom à afficher” serait de bon aloi. Quelque chose de ce genre, à placer dans la définiton de la classe FeuCirculation :

    dictReverse = {
        ROUGE : "ROUGE",
        ORANGE : "ORANGE",
        VERT : "VERT",
        ORANGE_CLIGNOTANT : "ORANGE_CLIGNOTANT",
        TOUT_ETEINT : "TOUT_ETEINT",
    }

Fort bien. Mais n’est-ce pas un peu casse-couille ? Eh si. Des répétitions à la pelle, et dictReverse doit rester synchro avec les définitions des valeurs. C’est redevenu chiant à maintenir.

Hey, je suis Dict-Reverse, et je suis chiant à maintenir. bi-boppe eu loulaaaa.

Définition dynamique de nouveaux types

Soyons franc, j’ai piqué cette astuce ici :
http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python

Vous connaissez certainement déjà la fonction native type(). Elle renvoie le type du truc passé en paramètre. Youpi.

Mais saviez-vous qu’elle permet également de créer de nouveaux types ? Pour ce faire, il faut l’appeler avec 3 paramètres :

  • Le nom.
  • Une liste indiquant les types de base (dans le cas où on veut que le type soit hérité).
  • Un dictionnaire contenant les attributs. Les clés sont des chaînes de caractère, et correspondront aux noms des attributs.

La création de l’enum FeuCirculation pourrait donc se faire de cette manière :

FeuCirculation = type(
    "FeuCirculation",
    (),
    {
        "ROUGE" : 0, 
        "ORANGE" : 1, 
        "VERT" : 2, 
        "ORANGE_CLIGNOTANT" : 3, 
        "TOUT_ETEINT" : 4
    })

>>> FeuCirculation

>>> FeuCirculation.VERT
2

On peut également ajouter le dictReverse dans le dictionnaire des attributs. (Ça fait un dictionnaire dans un dictionnaire, c’est lol).

FeuCirculation = type(
    "FeuCirculation",
    (),
    {
        "ROUGE" : 0,
        "ORANGE" : 1,
        "VERT" : 2,
        "ORANGE_CLIGNOTANT" : 3,
        "TOUT_ETEINT" : 4,
        "dictReverse" : {
            0 : "ROUGE",
            1 : "ORANGE",
            2 : "VERT",
            3 : "ORANGE_CLIGNOTANT",
            4 : "TOUT_ETEINT"}
    })

>>> FeuCirculation

>>> feuxActuel = FeuCirculation.VERT
>>> feuxActuel
2
>>> FeuCirculation.dictReverse[feuxActuel]
'VERT'

Et maintenant y’a plus qu’à coder une petite fonction qui fait tout ça génériquement.

def enum(enumName, *listValueNames):
    # Une suite d'entiers, on en crée autant
    # qu'il y a de valeurs dans l'enum.
    listValueNumbers = range(len(listValueNames))
    # création du dictionaire des attributs.
    # Remplissage initial avec les correspondances : valeur d'enum -> entier
    dictAttrib = dict( zip(listValueNames, listValueNumbers) )
    # création du dictionnaire inverse. entier -> valeur d'enum
    dictReverse = dict( zip(listValueNumbers, listValueNames) )
    # ajout du dictionnaire inverse dans les attributs
    dictAttrib["dictReverse"] = dictReverse
    # création et renvoyage du type
    mainType = type(enumName, (), dictAttrib)
    return mainType

>>> FeuCirculation = enum(
        "FeuCirculation",
        "ROUGE", "ORANGE", "VERT",
        "ORANGE_CLIGNOTANT", "TOUT_ETEINT")
>>> FeuCirculation.TOUT_ETEINT
4
>>> FeuCirculation.dictReverse[FeuCirculation.TOUT_ETEINT]
'TOUT_ETEINT'

Une petite précision : d’habitude, quand on crée des classes ou des types, c’est pour les instancier. Là on ne le fait pas, on se contente d’utiliser des valeurs statiques contenues dans le type. Vous pourriez faire feuDuCarrefour = FeuCirculation(). Ça va fonctionner, mais ça ne vous servira à rien.

Le code de la route a parfois besoin de refactoring

Mais si on mélange les enums ?

>>> FeuCirculation = enum(
        "FeuCirculation",
        "ROUGE", "ORANGE", "VERT",
        "ORANGE_CLIGNOTANT", "TOUT_ETEINT")
>>> etatMatiere = enum(
        "etatMatiere", 
        "SOLIDE", "LIQUIDE", "GAZEUX")
>>> etatMatiere.dictReverse[FeuCirculation.VERT]
'GAZEUX'

Mince alors. Ça fait n’importe quoi, sans signaler aucune erreur. C’est normal. D’un enum à l’autre, on ne fait que manipuler des entiers, qui restent les mêmes. Les correspondances se font joyeusement, même si ça n’a aucun sens. Dans le même ordre d’idée : FeuCirculation.ROUGE == etatMatiere.SOLIDE renverra True, ce qui ne veut rien dire. Comment régler ce problème ?

Better have traffic ice than traffic jam.

Solution 1 : ne pas régler le problème

“We are all consenting adults here”, on ne va donc pas compliquer le code en rajoutant des sécurités de partout, sous prétexte d’empêcher des erreurs. Le seule moyen valable de se prémunir de faire n’importe quoi quand on code, c’est tout bêtement de rester concentré et de faire gaffe à ce qu’on fait.

Honnêtement, ça ne m’est jamais arrivé de mélanger des enums entre eux. Pourtant, je n’ai pas la prétention de tout faire juste et sans bugs du premier coup. Parmi mes bourdes préférées : copier-coller la ligne du dessus et ne pas faire les bons remplacements (les “x” par des “y”, les “1” par des “2”, etc.), ou encore : déclarer une fonction, lui faire renvoyer un résultat bidon parce que je préfère la coder plus tard, et oublier que je ne l’ai pas codé.

def calculDist(point1, point2):
    distX = point1.x - point1.x
    distY = point2.x - point2.x
    return math.sqrt(distX * distX + distY * distY)
    # ami lecteur, sauras-tu trouver tous les fails
    # présents dans cette fonction ?

def RemoveDoubleLetters(strValue):
    # TODO : coder le truc. Mais là, j'ai la flemme.
    return strValue

BSOD !!

Pour limiter les risques de mélange, voici quelques conseils :

  • Préfixez les variables contenant une valeur d’enum : feu pour FeuCirculation, emat pour etatMatiere, etc. De cette manière, les erreurs de code sautent aux yeux. (making wrong code look wrong, tout ça…)
  • Il est important de ne pas polluer inutilement l’espace de nommage. Dans un fichier donné, n’importez pas les enums dont vous n’avez pas besoin. De cette manière, une erreur sera renvoyée si jamais vous utilisez une valeur d’enum imprévue. Je doute que vous ayez besoin, dans un même fichier de code, de renseigner des états de feux rouge et des états de matière.
  • Si vous faites exprès de mélanger des enums, en pensant que vous n’avez aucun moyen de faire autrement, c’est qu’il y a, dès l’origine, un problème de conception dans votre code.

Vieng fayre toi-même leu mélainge des enums, sur les muuuurs deu la cabaneu du côdeur. Vieng t'assouar.

Solution 2 : des enums fortement typés.

Au lieu que les valeurs internes des enums soient de simples entiers, on crée un type pour chacune d’elle. Comme ça, on est sûr que ça renverra une erreur si on se confusionne. Par contre, on perd en simplicité, et ce n’est plus aussi simple pour la sérialisation.

Soyons fous, et voyons ce que ça donne !

def strongTypedEnum(enumName, *listValueNames):
    # création d'une liste de type. sans attribut, sans héritage.
    # le nom du type est composé du nom de l'enum et du nom de la valeur,
    # séparés par un point.
    listValueTyped = [ type(".".join((enumName, nameValue)), (), {})
                       for nameValue in listValueNames ]
    # Ensuite, c'est tout pareil que la fonction d'avant.
    dictAttrib = dict( zip(listValueNames, listValueTyped) )
    dictReverse = dict( zip(listValueTyped, listValueNames) )
    dictAttrib["dictReverse"] = dictReverse
    mainType = type(enumName, (), dictAttrib)
    return mainType

>>> FeuCirculation = strongTypedEnum("FeuCirculation",
        "ROUGE", "ORANGE", "VERT",
        "ORANGE_CLIGNOTANT", "TOUT_ETEINT")
>>> etatMatiere = strongTypedEnum(
        "etatMatiere", 
        "SOLIDE", "LIQUIDE", "GAZEUX")
>>> etatMatiere.LIQUIDE

>>> etatMatiere.dictReverse[FeuCirculation.ROUGE]
Traceback (most recent call last):
  File "", line 1, in 
    etatMatiere.dictReverse[FeuCirculation.ROUGE]
KeyError: 

Mais est-ce bien nécessaire ?

Y’a une lib pour ça

Comme toujours, avec cette foutue prolifération de code libre, dès que quelque chose de cool peut potentiellement exister, un connard de geek l’a déjà fait, vous privant de l’occasion de le faire vous-même et de retirer la gloire qui en décombe. La lib flufl.enum permet donc de créer et manipuler des enums, avec autant et même plus de fonctionnalités que ce que je viens de décrire ici. (Connards de geeks qui viennent manger le pain des français).

Par contre, j’ai regardé le code, et j’ai pas pigé comment ça fonctionnait à l’intérieur. Ça fera peut-être l’objet d’un autre article, si j’ai le courage de me plonger dedans, et que je trouve ça intéressant et rigolo.

De toutes façons, les libs c’est chiant, ça ajoute des dépendances, il vaut mieux tout coder à la main.

Disgression 1 : quelle classe ce type !

Déclarer un type avec la fonction type(), et déclarer une classe, ça ne fait pas exactement la même chose.

>>> class MaClasse:
	pass

>>> MaClasse

>>> instanceClasse = MaClasse()
>>> instanceClasse
<__main__.MaClasse instance at 0x011C8BC0>

>>> MonType = type("Le_Nom_De_Mon_Type", (), {})
>>> MonType

# Hey ! Le nom est dans une string, et y'a pas d'adresse mémoire !

>>> instanceType = MonType()
>>> instanceType
<__main__.Le_Nom_De_Mon_Type object at 0x011C1B30>
# Hey ! C'est un object, et pas une instance !

Ça a peut-être quelque chose à voir avec une histoire de old-style class et new-style class. J’ai pas cherché plus loin pour l’instant. Ça fera peut-être l’objet d’un autre-autre article, si j’ai le courage de me plonger dedans, et que je trouve ça intéressant et rigolo.

Disgression 2 : Restons dans les règles

J’avais trouvé un autre exemple de machine à état, plus rigolo que des feux de circulation ou de la matière. Je ne m’en suis pas servi durant mes explications, parce que ça aurait distrait le lecteur / la lectrice (charge cérébrale, tout ça…). Mais ce serait vraiment dommage de ne pas vous en faire profiter. Le voici donc :

>>> PhaseCycleUterin = enum(
        "PhaseCycleUterin",
        "PREPUBERE", "MENSTRUELLE", 
        "OVULATION", "PROLIPERATIVE", "SECRETRICE"
        "ENCEINTE", "MENOPAUSE")

>>> etatMatiere.dictReverse[PhaseCycleUterin.MENSTRUELLE]
'LIQUIDE'

Voilà, ce sera tout pour aujourd’hui. La prochaine fois, nous redéfinirons le type “object” en utilisant uniquement les picots du langage Braille.

Une femme qui a ses règles.

]]>
http://sametmax.com/faire-des-enums-en-python/feed/ 59 3741