Was ist der kürzeste Weg, um die Anzahl der Elemente in einem Generator / Iterator zu zählen?

73

Wenn ich die Anzahl der Elemente in einem Iterable haben möchte, ohne mich um die Elemente selbst zu kümmern, wie würde ich das pythonisch erreichen? Im Moment würde ich definieren

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

aber ich verstehe, dass lambdaes fast als schädlich angesehen wird und lambda _: 1sicherlich nicht schön ist.

(Der Anwendungsfall hierfür ist das Zählen der Anzahl der Zeilen in einer Textdatei, die einem regulären Ausdruck entsprechen, d grep -c. H. )

Fred Foo
quelle
5
Bitte verwenden Sie diesen Namen nicht _als Variablennamen, da (1) er die Leute verwirrt und sie glauben lässt, dass dies eine spezielle Syntax ist, (2) _im interaktiven Interpreter kollidiert und (3) mit dem allgemeinen gettext-Alias ​​kollidiert .
Sven Marnach
5
@Sven: Ich benutze die _ganze Zeit für nicht verwendete Variablen (eine Gewohnheit aus der Prolog- und Haskell-Programmierung). (1) ist ein Grund, dies überhaupt zu fragen. Ich habe (2) und (3) nicht berücksichtigt, danke, dass Sie darauf hingewiesen haben!
Fred Foo

Antworten:

156

Aufrufe von itertools.imap()in Python 2 oder map()in Python 3 können durch entsprechende Generatorausdrücke ersetzt werden:

sum(1 for dummy in it)

Dies verwendet auch einen Lazy-Generator, sodass keine vollständige Liste aller Iteratorelemente im Speicher erstellt wird.

Sven Marnach
quelle
3
Sie können verwenden len(list(it))- oder wenn die Elemente eindeutig sind len(set(it)), um ein Zeichen zu speichern.
F1Rumors
26
@ F1Rumors Die Verwendung len(list(it))ist in den meisten Fällen in Ordnung. Wenn Sie jedoch einen faulen Iterator haben, der viele, viele Elemente liefert, möchten Sie nicht alle gleichzeitig im Speicher speichern, um sie zu zählen. Dies wird vermieden, wenn Sie den Code in dieser Antwort verwenden.
Sven Marnach
Einverstanden: Als Antwort wurde vorausgesetzt, dass "kürzester Code" wichtiger ist als "niedrigster Speicher".
F1Rumors
2
Wie in diesem Thread vorgeschlagen , wird sum(1 for _ in generator)vermieden, den Speicher zu füllen.
Sylvain
37

Methode, die bedeutend schneller ist als sum(1 for i in it)wenn die Iterable lang sein kann (und nicht bedeutend langsamer, wenn die Iterable kurz ist), während das Verhalten des festen Speicher-Overheads beibehalten wird (im Gegensatz zu len(list(it))), um Swap-Thrashing und Neuzuweisungs-Overhead für größere Eingaben zu vermeiden:

# 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

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

Wie len(list(it))es führt die Schleife in C - Code auf CPython ( deque, countund zipsind in C alle implementiert); Das Vermeiden der Ausführung von Bytecode pro Schleife ist normalerweise der Schlüssel zur Leistung in CPython.

Es ist überraschend schwierig, faire Testfälle für den Leistungsvergleich zu finden ( listCheats, __length_hint__die für Iterables mit beliebiger Eingabe wahrscheinlich nicht verfügbar sind, itertoolsFunktionen, die nicht bereitgestellt werden , verfügen __length_hint__häufig über spezielle Betriebsmodi, die schneller arbeiten, wenn der Wert in jeder Schleife zurückgegeben wird wird freigegeben / freigegeben, bevor der nächste Wert angefordert wird, was dequemit maxlen=0will do). Der Testfall, den ich verwendet habe, bestand darin, eine Generatorfunktion zu erstellen, die eine Eingabe übernimmt und einen Generator auf C-Ebene zurückgibt, dem spezielle itertoolsOptimierungen für Rückgabecontainer fehlten, oder __length_hint__Python 3.3 zu verwenden yield from:

def no_opt_iter(it):
    yield from it

Verwenden Sie dann ipython %timeitMagie (ersetzen Sie 100 durch verschiedene Konstanten):

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

Wenn die Eingabe nicht groß genug ist, len(list(it))um Speicherprobleme zu verursachen, dauert meine Lösung auf einer Linux-Box mit Python 3.5 x64 etwa 50% länger als def ilen(it): return len(list(it))unabhängig von der Eingabelänge.

Für die kleinsten Eingänge bedeuten die Einrichtungskosten für das Aufrufen von deque/ zip/ count/, nextdass es auf diese Weise unendlich länger dauert als def ilen(it): sum(1 for x in it)(ungefähr 200 ns mehr auf meinem Computer für einen Eingang der Länge 0, was einer Steigerung von 33% gegenüber dem einfachen sumAnsatz entspricht), aber für Bei längeren Eingaben dauert es ungefähr die Hälfte der Zeit pro zusätzlichem Element. Bei Eingaben der Länge 5 sind die Kosten gleich, und irgendwo im Bereich der Länge 50-100 ist der anfängliche Overhead im Vergleich zur tatsächlichen Arbeit nicht wahrnehmbar. Der sumAnsatz dauert ungefähr doppelt so lange.

