Python 3で「1000000000000000 in range(1000000000000001)」が非常に高速なのはなぜですか?


2116

私が理解しているのは、range()実際にはPython 3のオブジェクト型である関数が、ジェネレーターのようにその場でコンテンツを生成することです。

これが事実である場合、1兆が範囲内にあるかどうかを判断するためには、1兆の値を生成する必要があるため、次の行に膨大な時間がかかると予想していました。

1000000000000000 in range(1000000000000001)

さらに、ゼロをいくつ追加しても、計算には多少同じ時間がかかります(基本的には瞬時)。

私もこのようなことを試しましたが、計算はまだほとんど瞬時です:

1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens

私が自分の範囲関数を実装しようとすると、結果はそれほど良くありません!!

def my_crappy_range(N):
    i = 0
    while i < N:
        yield i
        i += 1
    return

何でrange()それがとても速くなりボンネットの下にやったオブジェクトは?


マルタインピータースの答えは、その完全性のために選ばれた、だけでなく、見たabarnertの最初の答えのためにそれが何を意味するかの良い議論のためのrange本格的なようにシーケンスのPython 3での、およびのための潜在的な矛盾に関するいくつかの情報/警告__contains__のPython実装間の機能の最適化。abarnertの他の回答は、さらに詳細に説明されており、Python 3での最適化の背後にある歴史(およびxrangePython 2 での最適化の欠如)に関心のある人にリンクを提供しています。pokewim による回答は、関連するCソースコードと、興味がある人のための説明を提供します。


70
これは、チェックしているアイテムがboolまたはlongタイプである場合にのみ当てはまることに注意してください。お試しください:100000000000000.0 in range(1000000000000001)
アシュビニーChaudharyさん

10
誰がそれをrange発電機だと言ったのですか?
abarnert

7
@abarnert私が行った編集は混乱をそのまま残したと思います。
リックはモニカをサポートします

5
@AshwiniChaudharyはPython2がPython3 xrangeと同じでrangeはありませんか?
スーパーベスト

28
@Superbest xrange()オブジェクトには__contains__メソッドがないため、アイテムチェックはすべてのアイテムをループする必要があります。さらにrange()、にはスライス(これrangeもオブジェクトを返す)をサポートし、ABC との互換性を保つためのメソッドcountindexメソッドが追加されているなど、には他にもいくつかの変更がありますcollections.Sequence
Ashwini Chaudhary

回答:


2171

Python 3 range()オブジェクトはすぐに数値を生成しません。オンデマンドで数値を生成するスマートシーケンスオブジェクトです。そこに含まれているのは、開始、停止、およびステップの値だけであり、オブジェクトを反復処理すると、次の整数が反復ごとに計算されます。

オブジェクトはobject.__contains__フックも実装し、数値がその範囲に含まれるかどうかを計算します。計算は(ほぼ)一定時間の操作です*。範囲内のすべての可能な整数をスキャンする必要は決してありません。

range()オブジェクトのドキュメントから:

range通常の、listまたはtuple範囲オブジェクトは、それが表す範囲のサイズに関係なく、常に同じ(少量)のメモリを使用するというタイプの利点です(それはstartstopおよびstep値を格納し、個々のアイテムとサブ範囲を計算するだけなので)必要に応じて)。

したがって、少なくとも、range()オブジェクトは次のようになります。

class my_range(object):
    def __init__(self, start, stop=None, step=1):
        if stop is None:
            start, stop = 0, start
        self.start, self.stop, self.step = start, stop, step
        if step < 0:
            lo, hi, step = stop, start, -step
        else:
            lo, hi = start, stop
        self.length = 0 if lo > hi else ((hi - lo - 1) // step) + 1

    def __iter__(self):
        current = self.start
        if self.step < 0:
            while current > self.stop:
                yield current
                current += self.step
        else:
            while current < self.stop:
                yield current
                current += self.step

    def __len__(self):
        return self.length

    def __getitem__(self, i):
        if i < 0:
            i += self.length
        if 0 <= i < self.length:
            return self.start + i * self.step
        raise IndexError('Index out of range: {}'.format(i))

    def __contains__(self, num):
        if self.step < 0:
            if not (self.stop < num <= self.start):
                return False
        else:
            if not (self.start <= num < self.stop):
                return False
        return (num - self.start) % self.step == 0

これには、実際にrange()サポートされているいくつかのものが欠けています(.index()または.count()メソッド、ハッシュ、同等性テスト、スライスなど)が、アイデアを与えるはずです。

また__contains__、整数テストのみに焦点を合わせるように実装を簡略化しました。実際のrange()オブジェクトに整数以外の値(のサブクラスを含むint)を指定すると、含まれているすべての値のリストに対して包含テストを使用する場合と同様に、一致するかどうかを確認するために低速スキャンが開始されます。これは、たまたま整数による同等性テストをサポートする他の数値型を引き続きサポートするために行われましたが、整数演算もサポートすることは期待されていません。封じ込めテストを実装した元のPythonの問題を参照してください。


* Python整数には制限がないため、Nが大きくなるにつれて数学演算も時間とともに大きくなり、これをO(log N)演算にするため、ほぼ一定の時間。すべてが最適化されたCコードで実行され、Pythonは整数値を30ビットのチャンクに格納するため、ここに含まれる整数のサイズによるパフォーマンスへの影響が見られる前に、メモリが不足します。


58
楽しい事実:あなたが働いて実装を持っているので、__getitem__そして__len____iter__実装は実際に不要です。
ルクレティエル

2
@Lucretiel:Python 2.3では、xrangeiterator速度が十分でないため、特別なものが特別に追加されました。そして、3.xのどこか(3.0と3.2のどちらであるかはわかりません)はトスされ、同じlistiterator型をlist使用しています。
abarnert 2015年

1
私はコンストラクタをとして定義def __init__(self, *start_stop_step)し、そこから構文解析します。引数のラベル付けの方法は、やや混乱しています。それにもかかわらず、+ 1。あなたはまだ間違いなく行動を説明しました。
Cody Piersall

1
@CodyPiersall:残念ながら、これは実際のクラスの初期化子のシグネチャです。rangeより古い*argsargclinicC-API関数に完全なPythonシグネチャを持たせるAPIがはるかに少ない)。(といくつかの新しい機能、のようないくつかの他の古い関数xrangeslice、およびitertools.islice一貫性のためには、)同じように動作しますが、ほとんどの部分は、グイドとコア開発者の残りの部分はあなたに同意するように見えます。2.0+のドキュメントrangeは、実際の紛らわしいシグネチャを示すのではなく、C ++スタイルのオーバーロードであるかのように説明し、友だちですらしています。
-abarnert

2
@CodyPiersall:実際に、argclinicニックコグランがrange明確に定義できるようにする方法を思いついたときの議論のGuidoからの引用は次のとおりです。それで、私は彼がrange書かれたようにそれが混乱していることに同意することを私はかなり確信しています。
abarnert

845

ここでの根本的な誤解は、それrangeがジェネレータであると考えることです。そうではありません。実際、それはいかなる種類のイテレータでもありません。

これはかなり簡単にわかります。

>>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4]

ジェネレーターの場合は、一度反復すると使い尽くされます。

>>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[]

range実際に、あることはちょうどリストのように、シーケンスです。これをテストすることもできます:

>>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True

つまり、シーケンスであることのすべてのルールに従う必要があります。

>>> a[3]         # indexable
3
>>> len(a)       # sized
5
>>> 3 in a       # membership
True
>>> reversed(a)  # reversible
<range_iterator at 0x101cd2360>
>>> a.index(3)   # implements 'index'
3
>>> a.count(3)   # implements 'count'
1

a rangeとa の違いlistは、a range遅延シーケンスまたは動的シーケンスであることです。それはその値のすべてを覚えていない、それだけでその覚えてstartstop、およびstep、オンデマンドで値を作成します__getitem__

もしあれば(、注意点としてprint(iter(a))、あなたはそれがわかりますrange同じ使用するlistiteratorようなタイプをlist。そのしくみを教えてください。Aはlistiteratorについては何の特別なを使用していないlist、それはのCの実装を提供しているという事実を除いて__getitem__、それがために罰金を動作しますので、rangeも。)


さて、それSequence.__contains__が一定の時間である必要があると言っていることは何もlistありません。実際、のようなシーケンスの明白な例では、そうではありません。しかし、それが不可能はないということ何もありません。そして、それは実装が簡単ですrange.__contains__ただ数学的にそれをチェックするために((val - start) % stepなぜ、実際にすべての値を生成し、テストによりますが、負のステップに対処するためのいくつかの余分な複雑さを持つ)べきではない、それはそれより良い方法でしょうか?

