generator – 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 Views VS generators http://sametmax.com/views-vs-generators/ http://sametmax.com/views-vs-generators/#comments Wed, 25 Mar 2015 18:57:20 +0000 http://sametmax.com/?p=15993 Avec Python 2.7, un outil appelé les “views” (les “vues”) est apparu. Une vue est juste un enrobage qui permet de voir un objet d’une certaine façon, et de le manipuler d’une certaine façon (avec une autre API), sans changer cet objet.

Les vues ont surtout été notables pour leur apparition dans les dictionnaires avec Python 2.7:

    >>> scores = {"sam": 1, "max": 0}
    >>> scores.items() # retourne une lsite
    [('max', 0), ('sam', 1)]
    >>> scores.iteritems() # retourne un générateur
    
    >>> list(scores.iteritems()) # sans views
    [('max', 0), ('sam', 1)]
    >>> scores.viewsitems() # avec views
    Traceback (most recent call last):
      File "", line 1, in 
        scores.viewsitems() # avec views
    AttributeError: 'dict' object has no attribute 'viewsitems'

    >>> scores.viewitems() # retourne une vue
    dict_items([('max', 0), ('sam', 1)])

Néanmoins personne ne les a vraiment utilisé, et c’est un tort. Elles sont en effet très performantes, et pour cette raison sont retournées par défaut avec items() en Python 3.

En effet, les vues ne sont qu’un enrobage : elles ne contiennent rien, et donc ne prennent pas beaucoup mémoire, tout comme les générateurs.

Mais contrairement aux générateurs, les vues ne se vident pas et peuvent exposer une API plus complète que les générateurs, comme par exemple déclarer une taille :

>>> items = scores.iteritems()
>>> list(items)
[('max', 0), ('sam', 1)]
>>> list(items) # woops
[]
>>> items = scores.viewitems()
>>> list(items)
[('max', 0), ('sam', 1)]
>>> list(items)
[('max', 0), ('sam', 1)]
>>> len(scores.iteritems()) # nope
Traceback (most recent call last):
  File "", line 1, in 
    len(scores.iteritems())
TypeError: object of type 'dictionary-itemiterator' has no len()

>>> len(scores.viewitems())
2

Alors certes, on ne peut pas mettre des vues partout, et les générateurs restent utiles. Mais quand il est possible de les utiliser, et à moins d’avoir besoin d’une liste afin de modifier les valeurs in place, il n’y pas de raison de ne pas le faire : c’est le meilleur des deux mondes.

]]>
http://sametmax.com/views-vs-generators/feed/ 6 15993
Aplatir un iterable like a boss en Python http://sametmax.com/applatir-un-iterable-like-a-boss-en-python/ http://sametmax.com/applatir-un-iterable-like-a-boss-en-python/#comments Fri, 23 Aug 2013 10:08:55 +0000 http://sametmax.com/?p=7172 >>> l = [(1, 2), (3, 4), (5, 6)] >>> [y for x in l for y in x] [1, 2, 3, 4, 5, 6] Mais quand on a beaucoup de niveaux...]]> Des structures un peu imbriquées ne sont pas trop difficiles à traiter en Python.

Par exemple, avec une liste en intention imbriquée :

>>> l = [(1, 2), (3, 4), (5, 6)]
>>> [y for x in l for y in x]
[1, 2, 3, 4, 5, 6]

Mais quand on a beaucoup de niveaux, par exemple…

a = []
for i in xrange(500):
    a = [a, i]
print(a)

[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[], 0], 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], 54], 55], 56], 57], 58], 59], 60], 61], 62], 63], 64], 65], 66], 67], 68], 69], 70], 71], 72], 73], 74], 75], 76], 77], 78], 79], 80], 81], 82], 83], 84], 85], 86], 87], 88], 89], 90], 91], 92], 93], 94], 95], 96], 97], 98], 99], 100], 101], 102], 103], 104], 105], 106], 107], 108], 109], 110], 111], 112], 113], 114], 115], 116], 117], 118], 119], 120], 121], 122], 123], 124], 125], 126], 127], 128], 129], 130], 131], 132], 133], 134], 135], 136], 137], 138], 139], 140], 141], 142], 143], 144], 145], 146], 147], 148], 149], 150], 151], 152], 153], 154], 155], 156], 157], 158], 159], 160], 161], 162], 163], 164], 165], 166], 167], 168], 169], 170], 171], 172], 173], 174], 175], 176], 177], 178], 179], 180], 181], 182], 183], 184], 185], 186], 187], 188], 189], 190], 191], 192], 193], 194], 195], 196], 197], 198], 199], 200], 201], 202], 203], 204], 205], 206], 207], 208], 209], 210], 211], 212], 213], 214], 215], 216], 217], 218], 219], 220], 221], 222], 223], 224], 225], 226], 227], 228], 229], 230], 231], 232], 233], 234], 235], 236], 237], 238], 239], 240], 241], 242], 243], 244], 245], 246], 247], 248], 249], 250], 251], 252], 253], 254], 255], 256], 257], 258], 259], 260], 261], 262], 263], 264], 265], 266], 267], 268], 269], 270], 271], 272], 273], 274], 275], 276], 277], 278], 279], 280], 281], 282], 283], 284], 285], 286], 287], 288], 289], 290], 291], 292], 293], 294], 295], 296], 297], 298], 299], 300], 301], 302], 303], 304], 305], 306], 307], 308], 309], 310], 311], 312], 313], 314], 315], 316], 317], 318], 319], 320], 321], 322], 323], 324], 325], 326], 327], 328], 329], 330], 331], 332], 333], 334], 335], 336], 337], 338], 339], 340], 341], 342], 343], 344], 345], 346], 347], 348], 349], 350], 351], 352], 353], 354], 355], 356], 357], 358], 359], 360], 361], 362], 363], 364], 365], 366], 367], 368], 369], 370], 371], 372], 373], 374], 375], 376], 377], 378], 379], 380], 381], 382], 383], 384], 385], 386], 387], 388], 389], 390], 391], 392], 393], 394], 395], 396], 397], 398], 399], 400], 401], 402], 403], 404], 405], 406], 407], 408], 409], 410], 411], 412], 413], 414], 415], 416], 417], 418], 419], 420], 421], 422], 423], 424], 425], 426], 427], 428], 429], 430], 431], 432], 433], 434], 435], 436], 437], 438], 439], 440], 441], 442], 443], 444], 445], 446], 447], 448], 449], 450], 451], 452], 453], 454], 455], 456], 457], 458], 459], 460], 461], 462], 463], 464], 465], 466], 467], 468], 469], 470], 471], 472], 473], 474], 475], 476], 477], 478], 479], 480], 481], 482], 483], 484], 485], 486], 487], 488], 489], 490], 491], 492], 493], 494], 495], 496], 497], 498], 499]

