Ce que vous ne saviez pas sur les collections en Python 13


Les collections en Python sont organisées autour de la philosophie du langage, notament EAFP, et la manie de l’itération.

Les dictionnaires

Valeur par défaut

Une fois à l’aise en Python, on utilise souvent les dictionnaires. Et on fait souvent ça:

>>> def get(d, key, default):
...     try:
...         return d[key]
...     except KeyError:
...         return default
... 
>>> d = {'a':1}
>>> get(d, 'foo', 'bar')
'bar'
>>> get(d, 'a', 'bar')
1

C’est parfaitement superflux, puisque Python le propose en standard:

>>> d.get("foo", 'bar')
'bar'
>>> d.get("a", 'bar')
1

Plus tordu encore:

>>> def get_and_set_if_not_exist(d, key, default):
...     try:
...         return d[key]
...     except KeyError:
...         d[key] = default
...         return default
... 
>>> d = {'a':1}
>>> get_and_set_if_not_exist(d, 'foo', []).append('wololo')
>>> d
{'a': 1, 'foo': ['wololo']}
>>> get_and_set_if_not_exist(d, 'foo', []).append('oyo oyo')
>>> d
{'a': 1, 'foo': ['wololo', 'oyo oyo']}

Python le propose aussi en standard:

>>> d = {'a':1}
>>> d.setdefault('foo', []).append('wololo')
>>> d.setdefault('foo', []).append('oyo oyo')
>>> d
{'a': 1, 'foo': ['wololo', 'oyo oyo']}

Clés des dictionnaires

Les clés des dictionnaires n’ont pas à être des strings. N’importe quel objet hashable fait l’affaire, par exemple, des tuples:

>>> positions = {}
>>> positions[(48.856614, 48.856614)] = "Paris"
>>> positions[(40.7143528, -74.0059731)] = "New York"
>>> positions
{(48.856614, 48.856614): 'Paris', (40.7143528, -74.0059731): 'New York'}
>>> positions[(48.856614, 48.856614)]
'Paris'

Les sets

Les sets sont un type de structure peu connu: ils représentent un ensemble non ordonné d’objets uniques. Il n’y a donc pas d’ordre évident dans un set, et le résultat est garanti sans doublon:

>>> e = set((3, 2, 1, 1, 1, 1, 1))
>>> e
set([1, 2, 3])
>>> e.add(1)
>>> e.add(1)
>>> e.add(14)
>>> e
set([1, 2, 3, 14])

Les opérations du set acceptent n’importe quel itérable. Y compris les opérations ensemblistes:

>>> e.update('abcdef')
>>> e
set(['a', 1, 2, 3, 'e', 'd', 'f', 'c', 14, 'b'])
>>> e = set('abc')
>>> e.union("cde")
set(['a', 'c', 'b', 'e', 'd'])
>>> e.difference("cde")
set(['a', 'b'])
>>> e.intersection("cde")
set(['c'])

Vérifier la présence l’un élément dans un set (avec l’opérateur in) est une opération extrêment rapide (compléxité O(1)), beaucoup plus que dans une liste ou un tuple. Le set reste pourtant itérable (mais on ne peut pas compter sur l’ordre).

Les opérateurs binaires sont overridés pour les opérations entre sets. De plus on peut utiliser une notation littérale pour décrire un set à partir de Python 2.7:

>>> {'a', 'b', 'c'} | {'c', 'd'} # union
set(['a', 'c', 'b', 'd'])
>>> {'a', 'b', 'c'} & {'c', 'd'} # intersection
set(['c'])
>>> {'a', 'b', 'c'} - {'c', 'd'} # difference
set(['a', 'b'])

Les listes

Pop() prend un argument

La raison pour laquelle il n’y a pas de unshift sur les listes en Python, c’est que l’on en a pas besoin:

>>
>>> l = [1, 2, 3, 4, 5]
>>> l.pop()
5
>>> l
[1, 2, 3, 4]
>>> l.pop(0)
1
>>> l
[2, 3, 4]
>>> l.pop(-2)
3
>>> l
[2, 4]

Le slicing accepte un 3eme argument

Le slicing, que l’on peut appliquer à tous les indexables (listes, tuples, strings, etc), est la fonctionalité bien pratique qui permet de récupérer une sous partie de la structure de données:

>>> l = range(10)
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:8]
[2, 3, 4, 5, 6, 7]
>>> l[5:]
[5, 6, 7, 8, 9]
>>> l[:5]
[0, 1, 2, 3, 4]

Ca vous connaissiez sûrement. Mais cette syntaxe accepte un 3eme nombre: le pas.

Le premier nombre dit d’où l’on part. Le second où l’on s’arrête. Le dernier dit de combien on avance (par défaut de 1).

>>> l[2:8:2]
[2, 4, 6]
>>> l[2::2] # chaque paramètre est optionel
[2, 4, 6, 8]

Et le pas peut être négatif, ce qui est plutôt sympas si vous voulez parcourir une liste ou une string à reculon.

>>> l[::-1]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

extend() accepte n’importe quel itérable

extend() permet de mettre à jour une liste. On l’utilise souvent en lui passant une autre liste:

>>> l = [1, 2, 3]
>>> l.extend([4, 5, 6])
>>> l
[1, 2, 3, 4, 5, 6]

Mais comme la plupart du code la bibliothèque standard, extend() accepte n’importe quel itérable.

>>> t = (42, 666, 1024) # un tuple
>>> s = '456' # une string
>>> d = {'3.14': 'pi'} # un dico
>>> l = [1, 2, 3]
>>> l.extend(s)
>>> l
[1, 2, 3, '4', '5', '6']
>>> l.extend(d) #
>>> l
[1, 2, 3, '4', '5', '6', '3.14']
>>> l.extend(t)
>>> l
[1, 2, 3, '4', '5', '6', '3.14', 42, 666, 1024]

Ca marche aussi avec les set, les fichiers, les expressions génératrices. Attention cependant, sachez que l’itération retourne: par exemple itérer sur un dico retourne ses clés, pas ses valeurs (car on peut récupérer l’un avec l’autre, mais pas l’inverse).

Les tuples

Ce qui permet de créer un tuple ne sont pas les parenthèses, mais la virgule:

>>> 1,2,3 # ceci EST un tuple
(1, 2, 3)
>>> 1, # tuple
(1,)
>>> 1 # int
1

La raison pour laquelle il est recommandé d’utiliser presque TOUJOURS les parenthèses, c’est qu’elles permettent d’éviter les ambiguïtés, et qu’elles autorisent la définition sur plusieurs lignes:

>>> type(1,2,3) # tuple ou paramètres ?
Traceback (most recent call last):
  File "<ipython-input-62-5be61417b8a3>", line 1, in <module>
    type(1,2,3)
TypeError: type() argument 1 must be string, not int
>>> type((1,2,3))
<type 'tuple'>
>>> (1, # un gros tuple s'écrit sur plusieurs lignes
... 2,
... 3)
(1, 2, 3)

Mais il existe des rares cas où il est acceptable de ne pas mettre de parenthèses:

>>> def debut_et_fin(lst):
...     """
...         Retourne le début et la fin d'une liste
...     """
...     debut = lst[0]
...     fin = lst[-1]
...     # donner l'illusion de retourner plusieurs valeurs
...     # alors qu'on retourne en fait un tuple
...     return debut, fin # 
... 
>>> debut, fin = debut_et_fin([1,2,3,4]) # unpacking
>>> debut
1
>>> fin
4
>>> debut, fin = fin, debut # variable swap
>>> debut
4
>>> fin
1

Le module collections

En plus des collections built-in, la bibliothèque standard de Python propose un module collections avec plein d’outils en bonus.

Des dictionnaires qui conservent l’ordre d’insertion (comme les Arrays en PHP):

>>> from collections import OrderedDict
>>> d = {} # l'ordre d'un dico n'est pas garanti
>>> d['c'] = 1
>>> d['b'] = 2
>>> d['a'] = 3
>>> d.keys()
['a', 'c', 'b']
>>> d = OrderedDict()
>>> d['c'] = 1
>>> d['b'] = 2
>>> d['a'] = 3
>>> d.keys()
['c', 'b', 'a']

Un compteur qui a une interface similaire à un dictionnaire spécialisé.

>>> from collections import Counter
>>> score = Counter()
>>> score['bob']
0
>>> score['robert'] += 1
>>> score['robert']
1
>>> score['robert'] += 1
>>> score['robert']
2

Comme vous pouvez le voir il gère les valeurs par defaut, mais en prime il compte le contenu de n’importe quel itérable:

>>> Counter([1, 1, 1, 1, 1, 1, 2, 3, 3])
Counter({1: 6, 3: 2, 2: 1})
>>> Counter('Une petite puce pique plus')
Counter({'e': 5, ' ': 4, 'p': 4, 'u': 3, 'i': 2, 't': 2, 'c': 1, 'l': 1, 'n': 1, 'q': 1, 's': 1, 'U': 1})

Des tuples qui ressemblent à des structs en C, mais itérables:

>>> from collections import namedtuple
>>> Fiche = namedtuple("Fiche", "force charisme intelligence")
>>> f = Fiche(force=18, charisme=17, intelligence=3)
>>> f
Fiche(force=18, charisme=17, intelligence=3)
>>> for x in f:
...     print x
...
18
17
3
>>> f.force
18

Des dicos dont la valeur par défaut est le résultat de l’appel d’une fonction:

>>> from collections import defaultdict
>>> import datetime
>>> d = defaultdict(datetime.datetime.now)
>>> d["jour"]
datetime.datetime(2012, 7, 10, 17, 34, 7, 265222)
>>> d["jour"] # la valeur est settée
datetime.datetime(2012, 7, 10, 17, 34, 7, 265222)
>>> d["raison"] = "test"
>>> d.items()
[("jour", datetime.datetime(2012, 7, 10, 17, 34, 7, 265222)), ("raison", 'test')]

Leave a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

