Sam & Max » set http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Redis : pourquoi et comment ? 23 http://sametmax.com/redis-pourquoi-et-comment/ http://sametmax.com/redis-pourquoi-et-comment/#comments Tue, 14 Oct 2014 11:31:17 +0000 http://sametmax.com/?p=12279 Redis fait partie de ces technologies tellement utiles et simples à mettre en oeuvre qu’il est facile d’oublier toutes les personnes qui ne savent toujours pas ce que c’est. D’autant plus qu’on l’associe avec beaucoup d’étiquettes : cache, queues, pub/sub, base de données, nosql… Et qu’est-ce que Redis comparé à Memcache, MongoDB, MySQL, RabbitMQ, des technos qui n’ont rien à voir et auxquelles on le compare ?

Bref, c’est pas clair tout ça.

Hello redis

D’abord, installer le bouzin.

Si vous êtes sous Linux, Redis est dans votre gestionnaire de paquets. Par exemple, sous Ubuntu :

sudo apt-get install redis-server

Pour Mac, via macport :

sudo port install redis
sudo port load redis

Je crois que brew install redis marche aussi pour les amateurs de homebrew.

Pour Windows, il y a un exe à télécharger ici.

Mais le plus fun avec redis, c’est que même quand il y a pas de binaire, c’est le truc le plus facile du monde à compiler. Et Dieu sait que je hais la compilation, donc quand je vous dis que c’est simple, c’est que c’est mega, ultra, simple.

Ensuite lui faire dire bonjour.

Utiliser Redis se fait à base de commandes, dont la liste est sur le site officiel. On peut envoyer ces commandes depuis n’importe quel langage, mais Redis fournit un shell qui permet de rentrer ces commandes directement:

$ redis-cli # lancer le shell redis
127.0.0.1:6379>  ECHO "Hello"
"Hello"

Redis comme base de données clés/valeurs expirables

L’usage de base de Redis, c’est de stocker des valeurs associées à des clés. Le serveur Redis est comme un gigantesque Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… En premier lieu, donc, on utilise Redis pour stocker des choses ainsi :

"cle1" => valeur1
"cle2" => valeur2
etc

La clé doit être une chaîne de caractères, de préférence ASCII. La valeur peut être n’importe quoi, vraiment, mais généralement c’est un gros bloc de texte, un entier ou un blob binaire.

Ca s’utilise avec les commandes SET et GET, qui peuvent, comme toutes les commandes, être tapées dans le shell de Redis :

127.0.0.1:6379> GET une_cle
(nil)
127.0.0.1:6379> SET une_cle "Une valeur"
OK
127.0.0.1:6379> GET une_cle
"Une valeur"
127.0.0.1:6379> SET une_cle 780708
OK
127.0.0.1:6379> GET une_cle
"780708"

Ce genre d’usage est surtout sollicité pour stocker des paramètres de configurations ou des compteurs.

En effet, la plupart des opérations sur les clés sont atomiques, rendant ce genre d’usage idéal. On peut même incrémenter ou décrémenter une valeur avec INCR et DECR atomiquement :

127.0.0.1:6379> INCR une_cle
(integer) 780709
127.0.0.1:6379> INCR une_cle
(integer) 780710
127.0.0.1:6379> DECR une_cle
(integer) 780709
127.0.0.1:6379>

Vous allez me dire, mais quel intérêt ? Je peux déjà faire ça avec une variable. Certes, mais Redis est accessible depuis n’importe quel programme de votre serveur. Vous pouvez donc facilement partager des valeurs entre vos processus. Par exemple, avec Django, on a souvent plusieurs workers WSGI, et Redis permet donc de partager des informations entre ces workers. Pour cette raison, on peut utiliser Redis pour créer un lock partagé.

Ainsi, un paramètre dynamique peut être changé et lu depuis tous les process de votre projet en utilisant Redis, et la valeur sera garantie d’être à jour. On peut bien entendu faire ça avec une base de données ordinaire, mais Redis a un plus : la performance.

En effet, Redis est par défaut configuré pour garder toutes les données de sa base en mémoire vive. Pour cette raison, il est très, très rapide, supportant 100000 lectures/écritures par seconde. Si vous comptez le nombre de visiteurs en ligne pour l’afficher sur chaque page, votre base de données vous dira merci de plutôt demander à Redis.

Plus encore, les clés peuvent expirer :