Là je désactive la coloration syntaxique du blog parce que le snippet a fait planté sublime :-D Heureusement, il reste VI.

Bref, quand on a ce genre de truc, comment on fait ? Pire, comment on traite un flux de données de types hétérogènes, et dont on ne connait pas la taille, ou de longueur infinie ? C’est une caractéristique de Python : on a des générateurs plein de duck typing partout !

Voici un petit snippet un peu tordu, mais qui fait des merveilles :

from collections import deque, OrderedDict, MutableSet, defaultdict


class Flattener(object):

    # les types qu'on va aplatir, c'est à dire la plupart
    # des iterables sauf les hashmaps
    DEFAULT_FLATTEN_TYPES = (
        list,
        tuple,
        set,
        (x for x in ()).__class__,
        xrange,
        deque,
        MutableSet,
    )

    # par défaut, on utilise DEFAULT_FLATTEN_TYPES et
    # aucun iterable_getters (on verra ce que c'est plus bas)
    # puisque c'est le cas le plus courant d'utilisation
    def __init__(self, flatten_types=None, iterable_getters={}):
        self.flatten_types = flatten_types or self.DEFAULT_FLATTEN_TYPES
        self.iterable_getters = iterable_getters


    # Retourne True si on doit aplatir l'objet.
    # Par défaut, vérifie dans si l'objet est d'un des types
    # DEFAULT_FLATTEN_TYPE.
    def should_flatten(self, obj):
        return isinstance(obj, self.flatten_types)

    # Si avant d'aplatir l'objet, l'objet a besoin d'une transformation
    # (par exemple appeler items() sur un dico), on l'applique. 
    # Par défaut il n'y a aucune transformation appliquée, quelque soit le 
    # type.
    def transform_iterable(self, obj):
        if obj.__class__ in self.iterable_getters:
            return self.iterable_getters[obj.__class__](obj)
        return obj


    # Permet d'appeler une instance de Flatener, comme si c'était une fonction
    def __call__(self, iterable):
        for e in:
            # Si un élément est à aplatir, on fait un appel récursif
            if self.should_flatten(e):
                # Appel récursif, et yielding du résultat de cet appel.
                for f in self(self.transform_iterable(e)):
                    yield f
            # On ne doit pas aplatir l'element (genre un int, un str...)
            # donc on le yield directement
            else:
                yield e


# fabrique un flattener, ici on prend la config par défaut
flatten = Flattener()

# et pouf

a = []
for i in xrange(500):
    a = [a, i]

applatie = list(flatten(a))

print(len(applatie))
print(applatie[:10])
5500
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Ca gère une longeur infinie, et une imbrication de centaines de niveaux. Comme Python a une limite de recursions (1000 par défaut), on est quand même bridé, mais c’est une situation rare d’avoir autant de nesting. Et dans les cas extrêmes, on peut allouer une plus grande stack avec sys.setrecursionlimit().

Via flatten_types, on peut créer différentes politiques d’aplatissement, bien que celle par défaut soit assez saine. Par exemple décider d’aplatir les strings, ou ne pas aplatir les tuples : il suffit de passer la bonne liste de types en paramètres. Comme le Flattener est une classe qui permet de créer flatten(), on peut la sous-classer et mettre à disposition plusieurs aplatisseurs personnalisés dans sa lib.

La partie :

                if e.__class__ in self.iterable_getters:
                    e = self.iterable_getters[e.__class__](e)

Permet de gérer des cas ambigüs, comme par exemple les dicos. Comment itérer dessus ? Par clé, par valeur ? Par défaut on ne les aplatit pas

On peut par exemple choisir d’aplatir complètement les dictionnaires. :

a = []
for i in xrange(2):
    a = [a, i] + [{'a': 1., 'b': {'c': 3.}}]
print(a)

[[[], 0, {'a': 1.0, 'b': {'c': 3.0}}], 1, {'a': 1.0, 'b': {'c': 3.0}}]

# on rajoute les dictionnaires aux types à aplatir
new_ft = Flattener.DEFAULT_FLATTEN_TYPES + (dict,)

dico_flatten = Flattener(flatten_types=new_ft,
                         # on dit qu'un dico rencontré doit être transformé
                         # via items() avant iteration
                         iterable_getters={dict: lambda x: x.items()})

print(list(dico_flatten(a)))

[0, u'a', 1.0, u'b', u'c', 3.0, 1, u'a', 1.0, u'b', u'c', 3.0]

On peut même overrider should_flatten et transform_iterable si des besoins plus importants se font sentir.

Attention tout de même à ce que vous mettez dans flatten_types. Par exemple, une string d’un caractère est à la fois yieldable comme valeur et itérable, ce qui va provoquer une recursion infinie. Adaptez toujours iterable_getters en conséquence.

Hop, dans batbelt.

]]>
http://sametmax.com/applatir-un-iterable-like-a-boss-en-python/feed/ 5 7172
S’affranchir des doublons d’un itérable en Python http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/ http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/#comments Tue, 20 Aug 2013 10:10:32 +0000 http://sametmax.com/?p=7143 Supprimer ou ignorer les doublons d’un itérable tel qu’une liste ou un array est un challenge dans tous les langages. Il faut se poser les questions suivantes :

  • Qu’est-ce qu’un doublon ?
  • Quels types d’itérables traite-t-on ?
  • Quel est la taille de l’itérable ?
  • Et niveau perfs ?

