103 votes

Quel est le moyen le plus court de compter le nombre d'éléments dans un générateur/itérateur ?

Si je veux le nombre d'éléments dans un itérable sans me soucier des éléments eux-mêmes, quelle serait la manière pythonique d'obtenir cela ? Pour l'instant, je définirais

def ilen(it):
    return sum(itertools.imap(lambda _: 1, it))    # or just map in Python 3

mais je comprends lambda est proche d'être considéré comme nuisible, et lambda _: 1 n'est certainement pas jolie.

(Le cas d'utilisation de ceci est le comptage du nombre de lignes dans un fichier texte correspondant à une regex, c'est-à-dire grep -c .)

8 votes

N'utilisez pas _ comme nom de variable, parce que (1) cela tend à confondre les gens, en leur faisant croire qu'il s'agit d'une sorte de syntaxe spéciale, (2) cela entre en collision avec _ dans l'interpréteur interactif et (3) entre en collision avec l'alias commun gettext.

9 votes

@Sven : J'utilise _ tout le temps pour les variables inutilisées (une habitude de la programmation Prolog et Haskell). (1) est une raison pour laquelle j'ai posé cette question en premier lieu. Je n'avais pas considéré les points (2) et (3), merci de les avoir signalés !

2 votes

198voto

Sven Marnach Points 133943

Appels à itertools.imap() dans Python 2 ou map() dans Python 3 peuvent être remplacées par des expressions génératrices équivalentes :

sum(1 for dummy in it)

Elle utilise également un générateur paresseux, ce qui évite de matérialiser en mémoire une liste complète de tous les éléments de l'itérateur.

4 votes

Vous pouvez utiliser len(list(it)) -- ou si les éléments sont uniques, alors len(set(it)) pour sauvegarder un caractère.

43 votes

@F1Rumors utilisant len(list(it)) est parfait dans la plupart des cas. Cependant, lorsque vous avez un itérateur paresseux qui donne beaucoup d'éléments, vous ne voulez pas les stocker tous en mémoire en même temps juste pour les compter, ce qui est évité en utilisant le code de cette réponse.

0 votes

D'accord : en tant que réponse, il était prévu que le "code le plus court" soit plus important que la "mémoire la plus faible".

50voto

ShadowRanger Points 44

Méthode qui est significativement plus rapide que sum(1 for i in it) lorsque l'itérable peut être long (et n'est pas significativement plus lent lorsque l'itérable est court), tout en maintenant un comportement de surcharge de mémoire fixe (contrairement à len(list(it)) ) afin d'éviter les surcharges de swap et de réallocation pour les entrées plus importantes :

# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip

from collections import deque
from itertools import count

# Avoid constructing a deque each time, reduces fixed overhead enough
# that this beats the sum solution for all but length 0-1 inputs
consumeall = deque(maxlen=0).extend

def ilen(it):
    # Make a stateful counting iterator
    cnt = count()
    # zip it with the input iterator, then drain until input exhausted at C level
    consumeall(zip(it, cnt)) # cnt must be second zip arg to avoid advancing too far
    # Since count 0 based, the next value is the count
    return next(cnt)

Comme len(list(it)) il exécute la boucle en code C sur CPython ( deque , count y zip sont tous implémentés en C) ; éviter l'exécution de code d'octet par boucle est généralement la clé de la performance en CPython.

Il est étonnamment difficile de trouver des cas de test équitables pour comparer les performances ( list tricheurs utilisant __length_hint__ qui ne sera probablement pas disponible pour des itérables d'entrée arbitraires, itertools les fonctions qui ne fournissent pas __length_hint__ ont souvent des modes de fonctionnement spéciaux qui fonctionnent plus rapidement lorsque la valeur renvoyée sur chaque boucle est libérée/libre avant que la valeur suivante ne soit demandée, ce qui deque con maxlen=0 fera l'affaire). Le cas de test que j'ai utilisé consistait à créer une fonction de générateur qui prendrait une entrée et renverrait un générateur de niveau C dépourvu d'éléments spéciaux. itertools retourner les optimisations du conteneur ou __length_hint__ en utilisant Python 3.3+. yield from :

def no_opt_iter(it):
    yield from it

Ensuite, en utilisant ipython %timeit magique (en remplaçant 100 par des constantes différentes) :

>>> %%timeit fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))

Lorsque l'entrée n'est pas assez grande pour que len(list(it)) causerait des problèmes de mémoire, sur une machine Linux exécutant Python 3.9 x64, ma solution prend environ 50% plus de temps que def ilen(it): return len(list(it)) indépendamment de la longueur de l'entrée.

Pour les plus petites entrées, les coûts de mise en place pour charger/appeler consumeall / zip / count / next signifie que cela prend infiniment plus de temps de cette façon que def ilen(it): sum(1 for _ in it) (environ 40 ns de plus sur ma machine pour une entrée de longueur 0, une augmentation de 10% par rapport à la simple sum ), mais au moment où vous atteignez les entrées de longueur 2, le coût est équivalent, et quelque part autour de la longueur 30, les frais généraux initiaux sont imperceptibles par rapport au travail réel ; l'approche de la sum prend environ 50 % de plus.

En gros, si l'utilisation de la mémoire est importante ou si les entrées n'ont pas une taille limitée et que la vitesse vous importe plus que la brièveté, utilisez cette solution. Si les entrées sont délimitées et de petite taille, len(list(it)) est probablement la meilleure solution, et s'ils ne sont pas délimités, mais que la simplicité et la brièveté comptent, vous utiliserez sum(1 for _ in it) .

9voto

Greg Hewgill Points 356191

Le chemin le plus court est :

def ilen(it):
    return len(list(it))

Notez que si vous générez un lot d'éléments (disons des dizaines de milliers ou plus), les placer dans une liste peut devenir un problème de performance. Cependant, il s'agit d'une expression simple de l'idée où les performances n'ont pas d'importance dans la plupart des cas.

1 votes

J'y avais pensé, mais les performances sont importantes car je traite souvent de gros fichiers texte.

9 votes

Tant que vous n'êtes pas à court de mémoire, cette solution est en fait assez performante, puisque la boucle est réalisée en code C pur - tous les objets doivent être générés de toute façon. Même pour les gros itérateurs, cette solution est plus rapide que sum(1 for i in it) tant que tout rentre dans la mémoire.

1 votes

C'est vraiment fou, que len(it) ne fonctionne pas. sum(it) , max(it) , min(it) et ainsi de suite fonctionnent comme prévu, seul len(it) ne le fait pas.

7voto

pylang Points 12013

more_itertools est une bibliothèque tierce partie qui implémente une ilen outil. pip install more_itertools

import more_itertools as mit

mit.ilen(x for x in range(10))
# 10

1 votes

Il est intéressant de noter que cela implémente essentiellement une autre réponse . (Ne vous méprenez pas. Je suis tout à fait favorable à l'idée de ne pas avoir à écrire mon propre code, donc j'adore cette réponse, d'autant plus que more_itertools a beaucoup d'autres choses. Je veux juste en prendre note).

5voto

Nikhil CSB Points 121
len(list(it))

Bien qu'il puisse se bloquer si c'est un générateur infini.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X