127.0.0.1:6379> SET une_autre_cle "Tu ne le sais pas mais tu es deja mort"
OK
127.0.0.1:6379> EXPIRE une_autre_cle 5 # cette clé expire dans 5 secondes
(integer) 1
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
"Tu ne le sais pas mais tu es deja mort"
127.0.0.1:6379> GET une_autre_cle
(nil)
127.0.0.1:6379> GET une_autre_cle
(nil)

La limite en taille de ce qu’on peut stocker par couple est assez large.

On peut également utiliser EXPIREAT et un timestamp unique si on a une date en tête.

Cette caractéristique le rend similaire à Memcache, qui est basé sur des clés/valeurs en mémoire qui peuvent expirer. Et comme Memcache, cela fait de Redis un outil idéal pour gérer du cache : faites une opération longue, stockez-là avec une clé, mettez lui une date d’expiration et pouf, vous avez une valeur cachée globale à votre app.

Redis a néanmoins 3 différences majeures qui le sépare de Memcache :

  • Redis sauvegarde régulièrement toutes les données sur le disque. Si vous rebootez le serveur, vous perdez au pire, quelques secondes de données. Avec Memcache, vous perdez tout.
  • Memcache possède des fonctionalités de clusterisation que n’a pas encore Redis, bien que ce soit actuellement en beta.
  • Redis peut faire bien plus que du stockage clé/valeur.

Redis comme base NoSQL zarbie

Redis n’a pas besoin d’un schéma particulier à définir pour pouvoir sauvegarder ses données, mais n’est pas pour autant limité à une forme d’organisation basique. En fait, des types très proches de ceux qu’on trouve dans le langage Python sont disponibles pour stocker internalement les données, et ils sont décrits dans son excellente doc.

Liste

Une liste est juste une collection ordonnée d’éléments. On utilise toujours la logique de clé, mais au lieu d’une valeur, on a une séquence d’éléments.

Cela permet d’avoir :

cle => [
	valeur1,
	valeur2,
	...
]

Dans le shell :

127.0.0.1:6379> lpush batman na
(integer) 1
127.0.0.1:6379> lpush batman na
(integer) 2
127.0.0.1:6379> lpush batman na
(integer) 3
127.0.0.1:6379> lpush batman na
(integer) 4
127.0.0.1:6379> lpush batman na
(integer) 5
127.0.0.1:6379> lpush batman na
(integer) 6
127.0.0.1:6379> llen batman
(integer) 6
127.0.0.1:6379> LINDEX batman 3
"na"
127.0.0.1:6379> LINDEX batman 10
(nil)
127.0.0.1:6379> LRANGE batman 0 -1 # du début au dernier élément
1) "na"
2) "na"
3) "na"
4) "na"
5) "na"
6) "na"
127.0.0.1:6379> LRANGE batman 2 3
1) "na"
2) "na"
127.0.0.1:6379>

On peut faire toutes les opérations qu’on a l’habitude de faire sur des listes : récupérer un élément, en ajouter un, en retirer un, faire du LIFO, du FIFO, du pipo, etc.

Utile pour créer des files d’attente, des journaux d’évènements (pensez jeux vidéos) ou plus simplement une valeur de config plus complexe qu’une entrée.

Hash

Le Hash se comporte comme un Hash Map, dictionnaire Python, object Javascript, Array associatif en PHP… Bref, comme Redis lui-même en fait, mais sans expiration de clé. Cela permet d’avoir :

cle => {
	cle : valeur,
	cle : valeur,
}

Dans le shell :

127.0.0.1:6379> HSET scores titi 1
(integer) 1
127.0.0.1:6379> HSET scores grosminet 0
(integer) 1
127.0.0.1:6379> HSET scores tom 0
(integer) 1
127.0.0.1:6379> HSET scores jerry 1
(integer) 1
127.0.0.1:6379> HGET scores jerry
"1"
127.0.0.1:6379> HGETALL scores
1) "titi"
2) "1"
3) "grosminet"
4) "0"
5) "tom"
6) "0"
7) "jerry"
8) "1"
127.0.0.1:6379>

Notez encore une fois qu’il n’est pas utile de vérifier que la clé existe avant de rajouter une valeur dans le hash, bien qu’il y ait une commande pour le faire. Si on essaye de récupérer une clé qui n’existe pas, Redis retourne nil.

Le hash est fort pratique pour toute forme de compteurs groupés, ou juste pour mettre en cache des relations. Redis n’ayant pas de JOIN, on utilise parfois un hash pour faire le lien entre deux listes par exemple.

Set

Comme les sets en Python, le set est une collection NON ordonnée d’éléments uniques. Il ne peut pas y avoir de doublons dans un set, et vérifier si un élément fait partie d’un set est une opération très rapide. Ils permettent aussi de faire des opérations ensemblistes de manière performante, par exemple vérifier quels éléments d’un set sont ou non dans un autre set.

cle => { valeur1, valeur2, ...}

Dans le shell :

127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.1 # ajouter 2x le même ne fait rien
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.1
(integer) 0
127.0.0.1:6379> SADD ip 192.168.1.2
(integer) 1
127.0.0.1:6379> SADD ip 192.168.1.3
(integer) 1
127.0.0.1:6379> SCARD ip
(integer) 3
127.0.0.1:6379> SMEMBERS ip
1) "192.168.1.2"
2) "192.168.1.1"
3) "192.168.1.3"
127.0.0.1:6379> SADD ip:banned 192.168.1.3 # le ":" est une séparateur courant pour les clés
(integer) 1
127.0.0.1:6379> SADD ip:banned 192.168.1.10 # ip:banned est un AUTRE set
(integer) 1
127.0.0.1:6379> SINTER ip ip:banned # trouver les IP qui sont dans les 2 sets
1) "192.168.1.3"

On va utiliser les sets pour éviter les doublons (tirage au sort par exemple) ou pour vérifier facilement une appartenance (notion de groupes, de permissions, etc.).

Le set possède une variante, le set ordonné, qui associe à chaque élément du set un score. On peut changer ce score, l’incrémenter ou le décrémenter atomiquement, avoir deux scores égaux… Et au final, récupérer le set dans l’ordre ascendant ou descendant des scores.

cle => { valeur1 (1), valeur2 (3), ...}

Dans le shell :

127.0.0.1:6379> zadd participants 1 staline
(integer) 1
127.0.0.1:6379> zadd participants 1 hitler
(integer) 1
127.0.0.1:6379> zadd participants 2 "pol pot"
(integer) 1
127.0.0.1:6379> zadd participants 1999 "rainbow dash"
(integer) 1
127.0.0.1:6379> zrange participants 0 - 1
(error) ERR value is not an integer or out of range
127.0.0.1:6379> zrange participants 0 -1 
1) "hitler"
2) "staline"
3) "pol pot"
4) "rainbow dash"
127.0.0.1:6379> zrange participants 0 -1 withscores
1) "hitler"
2) "1"
3) "staline"
4) "1"
5) "pol pot"
6) "2"
7) "rainbow dash"
8) "1999"

HyperLogLog

J’ai déjà parlé de l’HyperLogLog ici et celui de Redis fonctionne pareil. Cette structure de données permettra de faire des compteurs d’éléments uniques, comme par exemple un compteur de visiteurs ou de gens connectés, qui soit approximatif (+ ou – 1%) mais prenne une taille fixe en mémoire.

Dans le shell :

127.0.0.1:6379> PFADD connectes toto
(integer) 1
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes toto
(integer) 0
127.0.0.1:6379> PFADD connectes tata
(integer) 1
127.0.0.1:6379> PFADD connectes titi
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 1
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tutu
(integer) 0
127.0.0.1:6379> PFADD connectes tete
(integer) 1
127.0.0.1:6379> PFADD connectes bob
(integer) 1
127.0.0.1:6379> PFcount connectes
(integer) 6
127.0.0.1:6379>

Redis comme super pote de Python

Vu que Redis est simple à installer et à configurer, c’est un outil qu’on dégaine facilement, même pour un petit script, pas forcément pour une grosse app Web. Par exemple, je fais une analyse de mon serveur, plutôt que de stocker le résultat dans un fichier, je peux mettre tout ça dans Redis, c’est tellement pratique.

D’abord, la lib pour communiquer avec Redis est à un pip du clavier :

pip install redis

Ensuite, si vous utilisez le client StrictRedis, l’API est exactement la même que les commandes du shell :

>>> import redis
>>> r = redis.StrictRedis()
>>> r.hgetall('scores')
{b'titi': b'1', b'tom': b'0', b'jerry': b'1', b'grosminet': b'0'}

Il existe aussi des drivers asynchrones pour tornado, twisted et asyncio.

Redis comme message broker

J’aime le pub/sub, je pense qu’après mon enthousiasme pour WAMP.ws, c’est clair.

Redis met à disposition des primitives pour créer des files d’attente de messages et les lire, comme une version simplifiée de RabbitMQ ou Crossbar.io. Pour que ce soit intéressant, il nous faut deux process. D’abord un shell Python qui écoute les messages arrivant sur la file “sametmax” :