En Python, on a des structures de données qui suppriment automatiquement les doublons : les sets et les dictionnaires. Mais elles ne conservent pas l’ordre des élements.

Il y a aussi le fait qu’un itérable en Python peut avoir une taille inconnue, ou infinie.

Le post est long, donc…

Solution 1 : générateur et hashing

En utilisant conjointement les générateurs, les sets et une petite injection de dépendance, on peut trouver un compromis entre flexibilité et performances :

def skip_duplicates(iterable, key=lambda x: x):

    # on va mettre l’empreinte unique de chaque élément dans ce set
    fingerprints = set()

    for x in iterable:
        # chaque élement voit son emprunte calculée. Par défaut l’empreinte
        # est l'élément lui même, ce qui fait qu'il n'y a pas besoin de
        # spécifier 'key' pour des primitives comme les ints ou les strings.
        fingerprint = key(x)

        # On vérifie que l'empreinte est dans la liste des empreintes  des
        # éléments précédents. Si ce n'est pas le cas, on yield l'élément, et on
        # rajoute sont empreinte ans la liste de ceux trouvés, donc il ne sera
        # pas yieldé si on ne le yieldera pas une seconde fois si on le
        # rencontre à nouveau
        if fingerprint not in fingerprints:
            yield x
            fingerprints.add(fingerprint)

La fonction s’appelle skip_duplicates car c’est ce qu’elle fait. Elle ne retire pas vraiment les doublons, elle produit un flux de d’éléments qui ne comporte pas de doublons en ignorant tout doublons présent dans l’itérable initial.

Cette approche a plusieurs avantages :

  • Les doublons sont bien retirés, et l’ordre est conservé.
  • La complexité est de 0(n).
  • L’utilisateur peut choisir ce qui fait qu’un élément est unique : un attribut, un sous-élément, l’affichage sous forme de string…
  • C’est un générateur, est cela fonctionne donc avec des itérables de toute taille, même inconnue ou infinie.

Il faut néanmoins que l’ensemble des éléments uniques tiennent au moins une fois en mémoire en plus de l’itérable initial, et potentiellement d’un stockage à la sortie du générateur. On fait donc un trade-off sur la mémoire.

Comme la valeur de key par défaut est une valeur saine, ça fonctionne comme on s’y attend pour les cas simples :

>>> list(skip_duplicates([1, 2, 3, 4, 4, 2, 1, 3 , 4]))
[1, 2, 3, 4]
>>> list(skip_duplicates('fjsqlkdmfjsklqmdfjdmsl'))
[u'f', u'j', u's', u'q', u'l', u'k', u'd', u'm']
>>> list(skip_duplicates(((1, 2), (2, 1), (1, 2), (1, 1))))
[(1, 2), (2, 1), (1, 1)]

Pourvoir spécifier ‘key’ permet de faire des choix dans ce qu’est un doublon :

>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3')))
[1, 2, u'1', 3, u'3']
>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3'), key=lambda x: str(x)))
[1, 2, 3]

Et si on s’attaque à des cas plus complexes, le fonction vous force à préciser votre pensée :

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
... )
Traceback (most recent call last):
  File "", line 1, in 
    list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
  File "", line 7, in skip_duplicates
    if fingerprint not in fingerprints:
TypeError: unhashable type: 'list'

En effet les listes ne sont pas des types hashables en Python, on ne peut donc pas les stocker dans un set.

Mais on peut caster la liste, et faire ainsi le choix de savoir sur quel critère on base notre égalité. Par exemle, considère-t-on que deux itérables ayant le même contenu sont égaux, où alors doivent-ils avoir le même type ?

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: tuple(x)))
[[], [1, 2]]
>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: (type(x), tuple(x))))
[[], (), [1, 2], (1, 2)]

Nous utilisons le fait que :

>>> tuple([1, 2]) == (1, 2)
True
>>> (type([1, 2]), tuple([1, 2])) == (type((1, 2)), (1, 2))
False

Puisque :

>>> (type([1, 2]), tuple([1, 2]))
(, (1, 2))
>>> (type((1, 2)), (1, 2))
(, (1, 2))

Dans le cas où nous ne sommes pas capables de déterminer ce qu’est un doublon, la fonction ne retire simplement rien :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo

>>> list(skip_duplicates([Test(), Test(), Test('other')]))
[Test('bar'), Test('bar'), Test('other')]

Mais permet encore une fois de faire le choix de quoi retirer :

>>> list(skip_duplicates([Test(), Test(), Test('other')], key=lambda x: x.foo))
[Test('bar'), Test('other')]

Ce principe de la fonction key, on le retrouve dans sorted(), donc les habitués seront déjà à l’aise. Et j’aime beaucoup ce pattern, car il est très puissant. On peut avoir la fonction key qui renvoit des choses très simples :

  • Un attribut.
  • Un element (x[2], x['cle']…)
  • Une version castée avec int(), str(), tuple(), etc

Mais on peut aussi faire des choses très complexes. En effet, rien ne nous oblige à utiliser une lambda, on peut mettre une fonction complète et lui faire retourner :

  • Un hash md5.
  • Une entrée en base de données.
  • Un nouvel objet customisé.
  • Un tuple de tuples d’objets custos avec des dictionnaires en attributs…
  • Le contenu d’un fichier.

Python sait naturellement comparer tout ça.

Notez que nous trichons un peu, puisque nous retirons les doublons en nous basant sur un set qui va calculer un hash de l’objet, et pas véritablement vérifier l’égalité. La fonction en fonctionnera donc pas si l’utilisateur définie __eq__ et s’attend à ce que les doublons soient retirés. Ce qui nous amène à …

Solution 2 : iterateur et comparaison

Pour ce genre de chose, un autre algo, qui ne fontionerait que sur les itérables de taille finie, et qui serait bien plus lent (complexité n log(n)), peut être utilisé :

