Compter et grouper : encore plus fainéant


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.

6 thoughts on “Compter et grouper : encore plus fainéant

  • DarkNihilius

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

  • boblinux

    @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

  • boblinux

    Eso sí que es buena .

    Artículo sobresaliendo como de costumbre ;)

  • Seb

    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 :

    {
         'sam' : { # tous les "sam"
            5: { # qui ont 5 ans
                'Paris': [ , ], #qui habitent Paris
                'Nantes': [, ]
           },
           7: { #tous les sams qui ont 7 ans
                 'Paris' : [object5],
                'Tokyo': [object6, object7] # qui habitent Tokyo
          }
    }

    J’ai donc créé une classe qui s’utilise de la manière suivante:

     
    # on définit nos clefs de tri
     
    key_name = lambda x: x.name
     
    key_age = lambda x: x.age
     
    key_city = lambda x: x.city
     
    # objects contient tous les objets que je veux ordonner
     
    organized_dico = Organizer(objects, (f1, f2, f3))
     
    # on peut classer selon la vile d'abord si on veut:
     
    organized_dico2 = Organizer(objects, (f3, f1, f2))

    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 :

    from collections import defaultdict
     
    class Organizer(defaultdict):
     
        def __init__(self, iterable=tuple(), sort_keys=tuple()):
            super(Organizer, self).__init__(Organizer)
            self.sort_keys = sort_keys
            self.update(iterable)
     
        def update(self, iterable):
            for item in iterable:
                current_dict = self
                for key in self.sort_keys[:-1]:
                    current_dict = current_dict[key(item)] 
                slot = current_dict[self.sort_keys[-1](item)]
                if slot:
                    slot.append(item)
                else:
                    current_dict[self.sort_keys[-1](item)] = [item]

    Je ne suis pas pleinement satisfait par ma gestion du dernier niveau de récursion… Des idées pour faire un truc plus clean ?

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.