>>> import redis
>>> r = redis.StrictRedis()
>>> listener = r.pubsub()
>>> listener.subscribe(['sametmax'])
>>> for item in listener.listen():
...     print(item)
...

Le code va bloquer, et affichera quelque chose à chaque fois qu’il reçoit un message.

Du coup si je fais ceci dans le shell Redis :

127.0.0.1:6379> publish sametmax yolo
(integer) 1
127.0.0.1:6379> publish sametmax carpediem
(integer) 1
127.0.0.1:6379> publish sametmax wololo
(integer) 1
127.0.0.1:6379> publish sametmax2 oyooyo
(integer) 0
127.0.0.1:6379>

Mon shell python va afficher :

{'type': 'subscribe', 'pattern': None, 'channel': b'sametmax', 'data': 1}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'yolo'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'carpediem'}
{'type': 'message', 'pattern': None, 'channel': b'sametmax', 'data': b'wololo'}

Il n’a pas affiché oyooyo puisque c’est sur une autre channel.

C’est une méthode assez simple de faire du pub/sub. C’est bas niveau, il n’y a pas de RPC, il faut boucler à la main et créer soit-même la mécanique pour arrêter d’écouter ou faire du multitask, mais c’est facile pour débuter. Pour cette raison, plein de gens utilisent une solution bricolée là-dessus pour faire du pub/sub.

Et donc

Non seulement Redis est facile à installer, simple à utiliser et performant, mais en plus il est accessible depuis de nombreux langages. Ajoutez à cela ses très nombreuses fonctionnalités, et vous avez là un système qui est installé par défaut sur la plupart de mes serveurs. Par ailleurs, suivez le blog de l’auteur, ce mec est un génie. Il écrit pas souvent, mais quand il écrit, c’est passionant, et humble. C’est beau.

]]>
http://sametmax.com/redis-pourquoi-et-comment/feed/ 23
Le sourire du jour 6 http://sametmax.com/le-sourire-du-jour/ http://sametmax.com/le-sourire-du-jour/#comments Tue, 04 Mar 2014 10:29:16 +0000 http://sametmax.com/?p=9678 Je me baladais dans le code source de path.py, et je suis tombé sur l’auteur essayant de résoudre le problème suivant :

“Soit une chaine ‘mode’ représentant le mode d’ouverture du fichier, détecter si le fichier est demandé à être ouvert en écriture et dans ce cas lever une exception”

L’implémentation naïve serait:

for letter in mode:
    if letter in 'wa+':
        raise ValueError('Only read-only file modes can be used')

Une implémentation un peu plus classe serait :

if any(letter for letter in mode if letter in 'wa+'):
    raise ValueError('Only read-only file modes can be used')

Mais l’auteur a choisit d’utiliser les sets, qui sont exactement fait pour ça. Et la solution est simple et élégante:

if set(mode).intersection('wa+'):
    raise ValueError('Only read-only file modes can be used')

Je sais, il m’en faut pas grand chose pour être heureux.


Edit: explication demandée en comments :)

Un set permet de prendre n’importe quel itérable, et créer un ensemble avec. Il est capable de faire toutes les opérations ensemblistes (union, différence, etc) entre lui-même et un autre itérable.

Dans notre cas, si le mode contient un flag qui implique l’écriture, voici ce que ça donne en détails :

>>> mode = 'wb'
>>> set_de_mode = set(mode)
>>> set_de_mode
set([u'b', u'w'])
>>> set_de_mode.intersection('wa+')
set([u'w'])

On se retrouve donc avec un élément dans le set résultant de l’intersection. Un set non vide a une valeur True dans un contexte booléen en Python, donc la conditionif set(mode).intersection('wa+'): sera remplie.

A l’inverse, si il n’y a pas de flag qui implique l’écriture :

>>> set('r').intersection('wa+')
set([])

On obtient un set vide, qui vaut False dans un contexte booléen.

Tout ceci marche car les chaînes de caractères sont itérables en Python, et que les sets acceptent n’importe quel itérable. Vive le duck typing.

]]>
http://sametmax.com/le-sourire-du-jour/feed/ 6
Ce que vous ne saviez pas sur les collections en Python 16 http://sametmax.com/ce-que-vous-ne-saviez-pas-sur-les-collections-en-python/ http://sametmax.com/ce-que-vous-ne-saviez-pas-sur-les-collections-en-python/#comments Tue, 10 Jul 2012 22:05:52 +0000 http://sametmax.com/?p=1101 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')]
]]>
http://sametmax.com/ce-que-vous-ne-saviez-pas-sur-les-collections-en-python/feed/ 16