def strip_duplicates(iterable, equals=lambda x, y: x == y):

    # On transforme l'itérable en iterateur sur lui même, cela va nous
    # permettre d'appeler next() dessus et récupérer le premier élément,
    # même sur un objet non indexable (sur lequel on ne peut utiliser [0])
    iterable = iter(iterable)

    res = []
    # Une petite boucle infinie est nécessaire car la boucle 'for' ne nous
    # permet pas de récupérer le premier élément indépendamment des autres,
    # et la boucle 'while' attend une condition de sortie, ce que nous n'avons
    # pas forcément (il n'est pas possible de vérifier le nombre d'éléments
    # restant dans un générateur).
    while True:

        # on récupère le premier élément de l'iterable restant, si il n'y en
        # a plus, on sort de la boucle.
        try:
            elem = next(iterable)
        except StopIteration:
            break

        # Le premier élément est ajouté au résultat sans doublons. Maintenant
        # on va recréer l'itérable, mais en retirant tout ce qui était égal
        # au premier élément. Notez que 'être égal' est une condition modifiable
        # en passant une fonction en paramètre, comme l'était 'key' précédemment.
        res.append(elem)

        iterable = iter([x for x in iterable if not equals(elem, x)])

    return res

La fonction s’appelle strip_duplicates car elle produit une nouvelle liste, mais sans les éléments indésirables, comme le fait strip() sur une chaîne (produit une nouvelle chaîne, sans les éléments indésirables).

Ce type de fonction peut être utile dans plusieurs cas :

  • On a pas envie de se poser la question de savoir si nos types à comparer sont hashable ou pas, et on est prêt à payer un peu de CPU pour cela.
  • On a besoin de retirer les doublons sur la base d’une égalité, par exemple sur l’existence de la méthode __eq__.
  • On a besoin de retirer les doublons sur la base d’une logique complexe qui dépend du contexte.

A première vu cela fonctionne presque de la même manière que skip_duplicates, mais en retournant une liste plutôt qu’un générateur :

>>> strip_duplicates('fdjqkslfjdmkfdsqjkfmjqsdmlkfjqslkmfjsdklfl')
['f', 'd', 'j', 'q', 'k', 's', 'l', 'm']

Mais déjà il n’y a pas à se soucier de savoir si une structure de données est hashable :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)))
[[], (), [1, 2], (1, 2)]

Même si on garde la même flexibilité, bien que la fonction à passer ait une signature légèrement différente :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x, y: tuple(x) == tuple(y))
[[], [1, 2]]

Le plus interessant reste que cela fonctionne sur l’égalité, et donc cela marche d’office avec les objets qui déclarent __eq__ ce qui est le cas dans de nombreuses libs, comme les ORM :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo
    def __eq__(self, other):
        return self.foo == other.foo

>>> strip_duplicates([Test(), Test(), Test('other')])
[Test('bar'), Test('other')]

Dans certains cas, notamment dans le cas où le point de comparaison est un object non hashable de très grosse taille (par exemple un dico très long), on peut espérer aussi pas mal économiser en mémoire. Mais on est qu’en est-il des besoins en mémoire et en CPU ?

Solution 3 : retirer les doublons, in place

Enfin, pour ceux qui ont de grosses contraintes de mémoire et qui veulent un algo rapide au prix de la flexibilité du code, voici une solution qui oblige à travailler sur des listes et à modifier la liste sur place :

def remove_duplicates(lst, equals=lambda x, y: x == y):

    # Normalement en Python on adore le duck typing, mais là cet algo suppose
    # l'usage d'une liste, donc on met un gardefou.
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')

    # là on est sur quelque chose qui ressemble vachement plus à du code C ^^
    i1 = 0
    l = (len(lst) - 1)

    # on itère mécaniquement sur la liste, à l'ancienne, avec nos petites
    # mains potelées.
    while i1 < l:

        # on récupère chaque élément de la liste, sauf le dernier
        elem = lst[i1]

        # on le compare à l'élément suivant, et chaque élément après
        # l'élément suivant
        i2 = i1 + 1
        while i2 <= l:
            # en cas d'égalité, on retire l'élément de la liste, et on
            # décrément la longueur de la liste ainsi amputée
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1

        i1 += 1

    return lst

Et là on est bien dans de la modification sur place :

>>> lst = list('fjdsklmqfjskdfjmld')
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q', u'f', u'j', u's', u'k', u'd', u'f', u'j', u'm', u'l', u'd']
>>> remove_duplicates(lst)
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']

La fonction s'appelle cette fois bien remove_duplicates puisque c'est ce qu'elle fait : retirer les doublons de la liste originale.

Et maintenant, c'est l'heure du benchmark à deux balles !

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(skip_duplicates(lst))', setup=setup, number=1000)
0.9810519218444824

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)

        iterable = iter([x for x in iterable if not equals(elem, x)])

    return res

import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
41.462974071502686

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst

import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
0.37493896484375

Sans surprise, la version inplace est la plus rapide puisque la plus restrictive. En second vient notre strip_duplicates, beaucoup fois plus lente. Et en dernier, 50 fois plus lente, le compromis entre les deux : souple, consomme moins de mémoire que skip, mais plus que remove.

Mais ce n'est pas très juste pour strip, puisque que skip n'a pas à faire un gros travail de conversion. Essayons avec des clés plus grosses :

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(skip_duplicates(lst, lambda x: tuple(x)))', setup=setup, number=1000)
15.516181945800781

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)

        iterable = iter([x for x in iterable if not equals(elem, x)])

    return res

import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
22.047110080718994

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst

import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
14.763166904449463

Comme souvent les résultats sont contre untuitifs, car bien que remove garde son avance, elle s'est largement réduite. A l'inverse, skip n'est pas tant à la ramasse que ça, et strip reste le plus lent.

Il faudrait aussi mesurer la consommation mémoire, je suis certain que ce serait interessant.

Bon, il est temps de mettre tout ça dans batbelt.

]]>
http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/feed/ 15 7143
Comment utiliser yield et les générateurs en Python ? http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/ http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/#comments Tue, 02 Oct 2012 12:09:42 +0000 http://sametmax.com/?p=2416 Les générateurs sont une fonctionalité fabuleuse de Python, et une étape indispensable dans la maîtrise du langage. Une fois compris, vous ne pourrez plus vous en passer.

