web-dev-qa-db-ja.com

なぜPython 3では "1000000000000000の範囲(1000000000000001)"がとても速いのですか?

私の理解するところでは、range()関数は実際には Python 3のオブジェクト型 であり、その内容はジェネレータと同じようにその場で生成されます。

このような場合、次の行には非常に長い時間がかかることが予想されます。なぜなら、1兆分の1が範囲内にあるかどうかを判断するには、4兆分の1の値を生成する必要があるからです。

1000000000000000 in range(1000000000000001)

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

私もこのようなことを試してみましたが、計算はまだほぼ瞬時です。

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

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

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

それをとても速くするフードの下でrange()オブジェクトがしていることは何ですか?


Martijn Pietersの回答 は完全性のために選ばれましたが、 abarnertの最初の回答も参照してください Python 3でrangeが本格的なsequenceであることの意味そして、Python実装間での__contains__関数最適化の潜在的な矛盾に関する情報/警告。 abarnert's other answer は、もう少し詳しく説明し、Python 3での最適化の背後にある歴史(およびPython 2でのxrangeの最適化の欠如)に関心がある人へのリンクを提供します。 Answers pokeで and wimで 興味のある人のための適切なCソースコードと説明を提供する。

1660
Rick Teachey

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

オブジェクトは object.__contains__フック を実装し、あなたの数がその範囲の一部であればcalculateを実装します。計算はO(1)定時演算です。範囲内のすべての可能な整数をスキャンする必要はありません。

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 = stop, start
        else:
            lo, hi = start, stop
        self.length = ((hi - lo - 1) // abs(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の問題 を参照してください。

1586
Martijn Pieters

ここでの基本的な誤解は、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

rangelistの違いは、rangelazyまたはdynamicシーケンス;すべての値を覚えているわけではなく、 startstop、およびstepを記憶し、__getitem__でオンデマンドで値を作成します。

(補足として、print(iter(a))を使用すると、rangelistiteratorと同じlistタイプを使用することに気付くでしょう。それはどのように機能しますか?listiteratorは、__getitem__のC実装を提供するという事実を除いて、listについて特別なものを使用しないため、rangeでも正常に機能します。


さて、Sequence.__contains__が一定時間でなければならないということは何もありません。実際、listのようなシーケンスの明らかな例ではそうではありません。しかし、それはできないになることはありません。そして、数学的にチェックするためにrange.__contains__を実装するほうが簡単です((val - start) % step、しかしネガティブなステップに対処するための余分な複雑さ)実際にすべての値を生成してテストするので、なぜしないほうが良い方法ですか?

しかし、言語には保証これが起こることはないようです。AshwiniChaudhariが指摘しているように、整数に変換して数学的なテストを行う代わりに、非整数値を与えるとそして、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)までのすべての値を反復して消費する必要がありますか?

728
abarnert

source を使ってください、Luke!

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

  1. 数値がstartstopの間にあることを確認してください。
  2. ストライド値が私たちの数字を「超えない」ことを確認してください。 

たとえば、994range(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)
311
wim

Martijnの答えに加えて、これは source の関連部分です(Cでは、rangeオブジェクトはネイティブコードで書かれているため):

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オブジェクト(Python 3ではint)の場合、結果を決定するために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
116
poke

ご参考までに なぜ この最適化がrange.__contains__に追加されたのか、そしてなぜ そうではない 2.7のxrange.__contains__に追加されたのか:

最初に、Ashwini Chaudharyが発見したように、 issue 1766304[x]range.__contains__を最適化するために明示的に開かれました。これに対するパッチは 受け入れられて3.2 のためにチェックインされました、しかし2.7にバックポートされませんでした。 "xrangeは長い間このように振る舞いました。 「 (2.7はその時点ではほとんど出ていませんでした。)

その間:

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

範囲オブジェクトはほとんど動作しません。それらはインデックス、繰り返し、そしてlen関数のみをサポートします。

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

次に、 Abstract Base Classes PEPの実装の一部として、どの組み込み型をどのABCの実装としてマークする必要があるか、およびxrange/rangecollections.Sequenceを実装すると主張したとしても、それを扱うことが重要でした。 「ほとんど動作しません」。 問題9213 までその問題に誰も気づかなかった。その問題に対するパッチは3.2のindexcountrangeを追加しただけでなく、最適化された__contains__indexと同じ数学を共有し、countによって直接使用される)も作り直しました。** この変更点 は3.2でも導入され、2.xにバックポートされませんでした。「新しいメソッドを追加するのはバグ修正だからです」。 (この時点で、2.7はすでにrcステータスを過ぎていました。)

そのため、この最適化を2.7に戻す可能性は2つありましたが、どちらも拒否されました。


*実際には、lenとインデックスを使って無料で繰り返しを取得することもできますが、 2.3xrangeオブジェクトにはカスタムイテレータがあります。これは3.xで失われましたが、これはlistiteratorと同じlist型を使用します。

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

88
abarnert

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

>>> 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]

ご覧のとおり、rangeオブジェクトはその範囲を記憶していて、繰り返し使用している間でも何度も使用できるオブジェクトであり、単なる一回限りのジェネレータではありません。

40
Stefan Pochmann

これは評価に対する怠惰なアプローチとrangeのいくつかの追加の最適化についてのものです。

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

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

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

しかし:

float(sys.maxsize) in range(sys.maxsize) はかなり遅いです

(この場合、rangeには最適化がありません。そのため、pythonが予期しないfloatを受け取った場合、pythonはすべての数値を比較します)

実装の詳細を知っておく必要がありますが、これは将来変更される可能性があるため信頼しないでください。

11

これはC# 類似の実装です。 O(1) timeでContainsがどのように行われたかがわかります。

public struct Range
{

    private readonly int _start;
    private readonly int _stop;
    private readonly int _step;


    //other methods/properties omitted


    public bool Contains(int number)
    {
        // precheck: if the number isnt in valid point, return false
        // for example, if start is 5 and step is 10, then its impossible that 163 be in range at any interval      

        if ((_start % _step + _step) % _step != (number % _step + _step) % _step)
            return false;

        // v is vector: 1 means positive step, -1 means negative step
        // this value makes final checking formula straightforward.

        int v = Math.Abs(_step) / _step;

        // since we have vector, no need to write if/else to handle both cases: negative and positive step
        return number * v >= _start * v && number * v < _stop * v;
    }
}
5
Sanan Fataliyev

TL、DR

range()によって返されるオブジェクトは、実際にはrangeオブジェクトです。このオブジェクトはイテレータインタフェースを実装しているので、ジェネレータのように値を順番に繰り返すことができますが、実際にはオブジェクトがオブジェクトの右側に表示されるときに呼び出される __contains__ インタフェースも実装しています。 in演算子__contains__()メソッドは、項目がオブジェクト内にあるかどうかのブール値を返します。 rangeオブジェクトはその境界とストライドを知っているので、これはO(1)で実装するのが非常に簡単です。 

0
RBF06