しかし、これが起こることを保証する言語には何もないようです。Ashwini Chaudhariが指摘するように、整数に変換して数学的なテストを行う代わりに、非整数値を指定すると、すべての値を繰り返して1つずつ比較することにフォールバックします。そして、CPython 3.2+とPyPy 3.xバージョンがたまたまこの最適化を含んでいて、それが明らかに良いアイデアであり、簡単に実行できるという理由だけで、IronPythonまたはNewKickAssPython 3.xがそれを省略できなかった理由はありません。(そして実際、CPython 3.0-3.1に含まれていませんでした。)


場合はrange、実際には発電機だった、のようにmy_crappy_range、それはテストする意味がありません__contains__このように、またはそれは意味が明白ではないでしょうなり、少なくとも方法。最初の3つの値を既に繰り返している場合1でもin、ジェネレーターですか?テストを行うと1、それまでの1(または最初の値までの>= 1)すべての値が反復されて消費されますか?


10
これは正直に言うとかなり重要なことです。Python 2とPython 3の違いがこの点についての混乱を招いたのではないでしょうか。いずれにせよ、シーケンスタイプとしておよびとともにがリストされているのでrangelisttuple、私は気付いたはずです。
リックはモニカをサポート

4
@RickTeachey:実際には、2.6以降(おそらく2.5以降)xrangeもシーケンスです。2.7 docsを参照してください。実際、それは常にほぼシーケンスでした。
abarnert 2015年

5
@RickTeachey:実際、私は間違っていました。2.6-2.7(および3.0-3.1)では、シーケンスであると主張していますが、それでもまだほとんどシーケンスです。私の他の答えを見てください。
abarnert 2015年

2
これはイテレータではなく、シーケンスです(Javaの観点から反復可能、C#のIEnumerable)- .__iter__()イテレータを返すメソッドを持つもの。その順番で使用できるのは1回だけです。
Smit Johnth

4
@ThomasAhle:rangeが整数でない場合、は型をチェックしないため、型は__eq__と互換性のあるを持つことが常に可能intです。確かに機能strしませが、そこに存在できないすべての型を明示的にチェックすることで速度を低下させたくありませんでした(結局、strサブクラスがオーバーライド__eq__してに含まれる可能性がありますrange)。
ShadowRanger 2016年

377

ソースを使用して、ルーク!

CPythonでは、range(...).__contains__(メソッドラッパー)は最終的に、値が範囲内にある可能性があるかどうかをチェックする単純な計算に委任します。ここでの速度の理由は、範囲オブジェクトを直接反復するのではなく、範囲について数学的な推論を使用しているためです。使用されるロジックを説明するには:

  1. 数が間にあることを確認startしてstop、と
  2. ストライド値が数値を「超えない」ことを確認します。

たとえば994、次のrange(4, 1000, 2)理由によります:

  1. 4 <= 994 < 1000、および
  2. (994 - 4) % 2 == 0

完全なCコードは以下に含まれていますが、メモリ管理と参照カウントの詳細のために少し冗長ですが、基本的な考え方はそこにあります。

static int
range_contains_long(rangeobject *r, PyObject *ob)
{
    int cmp1, cmp2, cmp3;
    PyObject *tmp1 = NULL;
    PyObject *tmp2 = NULL;
    PyObject *zero = NULL;
    int result = -1;

    zero = PyLong_FromLong(0);
    if (zero == NULL) /* MemoryError in int(0) */
        goto end;

    /* Check if the value can possibly be in the range. */

    cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
    if (cmp1 == -1)
        goto end;
    if (cmp1 == 1) { /* positive steps: start <= ob < stop */
        cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
        cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
    }
    else { /* negative steps: stop < ob <= start */
        cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
        cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
    }

    if (cmp2 == -1 || cmp3 == -1) /* TypeError */
        goto end;
    if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
        result = 0;
        goto end;
    }

    /* Check that the stride does not invalidate ob's membership. */
    tmp1 = PyNumber_Subtract(ob, r->start);
    if (tmp1 == NULL)
        goto end;
    tmp2 = PyNumber_Remainder(tmp1, r->step);
    if (tmp2 == NULL)
        goto end;
    /* result = ((int(ob) - start) % step) == 0 */
    result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
  end:
    Py_XDECREF(tmp1);
    Py_XDECREF(tmp2);
    Py_XDECREF(zero);
    return result;
}

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

アイデアの「肉」は行に記載されています

/* result = ((int(ob) - start) % step) == 0 */ 

最後range_containsに、コードスニペットの下部にある関数を見てください。正確な型チェックが失敗した場合は、上記の賢いアルゴリズムを使用せず、代わりに_PySequence_IterSearch!この動作はインタープリターで確認できます(ここではv3.5.0を使用しています)。

