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)
.
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
Dupliqué : stackoverflow.com/questions/390852/
0 votes
python 3.x
s'il existe des éléments répétés et que vous souhaitez également vérifier le nombre d'éléments pour chaque élément, utilisezCounter(generator/iterator)
, par exemple..,c = Counter(iter('goodbadugly'))
puis comptez le total :sum(c.values())
1 votes
@SvenMarnach : Utilisation
_
à l'intérieur d'une fonction, en particulier à l'intérieur d'un genexpr, n'entrera pas en collision avec l'interpréteur interactif (dans Py2, l'utiliser à l'intérieur d'un listcomp à portée globale serait de l'utilisation de l'interpréteur interactif de_
mais cela a été corrigé dans Py3, où les listcomps s'exécutent dans un scope séparé). Si votre fonction utilise également l'alias gettext, alors oui, c'est un problème, mais sinon, dans un code d'interpréteur non interactif,_
est une manière acceptée de dire "je ne me soucie pas de la valeur ici", au point que les linters qui vérifient les noms non lus assignés l'accepteront spécifiquement.0 votes
Mon principal argument contre est le premier : les gens toujours pensent que le trait de soulignement a une signification particulière, qu'il jette le résultat au lieu de le conserver, mais ce n'est pas le cas - c'est juste un nom de variable ordinaire. Et si j'ai le choix entre écrire du code que tout le monde comprend immédiatement, et du code sur lequel certaines personnes ont des idées fausses, toutes choses égales par ailleurs, je choisirai le premier. Cependant, j'ai en quelque sorte abandonné ce combat particulier - c'est devenu trop commun.