IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Python Discussion :

dico: get avec valeur par defaut ou gestion d'exception ?


Sujet :

Python

  1. #1
    Membre régulier
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    99
    Détails du profil
    Informations personnelles :
    Âge : 46
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 99
    Points : 102
    Points
    102
    Par défaut dico: get avec valeur par defaut ou gestion d'exception ?
    Bonjour tout le monde,

    Je suis en train d'écrire du code ou je fais pas mal appel à des dictionnaire dont les clé ne sont pas forcément présentes et je me demandais ce que vous pensiez des deux types d'écriture possible ?

    A savoir utiliser dict.get() avec une valeure par defaut.
    Code exemple : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    dico = {"key":"val"}
    if dico.get("toto", None) is None:
        'Fais des choses.'

    Ou bien utiliser dict[] avec une gestion d'exception.
    Code exemple : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    dico = {"key":"val"}
    try:
        dico["toto"]
    except KeyError, e
        'Fais des choses.'

    Personnellement pas vraiment un fan de la programmation avec exceptions dans tous les sens, je vais naturellement vers la solution du dict.get. Mais je me demandais ce que vous faisiez ou si vous avez des raisons de préférer une solution plutôt qu'une autre?

    Peut-être y a-t-il une différence de runtime, il faut que je vérifie, tient.

  2. #2
    Membre habitué
    Inscrit en
    Avril 2010
    Messages
    99
    Détails du profil
    Informations personnelles :
    Âge : 42

    Informations forums :
    Inscription : Avril 2010
    Messages : 99
    Points : 143
    Points
    143
    Par défaut
    Bonjour,

    De mon point de vue, on ne devrait utiliser les exceptions seulement en cas "exceptionnel", c'est-à-dire quand le programme ne se déroule pas comme prévu.
    Donc ton cas, je préfère la première solution mais c'est juste mon avis.
    Au niveau performance, il me semble également que la première solution soit plus efficace mais à vérifier.

    Il peut quand même y avoir un problème.
    C'est que la clé "toto" existe et qu'elle soit associée à None (ce qui est différent du fait que la clé "toto" n'existe pas).
    Il faut mieux utiliser
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    dico = {"key":"val"}
    if "toto" not in dico:
        'Fais des choses.'
    pour éviter ce genre de problème.

    Sinon tu peux regarder du coté des defaultdict.
    http://docs.python.org/library/colle...ns.defaultdict

  3. #3
    Membre éprouvé
    Homme Profil pro
    Inscrit en
    Décembre 2007
    Messages
    758
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France

    Informations professionnelles :
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Décembre 2007
    Messages : 758
    Points : 970
    Points
    970
    Par défaut
    bonjour,

    histoire d'alimenter le débat, je préfère pour ma part l'approche avec try... except... (dite eafp) plutôt que le if (dite lbyl).

    eafb pour, littéralement: "il est plus facile de demander le pardon que la permission".

    lbyl pour, littéralement: "regarder avant de sauter".

    l'approche try... except... est très efficace, bien plus que la première car il n'y a pas de test à effectuer.

    la performance diminue au fur et à mesure que l'exception se manifeste souvent.

    un peu de lecture ici (un mail d'alex Martelli):

    http://mail.python.org/pipermail/pyt...ay/203039.html

    De mon point de vue, certes très influencé par la lecture de son bouquin (Alex Martelli toujours lui ) on constate que l'approche eafb permet:

    - un code plus compact (bon dans ton cas la condition est simple, elle pourrait ne pas l'être)
    - une meilleure visibilité aussi: on voit directement ce que l'on veut vraiment faire au DEBUT de la structure et les cas particuliers sont traités ensuite alors que dans l'approche lbyl les cas particuliers sont traités à un même niveau.

    Un example que j'aime donner pour illustrer les bienfaits de l'approche eafb est le suivant. Supposons que nous voulions additionner deux entités mais que nous ne savons pas si elles sont compatibles.

    Il serait fastidieux de définir une structure conditionnelle qui permettrait d'identifier dans quel cas de figure on est:

    - 2 entiers
    - 2 réels
    - 1 entier 1 réel
    - 1 entier 1 chaine de caractère
    - etc...

    pour ensuite soit réaliser l'opération à proprement parlé soit lever une exception.

    Alors que l'approche eafb est directe:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    try:
        return a + b
    except TypeError:
        ...

  4. #4
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 313
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 313
    Points : 36 819
    Points
    36 819
    Par défaut
    Salut,

    Le test ci-dessous semble montrer que "try/catch" est moins bon que "key in dict" qui est un peu meilleur que que "get(, default)".
    Mais ce n'est qu'un test...

    Personnellement, j'utilise les 3 formes avec quelques principes:

    - la lisibilité d'abord me fait préférer "key in dict" ou "get default" dans tous les cas où çà peut auto-documenter le code,

    - la lisibilité encore... Aujourd'hui je me suis retrouvé avec des tas de lignes de code qui faisaient plein de key in dict, key in dict,... qui dans le cas False devaient remonter un message d'erreur spécifique.
    J'ai remplacé cela par:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @catch_errors()
    def method(...):
         v1 = get_key1()
         v2 = get_key2()
         ...
    def get_key1():
         try:
              v = dict.get(key1)
         except KeyError:
             raise MyError('no key1')
         return v
    - W

    Résultats:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    try_catch            : 0.085 Ms
    in_dict              : 0.083 Ms
    with_default         : 0.082 Ms
    --- and the winner is: 
    with_default         : 1.00%
    in_dict              : 1.01%
    try_catch            : 1.04%
    Code Python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    import random
     
    ENTRIES = 100
    mydict = {}
    for x in range(ENTRIES):
        i = random.randint(1, ENTRIES)
        mydict[i] = i
     
    def try_catch():
        i = random.randint(1, ENTRIES)
        try:
            v = mydict.get(i)
        except KeyError:
            pass
     
    def in_dict():
        i = random.randint(1, ENTRIES)
        if i in mydict:
            v = mydict[i]
     
    def with_default():
        i = random.randint(1, ENTRIES)
        v =  mydict.get (i, None)
     
     
    if __name__ == '__main__':
        from timeit import Timer
        import operator
        COUNT = 20
        REPEAT = 100
        names = ( 'try_catch', 'in_dict', 'with_default' )
        results = list()
        better = float(sys.maxint)
        for name in names:
             t = Timer('%s()' % name, "from __main__ import %s" % name)
             try:
    #            ms = 1000 * t.timeit(number=COUNT) / COUNT
                 ms = 1000 * min(t.repeat(repeat=REPEAT, number=COUNT))/ COUNT
     
             except:
                t.print_exc()
                sys.exit(1)
             print "%-20s : %0.3f Ms" % (name, ms )
             results.append((name, ms))
             if ms < better:
                 better = ms
     
        print '--- and the winner is: '
     
        for item in sorted(results,
                key=operator.itemgetter(1)):
            name, ms = item
            print "%-20s : %0.2f%%" % (name, ms/better )

  5. #5
    Membre expérimenté
    Homme Profil pro
    Inscrit en
    Mars 2007
    Messages
    941
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations forums :
    Inscription : Mars 2007
    Messages : 941
    Points : 1 384
    Points
    1 384
    Par défaut
    Par ordre de préférence:

    1) collections.defaultdict
    C'est élégant et efficace. C'est moins général que dict.get (la valeur par défaut n'est pas contrôlable au site d'utilisation) ce qui est une bonne chose: plus c'est spécifique, plus c'est facile à comprendre (et donc à s'assurer que c'est correct).

    2) dict.get
    Si defaultdict ne suffit pas, ou si le dict est importé d'un autre module qu'on ne veut/peut pas modifier.

    3) key (not) in dict
    Si dict.get n'est pas assez général (s'il y a un morceau de code substantiel à exécuter si la clé n'est pas dans le dictionnaire, ou s'il est simple mais contient des effets de bords).

    4) try: catch:
    Si le fait que la clé ne soit pas dans le dictionnaire est possible mais est vraiment une situation exceptionnelle (et donc rare). Cela peut être l'inverse également (qu'il soit exceptionnel que la clé soit dans le dictionnaire).

    En fait, la logique là-derrière est que je préfère choisir la forme qui représente le mieux la sémantique que j'ai en tête quand j'écris le code. Les différences de performance ne sont que secondaires, et sont faibles de toute façon.

  6. #6
    Membre régulier
    Profil pro
    Inscrit en
    Novembre 2008
    Messages
    99
    Détails du profil
    Informations personnelles :
    Âge : 46
    Localisation : France

    Informations forums :
    Inscription : Novembre 2008
    Messages : 99
    Points : 102
    Points
    102
    Par défaut
    Je vois que les avis sont pas mal partagés. En ce qui me concerne, mon approche actuelle est assez proche de celle de Dividee. Je calque mon écriture sur la sémantique de mon code.
    Juste je ne connaissais pas le defaultcict, je vais aller regarder ca de suite. Merci aussi pour la référence MarkMail qui était intéressant a lire.


    En tout cas, merci pour vos réponses.

  7. #7
    Membre éprouvé
    Avatar de Antoine_935
    Profil pro
    Développeur web/mobile
    Inscrit en
    Juillet 2006
    Messages
    883
    Détails du profil
    Informations personnelles :
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur web/mobile

    Informations forums :
    Inscription : Juillet 2006
    Messages : 883
    Points : 1 066
    Points
    1 066
    Par défaut
    Citation Envoyé par wiztricks Voir le message
    Le test ci-dessous semble montrer que "try/catch" est moins bon que "key in dict" qui est un peu meilleur que que "get(, default)".
    Mais ce n'est qu'un test...
    Le test a été effectué avec des clefs entières. C'est très simple à comparer. Ce n'est donc sans doute pas une grosse perte de trouver une clef à deux reprises (if key in dict: dict[key]). Il en va peut-être autrement pour des clefs de type str. Comme vous le dites, « ce n'est qu'un test ».

  8. #8
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 313
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 313
    Points : 36 819
    Points
    36 819
    Par défaut
    Citation Envoyé par Antoine_935 Voir le message
    Le test a été effectué avec des clefs entières. C'est très simple à comparer. Ce n'est donc sans doute pas une grosse perte de trouver une clef à deux reprises (if key in dict: dict[key]). Il en va peut-être autrement pour des clefs de type str.
    Pour moi le but était de comparer les différences dans les méthodes d'accès (1). Quelque soit la méthode d'accès, cela se termine par une fonction de hash et en un parcours de liste pour vérifier si l'élément existe ou pas (2).
    Si on prend des clés compliquées, on augmente à priori (2) dans tous les cas de tests et on 'tasse' les différences.

    => il ne me semblait pas "utile" de compliquer (2) _au contraire_.

    - W

    Pratique:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    try_catch            : 0.100 Ms
    in_dict              : 0.098 Ms
    with_default         : 0.097 Ms
    --- and the winner is: 
    with_default         : 1.00%
    in_dict              : 1.01%
    try_catch            : 1.03%
    Le (2) augmente le temps passé, et la variation pour le même test se tasse de 1%... pouièmes mais 'prévisibles'.


    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    NAME = 'XXXX une tres%(ident)dlongue cle pour voirXXX -%(ident)d'
    get_name = lambda ident: NAME % dict(ident=ident)
     
    for x in range(ENTRIES):
        i = random.randint(1, ENTRIES)
        name = get_name(i)
        mydict[name] = name
     
    def try_catch():
        i = random.randint(1, ENTRIES)
        try:
            v = mydict.get(get_name(i))
        except KeyError:
            pass
     
    def in_dict():
        name = get_name(random.randint(1, ENTRIES))
        if name in mydict:
            v = mydict[name]
     
    def with_default():
        name = get_name(random.randint(1, ENTRIES))
        v =  mydict.get (name, None)

  9. #9
    Membre éprouvé
    Avatar de Antoine_935
    Profil pro
    Développeur web/mobile
    Inscrit en
    Juillet 2006
    Messages
    883
    Détails du profil
    Informations personnelles :
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur web/mobile

    Informations forums :
    Inscription : Juillet 2006
    Messages : 883
    Points : 1 066
    Points
    1 066
    Par défaut
    Tiens, ça je ne m'y attendais pas.

    Ma remarque était basée sur l'idée suivante:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    try:
        v = dict[key] # 1 recherche
    except:
        ...
     
    if key in dict: # 1 recherche
        v = dict[key] # 2 recherches
    Bon, pour le dict.get il n'y a qu'une recherche.

    Mais de fait, le temps nécessaire pour faire le hash d'une chaîne de caractères est négligeable. J'ai tendance à l'oublier. Mais peut-être que python a une sorte de « cache » de hash.
    Quoi qu'il en soit, merci pour ce code intéressant


    Par curiosité, j'ai aussi testé ce code avec Jython et IronPython. Les résultats sont assez inattendus...

    Voici pour IronPython (2.6 beta 2 sous Mono (soit dit en passant, ubuntu est à la traine pour le packaging...)).
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    <string>:1: RuntimeWarning: IronPython has no support for disabling the GC
    try_catch            : 0.0076293945 Ms
    in_dict              : 0.0076293945 Ms
    with_default         : 0.0076293945 Ms
    --- and the winner is: 
    try_catch            : 1.00%
    in_dict              : 1.00%
    with_default         : 1.00%
    Ce scénario se produit 9 fois sur 10. Un effet du garbage collector, peut-être ?

    Et voici pour Jython (2.5.1).
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    *sys-package-mgr*: can't create package cache dir, '/opt/Jython/cachedir/packages
    try_catch            : 0.0999927521 Ms
    in_dict              : 0.0000000000 Ms
    with_default         : 0.0000000000 Ms
    --- and the winner is: 
    Traceback (most recent call last):
      File "hash.py", line 59, in <module>
        print "%-20s : %0.2f%%" % (name, ms/better )
    ZeroDivisionError: float division
    Surprenant, pas vrai ? En relançant le script une vingtaine de fois, j'ai fini par obtenir ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    try_catch            : 0.0999927521 Ms
    in_dict              : 0.0999927521 Ms
    with_default         : 0.0499963760 Ms
    --- and the winner is: 
    with_default         : 1.00%
    try_catch            : 2.00%
    in_dict              : 2.00%
    De nouveau, soit il s'agit du gc, soit du système de threads. Si quelqu'un a une explication concrète, je suis preneur

  10. #10
    Membre expérimenté
    Homme Profil pro
    Inscrit en
    Mars 2007
    Messages
    941
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations forums :
    Inscription : Mars 2007
    Messages : 941
    Points : 1 384
    Points
    1 384
    Par défaut
    Citation Envoyé par Antoine_935 Voir le message
    De nouveau, soit il s'agit du gc, soit du système de threads. Si quelqu'un a une explication concrète, je suis preneur
    A mon avis, il s'agit d'une imprécision de la mesure du temps dans ces systèmes. Je pense que si le gc perturbe les résultats, ce serait plutôt par un augmentation occasionnelle du temps d'exécution d'un test, mais comme chaque test est exécuté 100 fois et que seule la plus petite valeur est gardée, cela ne devrait pas se voir.

    En augmentant le temps d'exécution d'un test cela donnerait de meilleurs résultats. Il y a un autre problème dans le code de wiztricks; il inclut le temps d'appel de la fonction et le temps d'exécution de randint qui est probablement supérieur au test qui suit.

    La randomisation n'apporte rien ici. Autant utiliser des valeurs données, on connaîtra ainsi à priori le nombre de fois où la clé se trouve dans le dictionnaire ou non.

    Je pense que ce code-ci est meilleur:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    import sys
    from collections import defaultdict
     
    mydict = {0 : 0}
    mydefdict = defaultdict(lambda:None, mydict)
    COUNT = 30000
     
    def try_catch(i):
        for _ in xrange(COUNT):
            try:
                v = mydict.get(i)
            except KeyError:
                pass
     
    def in_dict(i):
        for _ in xrange(COUNT):
            if i in mydict:
                v = mydict[i]
     
    def get_with_default(i):
        for _ in xrange(COUNT):
            v =  mydict.get (i, None)
     
    def default_dict(i):
        for _ in xrange(COUNT):
            v = mydefdict[i]
     
    if __name__ == '__main__':
        from timeit import Timer
        import operator
        REPEAT = 100
        names = ( 'try_catch', 'in_dict', 'get_with_default', 'default_dict' )
        print "\nMETHOD               :  present / absent"
        for name in names:
            ms=[]
            for arg in (0,1):
                t = Timer('%s(%d)' % (name,arg), "from __main__ import %s" % name)
                try:
                    ms.append(1000 * min(t.repeat(repeat=REPEAT, number=1)))
                except:
                    t.print_exc()
                    sys.exit(1)
            print "%-20s : %.3f ms / %.3f ms" % (name, ms[0], ms[1])
    Il n'y a qu'un seul élément (0) dans le dictionnaire, et le test est effectué d'abord lorsque l'élément est présent dans le dictionnaire, et ensuite lorsqu'il est absent du dictionnaire. Une boucle explicite est utilisée (plutôt que de passer une valeur de number > 1 à timeit) afin que la boucle soit plus serrée autour de ce qui nous intéresse (la recherche dans le dicitonnaire, pas l'appel de fonction). Elle est répétée suffisament de fois pour que le temps d'exécution atteigne quelques millisecondes, ce qui devrait donner une mesure plus précise si la résolution de l'horloge est faible.

    Résultats:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    METHOD               :  present / absent
    try_catch            : 6.542 ms / 6.710 ms
    in_dict              : 4.886 ms / 2.415 ms
    get_with_default     : 6.925 ms / 6.872 ms
    default_dict         : 2.432 ms / 2.364 ms
    On peut remarquer que:
    1. defautdict est le grand gagnant
    2. Que la clé soit présente ou non, cela ne change pas grand chose sauf pour in_dict (il peut sortir plus tôt grâce au if). Je suis étonné que cela ne soit pas l'inverse pour try_catch; je pensais que le temps d'exécution serait nettement plus élevé si l'exception est levée mais la différence n'est pas si importante que ça.
    3. En lançant le script plusieurs fois, il y a malgré tout des variations non négligeables des résultats (par exemple, entre 6,4 et 6,6 ms). Cela donne une idée de la marge d'erreur.


    [EDIT:]
    le test avec default_dict est injuste; le premier accès à la clé 1 va ajouter cette clé dans le dictionnaire; le temps calculé pour default_dict dans le cas où la clé est absente n'est donc pas juste. Ceci devrait corriger le problème:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import sys
    from collections import defaultdict
     
    mydict = {0 : 0}
    mydefdict = defaultdict(lambda:None, mydict)
    COUNT = 30000
     
    def try_catch(i):
        for _ in xrange(COUNT):
            try:
                v = mydict.get(i)
            except KeyError:
                pass
     
    def in_dict(i):
        for _ in xrange(COUNT):
            if i in mydict:
                v = mydict[i]
     
    def get_with_default(i):
        for _ in xrange(COUNT):
            v =  mydict.get (i, None)
     
    def default_dict(i):
        if i == 0:
            for _ in xrange(COUNT):
                v = mydefdict[i]
        else:
            for i in xrange(COUNT):
                v = mydefdict[i]
     
    if __name__ == '__main__':
        from timeit import Timer
        import operator
        REPEAT = 100
        names = ( 'try_catch', 'in_dict', 'get_with_default', 'default_dict' )
        print "\nMETHOD               :  present / absent"
        for name in names:
            ms=[]
            for arg in (0,1):
                t = Timer('%s(%d)' % (name,arg), "from __main__ import %s" % name)
                mydefdict = defaultdict(lambda:None, mydict)
                try:
                    ms.append(1000 * min(t.repeat(repeat=REPEAT, number=1)))
                except:
                    t.print_exc()
                    sys.exit(1)
            print "%-20s : %.3f ms / %.3f ms" % (name, ms[0], ms[1])
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    METHOD               :  present / absent
    try_catch            : 6.538 ms / 6.717 ms
    in_dict              : 4.787 ms / 2.433 ms
    get_with_default     : 6.590 ms / 6.787 ms
    default_dict         : 2.354 ms / 3.208 ms

  11. #11
    Membre éprouvé
    Avatar de Antoine_935
    Profil pro
    Développeur web/mobile
    Inscrit en
    Juillet 2006
    Messages
    883
    Détails du profil
    Informations personnelles :
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur web/mobile

    Informations forums :
    Inscription : Juillet 2006
    Messages : 883
    Points : 1 066
    Points
    1 066
    Par défaut
    Citation Envoyé par dividee Voir le message
    il inclut le temps d'appel de la fonction et le temps d'exécution de randint qui est probablement supérieur au test qui suit.
    C'est vrai ça, je n'y avais pas pensé.

    Au vu des temps de try/except et get_with_default, il y a fort à parier que cette fonction utilise aussi le try/except. Et ce ne serait pas si étonnant : c'est déjà le cas pour hasattr.

    EDIT: Pourtant, j'ai le souvenir assez clair d'avoir optimisé un code en utilisant un try/catch, avec du Python 2.5. Et, de fait, le code tournait plus vite. Ça devait être dans une boucle assez spéciale... Le fait est que c'est le « setup » du try/catch et la gestion des éventuelles erreurs qui coutent cher. Si on peut éviter d'en faire trop, ça devient vite intéressant (cfr. infra)

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    import sys
     
    mydict = {0 : 0}
    COUNT = 30000
     
    def try_catch_get(i):
        try:
            for _ in xrange(COUNT):
                v = mydict.get(i)
        except KeyError:
            pass
     
    def try_catch_getitem(i):
        try:
            for _ in xrange(COUNT):
                v = mydict[i]
        except KeyError:
            pass
     
    def in_dict_get(i):
        for _ in xrange(COUNT):
            if i in mydict:
                v = mydict.get(i)
            else:
                break 
     
    def in_dict_getitem(i):
        for _ in xrange(COUNT):
            if i in mydict:
                v = mydict[i]
            else:
                break
     
    if __name__ == '__main__':
        from timeit import Timer
        import operator
        REPEAT = 100
        names = ( 'try_catch_get', 'try_catch_getitem', 'in_dict_get', "in_dict_getitem")
        print "\nMETHOD               :  present / absent"
        for name in names:
            ms=[]
            for arg in (0,1):
                t = Timer('%s(%d)' % (name,arg), "from __main__ import %s" % name)
                try:
                    ms.append(1000 * min(t.repeat(repeat=REPEAT, number=1)))
                except:
                    t.print_exc()
                    sys.exit(1)
            print "%-20s : %.3f ms / %.3f ms" % (name, ms[0], ms[1])
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    try_catch_get        : 3.406 ms / 3.399 ms
    try_catch_getitem    : 1.576 ms / 0.002 ms
    in_dict_get          : 5.468 ms / 0.000 ms
    in_dict_getitem      : 3.098 ms / 0.000 ms
    Notez au passage que la syntaxe dict[key] est, du moins chez moi, plus rapide. Beaucoup plus !
    Cela s'explique : il existe un bytecode dédié à la souscription […]:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    >>> def test(d):
    ...     d.get("haha")
    ... 
    >>> def test2(d):
    ...     d["haha"]
    ... 
    >>> from dis import dis
    >>> dis(test)
      2           0 LOAD_FAST                0 (d)
                  3 LOAD_ATTR                0 (get)
                  6 LOAD_CONST               1 ('haha')
                  9 CALL_FUNCTION            1
                 12 POP_TOP             
                 13 LOAD_CONST               0 (None)
                 16 RETURN_VALUE        
    >>> dis(test2)
      2           0 LOAD_FAST                0 (d)
                  3 LOAD_CONST               1 ('haha')
                  6 BINARY_SUBSCR       
                  7 POP_TOP             
                  8 LOAD_CONST               0 (None)
                 11 RETURN_VALUE
    D'ailleurs, si l'on réécrit le test avec […] au lieu du get:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def try_catch(i):
        for _ in xrange(COUNT):
            try:
                v = mydict[i]
            except KeyError:
                pass
     
    def in_dict(i):
        for _ in xrange(COUNT):
            if i in mydict:
                v = mydict[i]
    Voici le résultat:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    METHOD               :  present / absent
    try_catch            : 1.932 ms / 21.581 ms
    in_dict              : 3.077 ms / 1.520 ms
    get_with_default     : 3.769 ms / 3.795 ms
    default_dict         : 1.576 ms / 2.119 ms

  12. #12
    Membre expérimenté
    Homme Profil pro
    Inscrit en
    Mars 2007
    Messages
    941
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Belgique

    Informations forums :
    Inscription : Mars 2007
    Messages : 941
    Points : 1 384
    Points
    1 384
    Par défaut
    Je n'avais pas fait attention que try_catch utilisait get (j'avais recopié le code de wiztricks sans faire attention).

    C'est normal que l'ancien test donnait le même temps dans les deux cas, puisque mydict.get(x) est identique à mydict.get(x, None) et l'exception n'était jamais levée.

    Là le comportement est conforme à ce que j'attendais (un temps d'exécution beaucoup plus long lorsque l'exception est levée).

    L'ancien try_catch et get_with_defaults étaient donc essentiellement identiques, c'est pourquoi leur temps d'exécution est similaire. Et le nouveau résultat, avec try_catch corrigé, montre au contraire que get n'utilise PAS le méthode du try/except.

    Globalement, le defaultdict reste la solution la plus rapide en général, le in_dict (look before you leap) n'est intéressant que si l'on sait qu'une majorité des clés testées ne se trouveront pas dans le dictionnaire.

Discussions similaires

  1. [AC-2003] requete selection avec valeur par defaut
    Par benoitXV dans le forum Requêtes et SQL.
    Réponses: 2
    Dernier message: 05/03/2010, 15h31
  2. champ type lookup avec valeur par defaut
    Par jeinny dans le forum CRM
    Réponses: 0
    Dernier message: 17/02/2010, 15h44
  3. Réponses: 7
    Dernier message: 29/01/2009, 12h32
  4. Creation de table avec valeur par defaut
    Par Tsukaasa dans le forum JDBC
    Réponses: 0
    Dernier message: 01/09/2008, 17h45
  5. Réponses: 2
    Dernier message: 19/01/2007, 20h00

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo