Du code, du communisme

Ce que vous ne saviez pas sur les collections en Python

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êmement rapide (complexité 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 fonctionnalité 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 optionnel
[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 à reculons.

>>> 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]

Ça 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 "", line 1, in 
    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']
>>> score['robert'] += 1
>>> score['robert']
1
>>> score['robert'] += 1
>>> score['robert']
2

Comme vous pouvez le voir il gère les valeurs par défaut, 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')]