Après avoir bien galéré à créer un compteur à la main avec un dico, vous avez découvert les joies des méthodes dict.get
et dict.setdefault
. Puis évidemment quelqu’un vous a pointé vers collections.defaultdict
, et enfin, vous avez fini par découvrir collections.Counter
. Joie.
Le parcours est à peu près toujours le même quand on veut grouper ou compter des valeurs en Python.
Malgré cela, je vois encore des gens qui sous utilisent ces collections. Par exemple, Counter
peut compter automatiquement :
>>> from collections import Counter >>> Counter('jfsqmfjdklmqfjsdqklmfjdsqhfdqsjkhfdshjkl') Counter({'j': 6, 'f': 6, 'q': 5, 's': 5, 'd': 5, 'k': 4, 'l': 3, 'm': 3, 'h': 3}) |
Mais ce que ne réalisent pas beaucoup de développeurs, c’est que cet objet accepte n’importe quel itérable en paramètre. Nous sommes en Python, et rededjiou, je me tue à répéter que l’itération est la philosophie centrale du langage.
Donc le compteur peut prendre une expression génératrice en paramètre.
Par exemple, si vous voulez compter un truc un peu plus complexe que des éléments, comme mettons, le ratio de lignes commentées dans un fichier, vous n’avez pas besoin de faire ça :
count = Counter() for line in open('/etc/fstab', encoding='ascii'): count[line.startswith('#')] += 1 # out : Counter({True: 10, False: 3}) |
Ceci marchera parfaitement :
count = Counter(line.startswith('#') for line in open('/etc/fstab', encoding='ascii')) # out : Counter({True: 10, False: 3}) |
Vous pouvez également utiliser des générateurs plus complexes. Combien de fichiers par types d’extensions ?
import os import pathlib def get_extensions(path): for dirpath, dirnames, files in os.walk(path): for name in files: ext = pathlib.Path(name).suffix if ext: # on ignore les fichiers sans extension yield ext Counter(get_extensions('/etc')).most_common(9) # Out : # ('.conf', 632), # ('.0', 348), # ('.gz', 323), # ('.jhansonxi', 207), # ('.pem', 177), # ('.load', 127), # ('.ttb', 86), # ('.ktb', 80), # ('.kti', 55)] |
Notez que le Counter
peut faire plus que compter. Ici il nous donne les 9 plus grandes valeurs du classement, mais en prime, il peut aussi nous faire des opérations ensemblistes :
>>> c = Counter("aabbbbbbbbbbbbcccc") >>> c & Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs min Counter({'b': 2, 'a': 2, 'c': 1}) >>> c | Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs max Counter({'a': 15, 'b': 12, 'd': 7, 'c': 4}) |
Le compteur fournit par Python est donc naturellement très, très puissant.
Une autre chose qui est rarement faite : sous-classer ces types.
Par exemple, si vous avez souvent des opérations où il faut grouper des valeurs :
from collections import defaultdict class Grouper(defaultdict): def __init__(self, iterable): super(Grouper, self).__init__(list) self.update(iterable) def update(self, iterable): try: iterable = iterable.items() except AttributeError: iterable = iterable for k, v in iterable: self[k].append(v) |
On prend un default dict, on lui dit qu’un update ajoute les éléments à la liste en valeur plutôt que de la remplacer, et zou, vous avez un dictionnaire qui va grouper toutes les valeurs automatiquement.
Liste des fichiers par extensions ? Fastoche !
def get_extensions(path): for dirpath, dirnames, files in os.walk(path): for name in files: ext = pathlib.Path(name).suffix if ext: yield ext, name # on rajoute le name ici >>>files = Grouper(get_extensions('/etc')) >>> files['.tti'] ['en-na-ascii.tti', 'numbers-french.tti', 'devanagari.tti', 'letters-cyrillic.tti', 'punctuation-basic.tti', 'malayalam.tti', 'ascii-basic.tti', 'spaces.tti', 'letters-latin.tti', 'letters-latin-dot8.tti', 'en-chess.tti', 'numbers-dot8.tti', 'punctuation-tibetan.tti', 'boxes.tti', 'gujarati.tti', 'numbers-nemeth.tti', 'punctuation-alternate.tti', 'common.tti', 'blocks.tti', 'gurmukhi.tti', 'kannada.tti', 'telugu.tti', 'tamil.tti', 'numbers-dot6.tti', 'de-chess.tti', 'control-latin.tti', 'letters-tibetan.tti', 'oriya.tti', 'bengali.tti'] |
Bref, compter et grouper sont des opérations si communes : ne vous faites par chier à refaire tout ça à la main.
>>> c = Counter("aabbbbbbbbbbbbcccc")
>>> c & Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs min
Counter({'b': 2, 'a': 2, 'c': 1})
>>> c | Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs max
Counter({'a': 15, 'b': 12, 'd': 7, 'c': 4})
UN petit soucis d’encodage non ? (et une petite typo en prime)
Si senor.
@Darkxxx
Me has mordido!
Acabo de notar al leer el artículo, al parecer, el Tío @Sam aún no ha actualizado .
PS : Bueno, yo entiendo su reactividad , es 02 a.m. a Francia
He actualizado el artículo. Hay algo que no me visto ?
Eso sí que es buena .
Artículo sobresaliendo como de costumbre ;)
Complément (intéressant ?) à l’article (il s’agit d’une sorte de généralisation du Grouper de l’article).
J’avais besoin de grouper les éléments d’une liste selon certains critères. Par exemple, mettons que j’ai une liste d’objets qui ont comme attributs “name”, “age”, et “city” (plus d’autres trucs) ; j’ai envie de récupérer un dico qui soit “trié” selon ces trois critères ; par exemple pour avoir :
J’ai donc créé une classe qui s’utilise de la manière suivante:
Et voici le code de la classe, en gros il s’agit d’un defaultdict “récursif” dont tous les niveaux sont eux même des defaultdict et le dernier est une liste. À chaque ajout d’un élément, on évalue successivement les différents sort_keys pour savoir où placer cet élément dans la structure :
Je ne suis pas pleinement satisfait par ma gestion du dernier niveau de récursion… Des idées pour faire un truc plus clean ?