より大きな入力に対するスワップのスラッシングと再割り当てのオーバーヘッドを回避するためにsum(1 for i in it)
( とは異なり) 固定のメモリ オーバーヘッド動作を維持しながら、反復可能オブジェクトが長い場合よりも有意に高速である (反復可能オブジェクトが短い場合でも有意に遅くない)メソッド:len(list(it))
# 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)
len(list(it))
CPython で C コードのループを実行するように ( deque
、すべて C で実装されています) count
; zip
通常、ループごとのバイト コードの実行を回避することが、CPython でのパフォーマンスの鍵となります。
パフォーマンスを比較するための公正なテストケースを考え出すのは驚くほど困難です (任意の入力 iterable では利用できない可能性が高いlist
チートを使用し、提供しない関数には、各ループで値が返されたときに高速に動作する特別な動作モードがあることがよくあります)。次の値が要求される前に解放/解放されます。私が使用したテスト ケースは、Python 3.3+ を使用して、入力を受け取り、特別なリターン コンテナーの最適化またはを欠いた C レベルのジェネレーターを返すジェネレーター関数を作成することでした。__length_hint__
itertools
__length_hint__
deque
maxlen=0
itertools
__length_hint__
yield from
def no_opt_iter(it):
yield from it
次に、ipython
%timeit
魔法を使用します(100をさまざまな定数に置き換えます):
>>> %%timeit fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
入力がメモリの問題を引き起こすほど大きくない場合、 Python 3.9 x64 を実行している Linux ボックスでは、入力の長さに関係なくlen(list(it))
、私のソリューションは よりも約 50% 長くかかります。def ilen(it): return len(list(it))
consumeall
最小の入力の場合、ロード/呼び出しのセットアップ コストは、zip
///よりも無限に時間がかかることを意味しますcount
(長さ 0 の入力の場合、私のマシンでは約 40 ns 長く、単純なアプローチよりも 10% 増加します)。長さ 2 の入力にヒットした場合、コストは同等であり、長さ 30 前後のどこかで、実際の作業と比較して初期オーバーヘッドは目立ちません。このアプローチは、約 50% 長くかかります。next
def ilen(it): sum(1 for _ in it)
sum
sum
基本的に、メモリの使用が重要な場合、または入力のサイズに制限がなく、簡潔さよりも速度を重視する場合は、このソリューションを使用してください。入力が制限されていて小さい場合len(list(it))
はおそらく最適であり、制限がないが単純さ/簡潔さが重要な場合は、 を使用しますsum(1 for _ in it)
。