Rappel sur les itérables

Quand vous lisez des éléments un par un d’une liste, on appelle cela l’itération:

lst = [1, 2, 3]
>>> for i in lst :
...     print(i)
1
2
3

Et quand on utilise une liste en intension, on créé une liste, donc un itérable. Encore une fois, avec une boucle for, on prend ses éléments un par un, donc on itère dessus:

lst = [x*x for x in range(3)]
>>> for i in lst :
...     print(i)
0
1
4

À chaque fois qu’on peut utiliser “forin…” sur quelque chose, c’est un itérable : lists, strings, files…

Ces itérables sont pratiques car on peut les lire autant qu’on veut, mais ce n’est pas toujours idéal car on doit stocker tous les éléments en mémoire.

Les générateurs

Si vous vous souvenez de l’article sur les comprehension lists, on peut également créer des expressions génératrices:

generateur = (x*x for x in range(3))
>>> for i in generateur :
...     print(i)
0
1
4

La seule différence avec précédemment, c’est qu’on utilise () au lieu de []. Mais on ne peut pas lire generateur une seconde fois car le principe des générateurs, c’est justement qu’ils génèrent tout à la volée: ici il calcule 0, puis l’oublie, puis calcule 1, et l’oublie, et calcule 4. Tout ça un par un.

Le mot clé yield

yield est un mot clé utilisé en lieu et place de return, à la différence près qu’on va récupérer un générateur.

>>> def creerGenerateur() :
...     mylist = range(3)
...     for i in mylist:
...         yield i*i
...
>>> generateur = creerGenerateur() # crée un générateur
>>> print(generateur) # generateur est un objet !
< generator object creerGenerateur at 0x2b484b9addc0>
>>> for i in generateur:
...     print(i)
0
1
4

Ici c’est un exemple inutile, mais dans la vraie vie vivante, c’est pratique quand on sait que la fonction va retourner de nombreuses valeurs qu’on ne souhaite lire qu’une seule fois.

Le secret des maîtres Zen qui ont acquis la compréhension transcendantale de yield, c’est de savoir que quand on appelle la fonction, le code de la fonction n’est pas exécute. A la place, la fonction va retourner un objet générateur.

C’est pas évident à comprendre, alors relisez plusieurs fois cette partie.

creerGenerateur() n’exécute pas le code de creerGenerateur.

creerGenerateur() retourne un objet générateur.

En fait, tant qu’on ne touche pas au générateur, il ne se passe rien. Puis, dès qu’on commence à itérer sur le générateur, le code de la fonction s’exécute.

La première fois que le code s’éxécute, il va partir du début de la fonction, arriver jusqu’à yield, et retourner la première valeur. Ensuite, à chaque nouveau tour de boucle, le code va reprendre de la où il s’est arrêté (oui, Python sauvegarde l’état du code du générateur entre chaque appel), et exécuter le code à nouveau jusqu’à ce qu’il rencontre yield. Donc dans notre cas, il va faire un tour de boucle.

Il va continuer comme ça jusqu’à ce que le code ne rencontre plus yield, et donc qu’il n’y a plus de valeur à retourner. Le générateur est alors considéré comme définitivement vide. Il ne peut pas être “rembobiné”, il faut en créer un autre.

La raison pour laquelle le code ne rencontre plus yield est celle de votre choix: condition if/else, boucle, recursion… Vous pouvez même yielder à l’infini.

Un exemple concret et un café, plz

yield permet non seulement d’économiser de la mémoire, mais surtout de masquer la complexité d’un algo derrière une API classique d’itération.

Supposez que vous ayez une fonction qui – tada ! – extrait les mots de plus de 3 caractères de tous les fichiers d’un dossier.

Elle pourrait ressembler à ça:

import os

def extraire_mots(dossier):
    for fichier in os.listdir(dossier):
        with open(os.path.join(dossier, fichier)) as f:
            for ligne in f:
                for mot in ligne.split():
                    if len(mot) > 3:
                        yield mot

Vous avez là un algo dont on masque complètement la complexité, car du point de vue de l’utilisateur, il fait juste ça:

for mot in extraire_mots(dossier):
    print mot

Et pour lui c’est transparent. En plus, il peut utiliser tous les outils qu’on utilise sur les itérables d’habitude. Toutes les fonctions qui acceptent les itérables acceptent donc le résultat de la fonction en paramètre grâce à la magie du duck typing. On créé ainsi une merveilleuse toolbox.

Controller yield

>>> class DistributeurDeCapote():
    stock = True
    def allumer(self):
        while self.stock:
            yield "capote"
...

Tant qu’il y a du stock, on peut récupérer autant de capotes que l’on veut.

>>> distributeur_en_bas_de_la_rue = DistributeurDeCapote()
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> print distribuer.next()
capote
>>> print distribuer.next()
capote
>>> print([distribuer.next() for c in range(4)])
['capote', 'capote', 'capote', 'capote']

Dès qu’il n’y a plus de stock…

>>> distributeur_en_bas_de_la_rue.stock = False
>>> distribuer.next()
Traceback (most recent call last):
  File "", line 1, in 
    distribuer.next()
StopIteration
< type 'exceptions.StopIteration'>

Et c’est vrai pour tout nouveau générateur:

>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> distribuer.next()
Traceback (most recent call last):
  File "", line 1, in 
    distribuer.next()
StopIteration

Allumer une machine vide n’a jamais permis de remplir le stock ;-) Mais il suffit de remplir le stock pour repartir comme en 40:

>>> distributeur_en_bas_de_la_rue.stock = True
>>> distribuer = distributeur_en_bas_de_la_rue.allumer()
>>> for c in distribuer :
...     print c
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
capote
...

itertools: votre nouveau module favori

Le truc avec les générateurs, c’est qu’il faut les manipuler en prenant en compte leur nature: on ne peut les lire qu’une fois, et on ne peut pas déterminer leur longeur à l’avance. itertools est un module spécialisé là-dedans: map, zip, slice… Il contient des fonctions qui marchent sur tous les itérables, y compris les générateurs.

