web-dev-qa-db-ja.com

Python 'for'ループのより良い方法

Python=で特定の回数ステートメントを実行する一般的な方法は、forループを使用することです。

これを行う一般的な方法は、

# I am assuming iterated list is redundant.
# Just the number of execution matters.
for _ in range(count):
    pass

上記のコードが一般的な実装であると主張する人はいないと思いますが、別の選択肢があります。 Pythonの速度を使用して、参照を乗算してリストを作成します。

# Uncommon way.
for _ in [0] * count:
    pass

古いwhile方法もあります。

i = 0
while i < count:
    i += 1

これらのアプローチの実行時間をテストしました。これがコードです。

import timeit

repeat = 10
total = 10

setup = """
count = 100000
"""

test1 = """
for _ in range(count):
    pass
"""

test2 = """
for _ in [0] * count:
    pass
"""

test3 = """
i = 0
while i < count:
    i += 1
"""

print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))

# Results
0.02238852552017738
0.011760978361696095
0.06971727824807639

わずかな違いがあった場合、私は主題を開始しませんが、速度の違いは100%であることがわかります。 2番目の方法がより効率的であれば、なぜPythonはそのような使用を奨励しませんか?より良い方法はありますか?

テストはWindows 1およびPython 3.6で行われます。

@Tim Petersの提案に従って、

.
.
.
test4 = """
for _ in itertools.repeat(None, count):
    pass
"""
print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test4, setup=setup).repeat(repeat, total)))

# Gives
0.02306803115612352
0.013021619340942758
0.06400113461638746
0.008105080015739174

これははるかに良い方法を提供し、これは私の質問にほぼ答えます。

両方がジェネレーターであるため、これがrangeよりも高速なのはなぜですか。値が変わらないからでしょうか?

65
Max Paython

を使用して

for _ in itertools.repeat(None, count)
    do something

は、すべての世界で最高のものを得るための非自明な方法です。小さな一定のスペース要件、および反復ごとに作成される新しいオブジェクトはありません。内部では、repeatのCコードはネイティブC整数型(Python整数オブジェクトではありません!)を使用して、残りのカウントを追跡します。

そのため、カウントはプラットフォームC ssize_tタイプ。通常は最大2**31 - 1は32ビットボックスで、ここは64ビットボックスで:

>>> itertools.repeat(None, 2**63)
Traceback (most recent call last):
    ...
OverflowError: Python int too large to convert to C ssize_t

>>> itertools.repeat(None, 2**63-1)
repeat(None, 9223372036854775807)

これは私のループにとって十分に大きなものです;-)

91
Tim Peters

最初の方法(Python 3)で)は、値の範囲を反復処理できる範囲オブジェクトを作成します(ジェネレーターオブジェクトに似ていますが、何度も反復処理できます)。値の範囲全体ではなく、現在の値と最大値のみが含まれ、最大値に達するか通過するまでステップサイズ(デフォルトは1)ずつ増加するため、多くのメモリを消費します。

range(0, 1000)のサイズをlist(range(0, 1000))のサイズと比較します: Try It Online! 。前者はメモリ効率が非常に高いです。サイズに関係なく48バイトしかかかりませんが、リスト全体はサイズの点で直線的に増加します。

2番目の方法は、高速ですが、私が過去に話したメモリを占有します。 (また、_0_は24バイトを占め、Noneは16バイトを占めるが、それぞれの_10000_の配列は同じサイズを持っているようだ。興味深い。おそらくポインターであるため)

興味深いことに、_[0] * 10000_はlist(range(10000))よりも約10000小さくなります。これは、最初のものではすべてが同じプリミティブ値であるため、最適化できるからです。

3番目のものは、別のスタック値を必要としないため、ニースでもあります(rangeを呼び出すには、呼び出しスタック上の別のスポットが必要です)。

itertoolsがそのようにクールだからといって、最後のものが最速かもしれません:P正しく覚えていれば、Cライブラリの最適化を使用すると思います。

11
HyperNeutrino

この答えは、便宜上ループ構成を提供します。 itertools.repeatを使用したループに関する追加の背景については、Tim Petersの回答 上記 、Alex Martelliの回答 here およびRaymond Hettingerの回答 here を参照してください。

# loop.py

"""
Faster for-looping in CPython for cases where intermediate integers
from `range(x)` are not needed.

Example Usage:
--------------

from loop import loop

for _ in loop(10000):
    do_something()

# or:

results = [calc_value() for _ in loop(10000)]
"""

from itertools import repeat
from functools import partial

loop = partial(repeat, None)
0
Darkonaut

最初の2つの方法では各反復にメモリブロックを割り当てる必要がありますが、3番目の方法では各反復にステップを作成するだけです。

範囲は低速な関数であり、range(0,50)など、速度を必要としない小さなコードを実行する必要がある場合にのみ使用します。 3つの方法を比較することはできないと思います。彼らは全く違います。

以下のコメントによると、最初のケースはPython 2.7、Python 3でのみ有効で、xrangeのように機能し、それぞれにブロックを割り当てません私はそれをテストしましたが、彼は正しいです。

0
Mr. bug