>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
...     pass
... 
>>> x_ = MyInt(x)
>>> x in r  # calculates immediately :) 
True
>>> x_ in r  # iterates for ages.. :( 
^\Quit (core dumped)

144

Martijnの答えに追加すると、これはソースの関連部分です(範囲オブジェクトはネイティブコードで記述されているため、Cでは):

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

したがって、PyLongオブジェクト(intPython 3にある)の場合、range_contains_long関数を使用して結果を決定します。そして、その関数は本質的にob指定された範囲内にあるかどうかをチェックします(ただし、Cでは少し複雑に見えます)。

intオブジェクトでない場合は、値が見つかる(または見つからない)まで、繰り返し処理にフォールバックします。

ロジック全体を次のように擬似Pythonに変換できます。

def range_contains (rangeObj, obj):
    if isinstance(obj, int):
        return range_contains_long(rangeObj, obj)

    # default logic by iterating
    return any(obj == x for x in rangeObj)

def range_contains_long (r, num):
    if r.step > 0:
        # positive step: r.start <= num < r.stop
        cmp2 = r.start <= num
        cmp3 = num < r.stop
    else:
        # negative step: r.start >= num > r.stop
        cmp2 = num <= r.start
        cmp3 = r.stop < num

    # outside of the range boundaries
    if not cmp2 or not cmp3:
        return False

    # num must be on a valid step inside the boundaries
    return (num - r.start) % r.step == 0

11
@ChrisWesseling:これは、Martijnの回答を編集することは適切ではなかったと思われる、十分に異なる(そして十分な)情報だと思います。それは判断の呼びかけですが、人々は通常、他の人々の答えに劇的な変更を加えないという側で誤りを犯します。
abarnert 2015年

105

なぜこの最適化がに追加されたのrange.__contains__か、なぜ2.7に追加されなかったのか疑問に思っている場合xrange.__contains__

まず、Ashwini Chaudharyが発見したように、最適化のために問題1766304が明示的に開かれました[x]range.__contains__。このパッチは受け入れられ、3.2チェックインされましたが、2.7にバックポートされていませんでした。「xrangeは長い間このように動作しており、このパッチをコミットするために何が必要かわかりません。」(その時点で2.7はほとんど出ていませんでした。)

その間:

もともとxrangeは、完全なシーケンスオブジェクトではありませんでした。3.1ドキュメントは言います:

範囲オブジェクトの動作はほとんどありませんlen。インデックスオブジェクト、反復、および関数のみをサポートします。

これは真実ではありませんでした。xrangeオブジェクトは、実際にインデックスとして自動的に来るいくつか他のものをサポートしlen*を含む__contains__(線形検索を経由して)。しかし、当時は完全なシーケンスにする価値があるとは誰も思っていませんでした。

次に、抽象基本クラス PEPの実装の一部として、どの「組み込み型」がどのABCの実装としてマークされ、xrange/ rangeが実装を要求されcollections.Sequenceていても、同じ「非常に小さな動作」しか処理しなかったとしても、それを把握することが重要でした。問題9213までは誰もその問題に気づきませんでした。その問題のためのパッチが追加されていないだけindexcount3.2のにrange、それはまた、働いていた再最適化された__contains__(と同じ数学を共有するindexと、直接で使用されていますcount)。** この変更は3.2にも適用され、2.xにバックポートされませんでした。「それは新しいメソッドを追加するバグ修正だからです」。(この時点で、2.7はすでにrcステータスを超えていました。)

したがって、この最適化を2.7にバックポートする可能性は2つありましたが、どちらも拒否されました。


*実際、インデックス作成だけで無料でイテレーションを取得することもできますが、2.3 では、xrangeオブジェクトにカスタムイテレータが追加されました。

**最初のバージョンは実際にそれを再実装し、詳細が間違っていました-たとえば、それはあなたに与えるでしょうMyIntSubclass(2) in range(5) == False。しかし、Daniel Stutzbachの更新されたバージョンのパッチは、最適化が適用されないときに_PySequence_IterSearch3.2より前のバージョンrange.__contains__が暗黙的に使用していたジェネリックへのフォールバックを含め、以前のコードのほとんどを復元しました。


4
ここのコメントから:改善xrange.__contains__、ユーザーに驚きの要素を残すためだけにPython 2にバックポートしていないようで、o_Oは遅すぎました。countそしてindex パッチは、後に追加されました。その時のファイル:hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.c
Ashwini Chaudhary

12
私はいくつかのコアpython開発者がはるかに優れたpython3に切り替えるように人々を奨励したいので、Python 2.xに対する「タフな愛」に部分的であるという不吉な疑いを持っています:)
wim