Et rappelez-vous, les strings, les listes, les sets et même les fichiers sont itérables.

Chaîner deux itérables, et prendre les 10 premiers caractères ? Piece of cake !

>>> import itertools
>>> d = DistributeurDeCapote().allumer()
>>> generateur = itertools.chain("12345", d)
>>> generateur = itertools.islice(generateur, 0, 10)
>>> for x in generateur:
...     print x
...     
1
2
3
4
5
capote
capote
capote
capote
capote

Les dessous de l’itération

Sous le capot, tous les itérables utilisent un générateur appelé “itérateur”. On peut récupérer l’itérateur en utiliser la fonction iter() sur un itérable:

>>> iter([1, 2, 3])
< listiterator object at 0x7f58b9735dd0>
>>> iter((1, 2, 3))
< tupleiterator object at 0x7f58b9735e10>
>>> iter(x*x for x in (1, 2, 3))
< generator object  at 0x7f58b9723820>

Les itérateurs ont une méthode next() qui retourne une valeur pour chaque appel de la méthode. Quand il n’y a plus de valeur, ils lèvent l’exception StopIteration:

>>> gen = iter([1, 2, 3])
>>> gen.next()
1
>>> gen.next()
2
>>> gen.next()
3
>>> gen.next()
Traceback (most recent call last):
  File "< stdin>", line 1, in < module>
StopIteration

Message à tous ceux qui pensent que je fabule quand je dis qu’en Python on utilise les exceptions pour contrôler le flux d’un programme (sacrilège !): ceci est le mécanisme des boucles internes en Python. Les boucles for utilisent iter() pour créer un générateur, puis attrappent une exception pour s’arrêter. À chaque boucle for, vous levez une exception sans le savoir.

Pour la petite histoire, l’implémentation actuelle est que iter() appelle la méthode __iter__() sur l’objet passé en paramètre. Donc ça veut dire que vous pouvez créer vos propres itérables:

>>> class MonIterableRienQuaMoi(object):
...     def __iter__(self):
...         yield 'Python'
...         yield "ça"
...         yield 'déchire'
...
>>> gen = iter(MonIterableRienQuaMoi())
>>> gen.next()
'Python'
>>> gen.next()
'ça'
>>> gen.next()
'déchire'
>>> gen.next()
Traceback (most recent call last):
  File "< stdin>", line 1, in < module>
StopIteration
>>> for x in MonIterableRienQuaMoi():
...     print x
...
Python
ça
déchire
]]>
http://sametmax.com/comment-utiliser-yield-et-les-generateurs-en-python/feed/ 24 2416
Quelques erreurs tordues et leurs solutions en Python http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/ http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/#comments Sun, 24 Jun 2012 02:29:56 +0000 http://sametmax.com/?p=995 Quand vous débuggez, rappelez-vous que pdb est votre ami, et qu’il est souvent bon de supprimer tous les fichiers .pyc pour éviter la confusion. Mais parfois l’erreur semble n’avoir aucun sens. Bien que Python soit un langage dont l’une des grandes qualités soit la cohérence, voici une liste d’erreurs et leurs solutions qui ont tendance à énerver (les erreurs hein, pas les solutions).

NameError: name 'x' is not defined

Python plante en annonçant que la variable n’est pas définie. Vous allez à la ligne donnée, et elle est là. Vous vérifiez qu’il n’y a pas de faute de frappe (genre un zéro mélangé avec la lettre O), ni une majuscule ou une minuscule échangée quelque part (Python est sensible à la casse).

Et rien.

Tout est niquel.

Alors pourquoi ça plante bordel de merde ?

Et bien ce message qui n’aide absolument pas peut venir du fait que les closures sont en lecture seule en Python. En résumé, vous avez essayé de faire un truc comme ça:

chose = 'truc'
def fonction():
    chose = 'machin'
    # ou chose += machin ou une variante

La solution est simple: ne modifiez pas chose. Si vous avez besoin de modifier son contenu, utilisez une variable intermédiaire:

chose = 'truc'
def fonction():
    bidule = chose
    bidule += 'machin' # je sais c'est bidon, c'est pour l'exemple

En Python 3.0, vous pouvez utiliser le mot clé nonlocal pour y palier: vous modifierez alors la variable du scope du dessus.

chose = 'truc'
def fonction():
    nonlocal chose
    chose += 'machin' # je sais c'est bidon, c'est pour l'exemple

Évitez d’utiliser global, qui a un fort potentiel d’effet de bord.

ImportError: cannot import name bidule et ImportError: No module named truc

Une fois que vous avez vérifié qu’un module existe bien avec ce nom (regardez de près, parfois c’est subtile), voici 3 possibilités:

Pas de fichier __init__.py

Un dossier n’est pas un module importable si il ne contient pas de fichier __init__.py. Vérifiez qu’il y en a un, et dans le cas contraire, créez en un vide.

Erreur de Python Path

Quand vous faites import bidule, bidule ne peut être importé que si le dossier qui le contient est dans le Python Path. Le Python Path est une variable qui contient une liste de dossiers dans lesquels chercher les modules à importer.

Le dossier courrant, le dossier contenant la bibliothèque standard de Python et le dossier où sont installés les bibliotèques Python de votre système d’exploitation sont automatiquement présents dans le Python Path.

Première chose: assurez-vous d’être à la racine du projet que vous lancez (erreur typique quand on utilise la commande ./manage.py avec Django par exemple).

Deuxième chose: si vous utilisez une bibliothèque qui n’est pas dans le Python Path (ça arrive assez souvent avec les tests unitaires: on éxécute les tests depuis le dossier de test, et le projet est dans un dossier à côté, donc pas dans le Python Path), vous pouvez ajouter manuellement un chemin dans le Python Path.

Pour se faire, avant l’import qui va foirer:

import sys
sys.path.append('/chemin/vers/le/dossier/parent/du/module/a/importer')

On peut tout à fait spécifier un dossier relativement au dossier courant. Il n’est pas rare d’ajouter le dossier parent du dossier courrant au Python Path:

import sys
import os