13 thoughts on “Ce que vous ne saviez pas sur les collections en Python

  • Recher

    Le deuxième bloc de code à propos des ‘set’, ça donne pas vraiment ça.
    Il manquerait pas un petit e = set('abc') quelque part ? Histoire de réinitialiser tout le bazar effectué lors des précédents exemples.

    Ca nous y ferait quelque chose comme ceci :

    >>> e.update('abcdef')
    >>> e
    set(['a', 1, 2, 3, 'e', 'd', 'f', 'c', 14, 'b'])
    >>> e = set('abc')
    >>> e.union("cde")
    set(['a', 'c', 'b', 'e', 'd'])
    >>> e.difference("cde")
    set(['a', 'b'])
    >>> e.intersection("cde")
    set(['c'])

    Pendant que j’y suis, si on pouvait prévisualiser les commentaires qu’on poste sur votre superbe blog, ça mettrait du beurre dans le cul de la cremière. Parce que là, je suis jamais sûr de ce que je vous bave.

  • Sam Post author

    Doublement oui mon cher Recher.

    Oui, j’ai merdé au copier/coller dans le deuxième block de code de set. C’est corrigé, merci.

    Et oui, les commentaires ont cruellement besoin de previsualisation, si possible en temps réel. Je vais voir ce que je peux faire, mais j’ai un poil dans la main de la taille du penis de Max.

  • Max

    TOUJOURS VERIFIER avant de mettre en prod!
    C’est la base j’ai envie de dire ^^

  • Tony

    Top moumoutte l’article ! (encore une fois)

    Certains éléments, comme le get et get_and_set des dico, sont decrits dans le “Code like a pythonista” que tout pratiquant devrait vénérer.

    Merci pour la découverte de la lib collections, et le 3eme arg du slice de list, et surtout l’usage du [::-1]

  • Sam Post author

    @Luigi: tout ce qu’il y a sur le blog vise uniquement Python 2.7. En effet malgré le temps qui passe, Mac est toujours sous Python 2.6, Ubuntu sous 2.7, et la plupart des serveurs Web sous 2.6 (avec Max on a encore des vieilles cent os avec la 2.4 dans les dépots, obligés de compiler la 2.6 à la mano). Parler de la V3 n’est pas un bon investissement de temps: ceux qui l’utilisent savent généralement ce qu’ils font car il faut vraiment avoir un use case très précis vu qu’on peut encore rien faire avec (la plupart des bonnes libs ne sont pas encore portées).

    @Xavier: faudrait faire un tableau pour que chacun coche “ça je le sais”, “ça je le savais pas” :-p

  • Etienne

    Sympa tout ça!

    Intéressant aussi le “Code like a pythonista”. C’est le genre de trucs bien utiles pour un autodidacte.

    Merci les gars.

  • Lujeni

    Super Article ! Merci pour l’info sur le module collections qui va remplacer mes compteurs dico fait à la main :)

  • Dam's

    Et le piège à con :


    >>> set('123') == {'123'}
    False
    >>> set('123')
    set(['1', '3', '2'])
    >>> {'123'}
    set(['123'])

    Bug or feature ?

  • kontre

    C’est vicieux mais ça a une certaine logique: la fonction set (et list tout pareil) prend un itérable en entrée et construit un objet set à partir de ses éléments.
    C’est un bug si tu travailles avec des mots et une feature si tu travailles avec des lettres !

    Ce genre de trucs est gênant pour les arguments d’entrée, quand tu peux accepter soit une string soit une liste de string, il faut utiliser isinstance(). Et si on veut rester compatible ptyon2 et python3, isinstance avec des strings est bien chiant.
    C’est à tel point qu’il y a des propositions sur la liste de développement python pour rentre les strings non itérables.

  • Sam Post author

    @Dam’s:

    Feature.

    La fonction set() et la notation litérale {} ne font pas la même chose, et n’ont pas le même usage.

    Ce n’est pas un piège. Dans les tutos, pour des raisons de simplification, les gens montres les deux comme étant pareil, mais c’est faux.

    On va utiliser le litéral pour construire statiquement, à la main, un set. On va utiliser la fonction pour crée dynamiquement un set.


    Set()
    accepte n’importe quel itérable (une string, une liste, un tuple, un fichier, un générateur, etc, ce qui te permet de construire un tuple à partir d’une séquence ou d’une coroutine sans avoir à faire une boucle for :

    >>> set(range(10))
    set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    >>> set((x + x for x in 'azerty'))
    set(['aa', 'rr', 'ee', 'tt', 'yy', 'zz'])
    >>> set(open('/etc/fstab'))
    set(['#\n', 'UUID=4c0455fb-ff57-466a-8d1f-22b575129f4f none            swap    sw              0       0\n', '...'])

    Si je voulais faire pareil avec le litéral, je devrais faire une boucle for à chaque fois. Or en Python, on utilise beaucoup l’itération, donc le langage fournit des raccourcis partout la facilité.

    Règle donc : utiliser les littéraux quand on veut écrire vite, utiliser les fonctions quand on veut générer facilement.