Grundsätzlich sollten Sie diese Lösung verwenden, wenn die Speichernutzung oder Eingaben keine begrenzte Größe haben und Sie mehr Wert auf Geschwindigkeit als auf Kürze legen. Wenn Eingaben begrenzt und klein sind, len(list(it))ist dies wahrscheinlich am besten, und wenn sie unbegrenzt sind, aber Einfachheit / Kürze zählt, würden Sie verwenden sum(1 for x in it).

ShadowRanger
quelle
Dies ist genau die Implementierung in more_itertools.ilen.
Rsalmei
3
@rsalmei: Sieht so aus, als hätten sie meine Implementierung vor acht Monaten übernommen . Technisch gesehen ist es etwas langsamer (weil sie maxlennach Schlüsselwörtern und nicht nach Position übergeben wurden), aber das ist ein fester Overhead, der für die Big-O-Laufzeit nicht von Bedeutung ist. So oder so haben sie mich kopiert (ich habe das vor 3,5 Jahren gepostet), nicht umgekehrt. :-)
ShadowRanger
Schöne Lösung. Als Beobachtung - wenn es „ überraschend schwierig Leistung mit fairen Testfällen kommen für den Vergleich“ , dann vielleicht gibt es keine allgemeine Lösung wert , und es würde die verschiedenen Implementierungen beste Zeit sein (dies, sum(1 ..), len(list())etc. ) auf die besondere Situation.
user650654
9

Ein kurzer Weg ist:

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

Wenn Sie viele Elemente generieren (z. B. Zehntausende oder mehr), kann das Einfügen in eine Liste zu einem Leistungsproblem werden. Dies ist jedoch ein einfacher Ausdruck der Idee, bei der die Leistung in den meisten Fällen keine Rolle spielt.

Greg Hewgill
quelle
1
Ich hatte darüber nachgedacht, aber die Leistung spielt eine Rolle, da ich oft große Textdateien verarbeite.
Fred Foo
8
Solange Ihnen nicht der Speicher ausgeht, ist diese Lösung in Bezug auf die Leistung recht gut, da dies die Schleife in reinem C-Code ausführt - alle Objekte müssen trotzdem generiert werden. Selbst für große Iteratoren ist dies schneller als sum(1 for i in it)solange alles in den Speicher passt.
Sven Marnach
Es ist eigentlich verrückt, das len(it)funktioniert nicht. sum(it), max(it), min(it)Und so weiter wie erwartet, nur len(it)nicht.
Kai Petzke
2
@KaiPetzke: Wenn ites sich um einen Iterator handelt, gibt es keine Garantie dafür, dass er seine eigene Länge kennt, ohne sie zu verlieren . Das offensichtlichste Beispiel sind Dateiobjekte. Sie haben eine Länge, die auf der Anzahl der Zeilen in der Datei basiert. Die Zeilen haben jedoch eine variable Länge. Die einzige Möglichkeit, die Anzahl der Zeilen zu ermitteln, besteht darin, die gesamte Datei zu lesen und die Zeilenumbrüche zu zählen. len()soll eine billige O(1)Operation sein; Möchten Sie, dass Dateien mit mehreren GB stillschweigend gelesen werden, wenn Sie nach ihrer Länge fragen? sum, maxUnd minsind Aggregationsfunktionen , die ihre Daten lesen müssen, lenist es nicht.
ShadowRanger
@ShadowRanger: Möglicherweise können Sie ein O (n) -Aggregat hinzufügen count(it).
Kai Petzke
7

more_itertoolsist eine Bibliothek eines Drittanbieters, die ein ilenTool implementiert . pip install more_itertools

import more_itertools as mit


mit.ilen(x for x in range(10))
# 10
Pylang
quelle
1
Bemerkenswert ist, dass dies im Grunde eine andere Antwort implementiert . (Versteh mich nicht falsch. Ich bin alles dafür, dass ich keinen eigenen Code schreiben muss, deshalb liebe ich diese Antwort, zumal more_itertoolssie viele andere Dinge enthält. Ich möchte sie nur notieren.)
jpmc26
1

Ich mag das Kardinalitätspaket dafür, es ist sehr leicht und versucht, die schnellstmögliche Implementierung zu verwenden, die je nach Iterable verfügbar ist.

Verwendung:

>>> import cardinality
>>> cardinality.count([1, 2, 3])
3
>>> cardinality.count(i for i in range(500))
500
>>> def gen():
...     yield 'hello'
...     yield 'world'
>>> cardinality.count(gen())
2
Erwin Mayer
quelle
1

Dies wären meine Entscheidungen, entweder die eine oder die andere:

print(len([*gen]))
print(len(list(gen)))
Prosti
quelle
1
Die erste Option scheint wenig Sinn zu machen, da sie nur den Aufwand für die Erweiterung des gesamten Generators vor der Konvertierung in a erhöhen würde list. Dies bedeutet, dass diese Antwort gegenüber anderen Antworten keinen Mehrwert bietet, es sei denn, Sie können erklären, warum die erste Option einen Wert hat.
jpmc26
@ jpmc26 fragte das OP nach dem kürzesten Weg, um die Anzahl der Elemente im Generator zu zählen. len([*gen])ist ziemlich kurz. Dies wäre beispielsweise in Code Golf wertvoll. Ich stimme Ihnen jedoch zu, dass diese Lösung in den meisten Anwendungsfällen nicht optimal ist.
Ruancomelli
Eigentlich steht im Titel "der kürzeste Weg", aber der Fragentext ist etwas anders. len([*gen])fühlt sich für mich unpythonisch an.
Ruancomelli