DOSSIER_COURRANT = os.path.dirname(os.path.abspath(__file__))
DOSSIER_PARENT = os.path.dirname(DOSSIER_COURRANT)
sys.path.append(DOSSIER_PARENT)

Par exemple, souvent dans le dossier d’un projet Django je fais un sous-dossier ‘apps’, puis je rajoute ceci au fichier settings.py:

import sys
import os

PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(PROJECT_DIR, 'apps'))

Il y a deux avantages à cela:

  • Mes applications sont regroupées dans un dossier et pas en vrac à la racine du projet, mais je peux quand même les importer en faisant import nom et pas import apps.nom.
  • J’ai maintenant une variable PROJECT_DIR que je peux utiliser partout, notamment pour définir où sont certains dossiers comme le dossiers des fichiers statiques:
STATIC = os.path.join(PROJECT_DIR, 'static')

Imports circulaires

Si vous importez poisson.rouge dans force.py, et force.bleu dans poisson.py, vous aurez aussi ce message d’erreur (qui n’aide pas beaucoup, on est d’accord).

Il n’y a pas vraiment de façon élégante de s’en sortir, c’est une des plus grosses couillasses en Python.

Solution 1: vous refactorez votre code pour avoir bleu et rouge dans un fichier couleur.py, lequel est importé dans poisson.py et force.py. C’est propre, mais parfois ça n’a aucun sens, et parfois ce n’est juste pas possible.
Solution 2: vous mettez l’import dans une fonctions ou une méthode dans un des deux modules (n’importe lequel):

def make_bouillabaisse():
    from poisson import rouge

C’est moche, mais c’est facile. Et je le répète, je n’ai jamais vu quelqu’un en 10 ans de Python proposer une solution élégante à ce problème. C’est un What The Fuck d’or.

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

Arf. L’erreur à la con. Parce que généralement elle vient du fait que l’on ne comprend pas vraiment ce qu’on fait. Or difficile de résoudre un problème quand on ne comprend pas de quoi il est question. Ne vous sentez pas mal, on s’est tous retrouvé comme un demeuré devant un problème d’encodage.

A noter que ce n’est pas une erreur spécifique à Python, mais si vous venez d’un langage comme PHP qui passe silencieusement ce genre d’erreur et affiche en prod des texts illisibles, voire une grosse erreur à l’écran peut surprendre.

Voici des causes très fréquentes:

Encodage du fichier.py

Comme il peut y avoir 1 million de possibilités, forcez vous à:

– TOUJOURS avoir votre éditeur de texte réglé pour utiliser UTF-8. Surtout sur Windows. Si votre chef vous l’interdit parce que “ça pose des problèmes d’encodage” (sic), quittez votre job (meilleur choix) ou faites vous former pour comprendre comment marchent les encodages et travailler dans cet environnement hostile.
– TOUJOURS avoir votre encodage (UTF-8 j’ai dis !) déclaré en haut du fichier.py: # -*- coding: utf-8 -*-

Vérifiez que les textes en entrée sont dans l’encodage prévu

Le contenu des bases de données ne sont parfois pas dans l’encodage déclaré de la table ou de la base. Le contenu d’une page HTML n’est parfois pas encodé dans l’encodage déclaré dans le HEAD. Le contenu d’un fichier n’est parfois pas encodé dans l’encodage par défaut de votre OS.

Il n’y a pas de secret. Pas de moyen infaillible de détection automatique. Il faut vérifier.

Vous confondez encodage et décodage (Python 2.7 et moins)

En Python, on DECODE pour passer d’un texte en encodé (UTF8, ISO-8859, CP1552, etc) et donc de type ‘str’ c’est à dire un flux de bits, à un texte unicode, une représentation interne, un objet non imprimable. Il est recommandé de décoder tout texte venant d’une source extérieur à votre programme, pour tout uniformiser.

A l’inverse, on ENCODE pour passer du type ‘unicode’ à un type ‘str’. Il obligatoire d’encoder un texte pour le communiquer au monde extérieur. Si vous ne le faites pas manuellement, Python le fera automatiquement, en essayant de deviner. Il n’est pas excellent à deviner.

En résumé:

In [7]: texte = open('/etc/fstab').read() # ou un téléchargement, ou une requete SQL...
In [8]: type(texte)
Out[8]: str
In [9]: texte = texte.decode('UTF8')
In [10]: type(texte)
Out[10]: unicode
In [11]: print texte # encode automatiquement le texte car votre terminal ne comprend qu'un text encodé
# /etc/fstab: static file system information.
#
[.............]
In [12]: type(texte.encode('UTF8')) # à faire avant de faire un write
Out[12]: str

Si ça continue de foirer, prenez tous les fichiers de votre application un par un: changez toutes les strings en unicode (les précéder d’un “u”), assurez vous que tout ce qui entre est converti en unicode (unicode() après urllib, open, etc) et tout ce qui sort est converti dans un encodage adapté (souvent UTF8) (encode(‘UTF-8’) avant send(), write() ou print).

Si ça ne marche toujours pas, embauchez un mec comme moi qui est payé cher pour se taper la tête contre les murs à la place des autres.

TypeError: ‘int’ object has no attribute ‘__getitem__’ et autres erreurs sur les tuples

Tuples d’un seul élément

CECI N’EST PAS UN TUPLE: (1)

Ceci est un tuple: (1,)

>>> type((1))

>>> type((1,))

>>> t = (1,)
>>> t[0]
1
>>> t = (1)
>>> t[0]
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'int' object has no attribute '__getitem__'

Et il y a plus vicieux:

>>> a = ("12345")
>>> b = ("12345",)
>>> a[0]
'1'
>>> b[0]
'12345'

C’est très dur à débugguer car on dans les deux cas il n’y a pas d’erreur étant donné que c’est une opération tout à fait légitime.

Concaténation automatique

Python vient avec une fonctionnalité qui concatène automatiquement les descriptions littérales de chaînes de caractères:

>>> "Ceci est un"                                  " test"
'Ceci est un test'

C’est très pratique pour les chaînes longues:

>>> print ("Ceci est une chaîne longue "
... "et je peux la diviser sur plusieurs lignes"
... " sans me fouler")
'Ceci est une chaîne longue et je peux la diviser sur plusieurs lignes sans me fouler'

Mais si vous oubliez une virgule dans un tuple (par exemple dans INSTALLED_APPS dans le fichier de settings.py de Django):

>>> REGLES = (
...     "Ne jamais parler du fight club",
...     "Ne jamais croiser les effluves",
...     "Ne jamais appuyer sur le petit bouton rouge" # <===== virgule oubliée !
...     "Ne jamais goûter"
... )
>>> print REGLES[3]
Traceback (most recent call last):
  File "", line 1, in 
IndexError: tuple index out of range
>>> print REGLES[-1]
Ne jamais appuyer sur le petit bouton rougeNe jamais goûter

Le fichier/la liste est vide

On ne peut lire qu’une seule fois les générateurs en Python.

Si vous faites:

toto = (blague.title() for blague in histoire)

ou

toto = open('histoire.txt')

Et ensuite:

for blague in toto:
    print toto

len(list(toto))

La dernière ligne ne marchera pas. Toto aura été vidé par la première boucle for. Si vous souhaitez utiliser plusieurs fois le résultat de votre générateur, il faut le transformer en liste:

toto = list(toto)
for blague in toto:
    print toto

len(list(toto))

Attention, car vous avez maintenant l’intégralité des données chargées en RAM.

TypeError: ma_function() takes exactly x argument (y given)

Cette erreur est très explicite, et la plupart du temps ne pose aucun problème: vérifiez que vous passez le bon nombre d’arguments à la fonction. Faites particulièrement attention si vous utilisez l’opérateur splat.

Il existe néanmoins un cas particulier un peu taquin:

>>> class Americaine(object):
...     def dernier_mot(mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Traceback (most recent call last):
  File "", line 1, in 
TypeError: dernier_mot() takes exactly 1 argument (2 given)

On définie une seul argument (mot) et on en passe un seul ("Monde de merdes !"), alors pourquoi Python n’est pas d’accord ?

C’est parce que l’on déclare une méthode sans self dans la signature. Or Python va passer automatiquement (et de manière invisible) la référence à l’objet courrant en premier argument, du coup la méthode reçoit deux arguments: la référence à homme_le_plus_classe_du_monde et "Monde de merde !". Ca ne marche pas puisque la méthode est déclarée pour n’accepter qu’un seul argument.

Il y a deux solutions. La plus simple, ajoutez self:

>>> class Americaine(object):
...     def dernier_mot(self, mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Monde de merde !

Une seconde solution consiste à déclarer une méthode statique. Du coup on a plus besoin d’instance:

>>> class Americaine(object):
...     @staticmethod
...     def dernier_mot(mot):
...         print mot
... 
>>> Americaine.dernier_mot("Monde de merde !")
Monde de merde !

Ma structure de données par défaut n’est pas la bonne

Piège classique en Python, qu’il est important de répéter encore et encore tant il est la source de frustration chez les personnes qui ne le connaissent pas.

>>> from random import choice
>>> def bioman(forces=['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'], invite=None):
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)
... 
>>> bioman()
'rose'
>>> bioman()
'rouge'
>>> bioman(invite='magenta a pois gris')
'vert'
>>> bioman()
'jaune devant, marron derriere'
>>> bioman() # WTF ??????????
'magenta a pois gris'

Dans le dernier appel ‘magenta a pois gris’ est tiré au sort alors qu’on ne l’a pas passé en paramètre. Comment cela est-il possible ?

Cela vient du fait que les paramètres par défaut sont initialisés une seule fois pour tout le programme: dès que le module est chargé.

Si vous utilisez un objet mutable (liste, set, dico) et que vous le modifiez (ici avec append), le prochain appel de la fonction utilisera toujours la référence de cet objet, et donc de sa versio modifiée.

La solution est soit de ne pas utiliser d’objet mutable (tuple, strings, int, etc), soit de ne pas modifier l’objet:

>>> def bioman(forces=('rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'), invite=None):
...     if invite is not None:
...         forces += (invite,) # ne modifie pas l'ancien objet
...     return choice(forces)

Ou alors (et ceci est souvent utilisé même si c’est moche):

>>> def bioman(forces=None, invite=None):
...     if forces is None:
...        forces = ['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere']
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)

Toutes les parties qui sont éxécutées à l’inialisation du code (en opposition à celles qui le sont à l’appel du code) sont concernées par ce problème: les paramètres par défaut, les variables à la racine des modules, les attributs de classe déclarés en dehors d’une méthode, etc.

ItZ naute a beuhgue, Itse fitiure

Néanmoins cela a aussi son utilité. On peut en effet l’utiliser pour partager des états:

class Model(object):
    _pool = {
        'mysql': MySQL().connect('test'),
        'sqlite': Sqlite.open('test.db')
    }
    default_connection = 'mysql'

    def query(self, connection=default_connection, *params):
        connection.super_query(*params)

Et vous avez maintenant une classe de modèle qui gère plusieurs connections. Si vous l’étendez, les enfants de la classe et toutes les instances partageront le même objet connection, mais tout le reste sera unique à chacun d’eux. Cela évite un effet de bord du singleton qui oblige à partager un état et une identité. Ici on ne partage que la partie de l’état que l’on souhaite, et pas l’identité.

On gagne sur les deux tableaux: si on update la connection MySQL (par exemple parcequ’on a détecté qu’elle était stale), toutes les instances ont accès à l’objet modifé. Mais si on veut overrider la connection pour une seule classe, on peut le faire sans affecter les autres simplement en remplaçant l’objet à la déclaration de la classe.

On peut aussi utiliser cette fonctionnalité pour créer un cache. On appelle ça “mémoiser”:

def fonction_lente(param1, param2, _cache={}):
    # les tuples peuvent être des clés de dico \o/
    key = (param1, param2)
    if key not in _cache:
        _cache[key] = process_lent(param1, param2)
    return _cache[key]

Tous les résultats sont alors stockés en mémoire vive.

]]>
http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/feed/ 23 995