4
また、古いバージョンに新しい機能を追加しなければならないのは大変な負担だと思います。オラクルに行って「ほら、私はJava 1.4を使っていて、ラムダ式に値する!何もせずにバックポートする」と言ったと想像してみてください。
ロブ・グラント

2
@RickTeacheyええ、それは単なる例です。私が1.7と言ったとしても、それは当てはまります。それは定性的ではなく定量的な違いです。基本的に、(無給の)開発者は、3.xでクールな新しいものを永遠に作り、アップグレードしたくない人のためにそれを2.xにバックポートすることはできません。それは巨大でばかげた負担です。私の推論にまだ何か問題があると思いますか?
ロブ・グラント

3
@RickTeachey:2.7は3.1から3.2の間で、3.3あたりではありませんでした。そして、それは、3.2への最後の変更が行われたときに2.7がrcに入っていたことを意味します。これにより、バグのコメントが理解しやすくなります。とにかく、私は彼らが振り返ってみると、いくつかのミスを犯したと思います(特に仮定の人々が経て移行するでしょう2to3代わりのようなライブラリの助けを借りて、デュアルバージョンコードを経由してsix、我々はのようなものだ理由は、dict.viewkeysその誰もが今まで使用しに行くんですが)、とありました3.2で遅すぎたいくつかの変更ですが、2.7はかなり印象的な「これまでの2.xの」リリースでした。
-abarnert

47

他の答えはすでにそれをうまく説明しましたが、範囲オブジェクトの性質を説明する別の実験を提供したいと思います:

>>> r = range(5)
>>> for i in r:
        print(i, 2 in r, list(r))

0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]

ご覧のように、範囲オブジェクトはその範囲を記憶しているオブジェクトであり、(それを反復している間でも)何度も使用でき、1回限りのジェネレータではありません。


27

それはすべて、評価への遅延アプローチとの追加の最適化に関するものrangeです。範囲内の値は、実際に使用するまで、または追加の最適化によりさらに計算する必要はありません。

ところで、あなたの整数はそれほど大きくありません、考慮してください sys.maxsize

sys.maxsize in range(sys.maxsize) かなり速い

最適化のため-指定された整数を範囲の最小値と最大値と比較するのは簡単です。

だが:

Decimal(sys.maxsize) in range(sys.maxsize) かなり遅いです。

(この場合、には最適化がないrangeため、Pythonが予期しないDecimalを受け取った場合、Pythonはすべての数値を比較します)

実装の詳細に注意する必要がありますが、これは将来変更される可能性があるため、信頼すべきではありません。


4
大整数の浮動小数点数に注意してください。float(sys.maxsize) != sys.maxsize)にもかかわらず、ほとんどのマシンでsys.maxsize-float(sys.maxsize) == 0
holdenweb

18

TL; DR

によって返されるオブジェクトは、range()実際にはrangeオブジェクトです。このオブジェクトは反復子インターフェースを実装しているため、ジェネレーター、リスト、またはタプルのように、その値を順次反復できます。

しかし、それはまた__contains__、オブジェクトがinオペレーターの右側に現れるときに実際に呼び出されるものであるインターフェースを実装します。この__contains__()メソッドは、boolの左側のアイテムinがオブジェクト内にあるかどうかを返します。rangeオブジェクトは境界とストライドを知っているため、これはO(1)で実装するのが非常に簡単です。


0
  1. 最適化により、指定された整数を最小範囲と最大範囲で比較するのは非常に簡単です。
  2. Python (3)range()関数が非常に高速である理由は、ここでは、範囲オブジェクトを直接反復するのではなく、数学的推論を境界に使用しているためです。
  3. ここでロジックを説明するために:
    • 番号が開始と停止の間にあるかどうかを確認します。
    • ステップ精度の値が数値を超えていないかどうかを確認します。
  4. 例として、997は範囲(4、1000、3)にあります

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.


1
そのソースを共有できますか?それが合法に聞こえるかもしれませんが、実際のコードでこれらの主張を裏付けるのは良いことです
ニコ・ハーセ

これは実装可能な例だと思います。それが実装されている正確な方法ではありません。参照は提供されていませんが、範囲の包含チェックがリストまたはタプルよりもはるかに高速である理由を理解するのに十分なヒントとして
役立ちます

0

ジェネレーター内包表記を使用して最適化が呼び出されないように、x-1 in (i for i in range(x))大きなx値を試してくださいrange.__contains__

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.