it-swarm.com.de

Warum ist "1000000000000000 im Bereich (1000000000000001)" in Python 3 so schnell?

Ich verstehe, dass die range()-Funktion, die eigentlich ein Objekttyp in Python 3 ist, ihren Inhalt im Handumdrehen erzeugt, ähnlich einem Generator. 

Da dies der Fall ist, hätte ich erwartet, dass die folgende Zeile übermäßig viel Zeit in Anspruch nehmen würde, da zur Bestimmung, ob 1 Quadrillion im Bereich liegt, eine Billiarde Werte generiert werden müssten: 

1000000000000000 in range(1000000000000001)

Außerdem scheint es so zu sein, dass, egal wie viele Nullen ich addiere, die Berechnung mehr oder weniger die gleiche Zeit in Anspruch nimmt (im Wesentlichen sofort). 

Ich habe auch so etwas ausprobiert, aber die Berechnung ist immer noch fast sofort: 

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

Wenn ich versuche, meine eigene Range-Funktion zu implementieren, ist das Ergebnis nicht so schön !! 

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

Was macht das range()-Objekt unter der Haube, das es so schnell macht? 


Antwort von Martijn Pieters wurde wegen ihrer Vollständigkeit gewählt, aber auch in abarnerts erster Antwort für eine gute Diskussion darüber, was es bedeutet, dass range eine vollwertige sequence in Python 3 ist, und einige Informationen/Warnhinweise zu möglichen Inkonsistenzen bei der __contains__-Funktionsoptimierung in Python-Implementierungen. Die andere Antwort von Abarnert geht noch detaillierter auf und bietet Links für diejenigen, die an der Historie der Optimierung in Python 3 interessiert sind (und die fehlende Optimierung von xrange in Python 2). Die Antworten per Poke und by Wim liefern den relevanten C-Quellcode und die entsprechenden Erklärungen für alle, die daran interessiert sind. 

1572
Rick Teachey

Das Python 3 range()-Objekt erzeugt keine sofortigen Zahlen. Es ist ein intelligentes Sequenzobjekt, das Zahlen on demand erzeugt. Alles, was darin enthalten ist, sind Ihre Start-, Stopp- und Schrittwerte. Wenn Sie das Objekt durchlaufen, wird die nächste Ganzzahl bei jeder Iteration berechnet.

Das Objekt implementiert auch den object.__contains__-Hook , und berechnet, wenn Ihre Nummer Teil seines Bereichs ist. Die Berechnung ist eine O(1) Konstante Zeitoperation. Es ist nie nötig, alle möglichen ganzen Zahlen im Bereich zu scannen.

Aus der range() Objektdokumentation :

Der Vorteil des range-Typs gegenüber einer regulären list oder Tuple besteht darin, dass ein Bereichsobjekt immer dieselbe (kleine) Speichermenge beansprucht, unabhängig von der Größe des Bereichs, den es darstellt (da nur die Werte start, stop und step gespeichert werden) (Berechnung einzelner Artikel und Teilbereiche nach Bedarf).

Zumindest würde Ihr range()-Objekt Folgendes tun:

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

Es fehlen immer noch einige Dinge, die eine echte range() unterstützt (wie die .index()- oder .count()-Methoden, Hashing, Gleichheitstests oder Slicing), sollten Ihnen aber eine Idee geben.

Ich habe auch die __contains__-Implementierung vereinfacht, um mich nur auf ganzzahlige Tests zu konzentrieren. Wenn Sie einem realen range()-Objekt einen nicht ganzzahligen Wert geben (einschließlich Unterklassen von int), wird ein langsamer Scan gestartet, um festzustellen, ob eine Übereinstimmung vorliegt, als ob Sie einen Eindämmungstest gegen eine Liste aller enthaltenen Werte verwenden. Dies wurde getan, um auch andere numerische Typen zu unterstützen, die gerade Gleichheitstests mit Ganzzahlen unterstützen, von denen jedoch nicht erwartet wird, dass sie auch Integer-Arithmetik unterstützen. Siehe das ursprüngliche Python-Problem , das den Eindämmungstest implementiert hat.

1586
Martijn Pieters

Das grundlegende Missverständnis besteht darin, dass range ein Generator ist. Es ist nicht. Tatsächlich ist es kein Iterator.

Sie können dies ziemlich leicht sagen:

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

Wenn es sich um einen Generator handeln würde, würde das einmalige Iterieren ihn erschöpfen:

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

Was range ist, ist eine Sequenz, genau wie eine Liste. Sie können dies sogar testen:

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

Das heißt, es müssen alle Regeln der Reihenfolge eingehalten werden:

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

Der Unterschied zwischen einem range und einem list ist, dass ein range ein fauler oder dynamische Sequenz; Er merkt sich nicht alle Werte, sondern merkt sich nur die Werte für start, stop und step und erstellt die Werte bei Bedarf auf __getitem__.

(Nebenbei bemerkt, wenn Sie print(iter(a)), werden Sie feststellen, dass range den gleichen listiterator Typ wie list verwendet. Wie funktioniert das? Ein listiterator verwendet nichts Besonderes an list, mit Ausnahme der Tatsache, dass es eine C-Implementierung von __getitem__ Bietet, also funktioniert es auch für range .)


Nun, es gibt nichts, was besagt, dass Sequence.__contains__ Eine konstante Zeit sein muss - für offensichtliche Beispiele von Sequenzen wie list ist dies tatsächlich nicht der Fall. Aber nichts sagt, dass es nicht sein kann . Und es ist einfacher, range.__contains__ Zu implementieren, um es nur mathematisch zu überprüfen ((val - start) % step, Aber mit etwas mehr Komplexität, um mit negativen Schritten umzugehen), als alle Werte tatsächlich zu generieren und zu testen. Also warum sollte es nicht besser machen?

Aber es scheint nichts in der Sprache zu geben, das garantiert, dass dies geschehen wird. Wie Ashwini Chaudhari betont, werden alle Werte wiederholt und einzeln verglichen, wenn Sie einen nicht ganzzahligen Wert angeben, anstatt in eine Ganzzahl umzuwandeln und den mathematischen Test durchzuführen. Und nur, weil die Versionen CPython 3.2+ und PyPy 3.x diese Optimierung enthalten und es offensichtlich eine gute Idee und einfach zu tun ist, gibt es keinen Grund, warum IronPython oder NewKickAssPython 3.x sie nicht auslassen könnten. (Und tatsächlich hat CPython 3.0-3.1 es nicht eingeschlossen.)


Wenn range tatsächlich ein Generator wie my_crappy_range Wäre, wäre es nicht sinnvoll, __contains__ Auf diese Weise zu testen, oder zumindest nicht so, wie es Sinn macht offensichtlich. Wenn Sie die ersten 3 Werte bereits wiederholt haben, ist 1 Immer noch in der Generator? Sollte das Testen auf 1 Dazu führen, dass alle Werte bis zu 1 (Oder bis zum ersten Wert >= 1) Iteriert und verbraucht werden?

728
abarnert

Verwenden Sie die Quelle , Luke!

In CPython wird range(...).__contains__ (ein Methodenwrapper) schließlich an eine einfache Berechnung delegieren, die überprüft, ob der Wert möglicherweise im Bereich liegt. Der Grund für die Geschwindigkeit hier ist, dass wir mathematische Überlegungen zu den Grenzen verwenden, anstatt eine direkte Wiederholung des Bereichsobjekts. Um die verwendete Logik zu erklären: 

  1. Stellen Sie sicher, dass die Nummer zwischen start und stop und liegt
  2. Vergewissern Sie sich, dass der Schrittwert unsere Zahl nicht überschreitet. 

Zum Beispiel ist 994 in range(4, 1000, 2), weil:

  1. 4 <= 994 < 1000 und
  2. (994 - 4) % 2 == 0.

Der vollständige C-Code ist unten enthalten, was aufgrund der Speicherverwaltung und der Referenzzählung etwas ausführlicher ist. Die Grundidee ist jedoch vorhanden:

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);
}

Das "Fleisch" der Idee wird in der Zeile erwähnt:

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

Als letzte Anmerkung - schauen Sie sich die range_contains-Funktion am unteren Rand des Code-Snippets an. Wenn die genaue Typprüfung fehlschlägt, verwenden wir nicht den beschriebenen intelligenten Algorithmus, sondern greifen mit _PySequence_IterSearch auf eine stumme Iterationssuche des Bereichs zurück. Sie können dieses Verhalten im Interpreter überprüfen (ich verwende v3.5.0 hier):

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

Um Martijns Antwort hinzuzufügen, ist dies der relevante Teil von der Quelle (in C, da das Bereichsobjekt in nativem Code geschrieben ist):

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);
}

Bei PyLong-Objekten (was int in Python 3 ist) verwendet es die range_contains_long-Funktion, um das Ergebnis zu ermitteln. Diese Funktion prüft im Wesentlichen, ob sich ob im angegebenen Bereich befindet (obwohl sie in C etwas komplexer erscheint).

Wenn es sich nicht um ein int-Objekt handelt, wird es wiederholt, bis der Wert gefunden wird (oder nicht).

Die gesamte Logik könnte wie folgt in Pseudo-Python übersetzt werden:

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

Wenn Sie sich fragen, warum wurde diese Optimierung zu range.__contains__ hinzugefügt und warum nicht zu xrange.__contains__ in 2.7 hinzugefügt wurde:

Wie Ashwini Chaudhary herausfand, wurde Ausgabe 1766304 explizit geöffnet, um [x]range.__contains__ zu optimieren. Ein Patch dafür war akzeptiert und für 3.2 eingecheckt , wurde aber nicht auf 2.7 zurückportiert, weil "xrange sich so lange so benommen hat, dass ich nicht sehe, was es bringt, den Patch zu begehen spät." (2,7 war zu diesem Zeitpunkt fast raus.)

Inzwischen:

Ursprünglich war xrange ein nicht ganz Sequenzobjekt. Wie die 3.1-Dokumente sagen:

Bereichsobjekte haben nur ein sehr geringes Verhalten: Sie unterstützen nur die Indizierung, Iteration und die Funktion len.

Das stimmte nicht ganz. ein xrange-Objekt unterstützte tatsächlich einige andere Dinge, die automatisch mit der Indizierung und len geliefert werden.* einschließlich __contains__ (über lineare Suche). Aber niemand dachte, es lohnte sich damals, vollständige Sequenzen zu machen.

Im Zuge der Implementierung des PEP ( Abstract Base Classes } war es wichtig herauszufinden, welche eingebauten Typen als implementieren gekennzeichnet werden sollten, die ABCs implementieren, und xrange/range behauptete, collections.Sequence zu implementieren, obwohl es immer noch nur behandelt wurde das gleiche "sehr wenig Verhalten". Niemand hat dieses Problem bis Ausgabe 9213 bemerkt. Der Patch für dieses Problem fügte nicht nur index und count zu 3.2s range hinzu, sondern änderte auch den optimierten __contains__ (der die gleiche Mathematik mit index teilt und direkt von count verwendet wird).** Diese Änderung ging auch für 3.2 und wurde nicht auf 2.x zurückportiert, da "es ein Bugfix ist, der neue Methoden hinzufügt". (Zu diesem Zeitpunkt war 2.7 bereits über den rc-Status hinaus.)

Es gab also zwei Chancen, diese Optimierung auf 2,7 zu ​​bringen, aber beide wurden abgelehnt.


* Tatsächlich erhalten Sie sogar eine kostenlose Iteration mit len und Indizierung, aber in 2.3xrange-Objekte haben einen benutzerdefinierten Iterator erhalten. Was sie dann in 3.x verloren haben, das den gleichen listiterator-Typ wie list verwendet.

** Die erste Version hat sie tatsächlich reimplementiert und die Details falsch angegeben - zum Beispiel würde sie Ihnen MyIntSubclass(2) in range(5) == False geben. Die aktualisierte Version des Patches von Daniel Stutzbach hat jedoch den Großteil des vorherigen Codes wiederhergestellt, einschließlich des Rückfalls auf den generischen, langsamen _PySequence_IterSearch, den vor Version 3.2 range.__contains__ implizit verwendet wurde, wenn die Optimierung nicht angewendet wird.

88
abarnert

Die anderen Antworten erklärten es bereits gut, aber ich möchte ein weiteres Experiment anbieten, das die Natur von Entfernungsobjekten veranschaulicht:

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

Wie Sie sehen, ist ein Bereichsobjekt ein Objekt, das sich an seinen Bereich erinnert und mehrmals verwendet werden kann (sogar während es iteriert wird), nicht nur ein einmaliger Generator.

40
Stefan Pochmann

Es geht nur um einen trägen Ansatz bei der Bewertung und einige zusätzliche Optimierungen von range. Werte in Bereichen müssen erst bei der tatsächlichen Verwendung oder aufgrund zusätzlicher Optimierung berechnet werden.

Ihre ganze Zahl ist übrigens nicht so groß, betrachten Sie sys.maxsize

sys.maxsize in range(sys.maxsize) ist ziemlich schnell

aufgrund der Optimierung ist es einfach, gegebene Integer-Werte nur mit min und max des Bereichs zu vergleichen.

aber:

float(sys.maxsize) in range(sys.maxsize) ist ziemlich langsam .

(In diesem Fall gibt es keine Optimierung in range. Wenn also Python unerwartetes Float erhält, vergleicht Python alle Zahlen.)

Sie sollten ein Implementierungsdetail kennen, sich aber nicht darauf verlassen, da sich dies in der Zukunft ändern kann.

11

Hier ist ähnliche Implementierung in C#. Sie können sehen, wie Contains in der Zeit O(1) gemacht wurde.

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

Das von range() zurückgegebene Objekt ist tatsächlich ein range-Objekt. Dieses Objekt implementiert die Iteratorschnittstelle, so dass Sie die Werte wie ein Generator sequenziell durchlaufen können. Es implementiert jedoch auch die __contains__ - Schnittstelle, die tatsächlich aufgerufen wird, wenn ein Objekt auf der rechten Seite des in-Operators angezeigt wird . Die __contains__()-Methode gibt einen Bool zurück, ob das Objekt im Objekt enthalten ist oder nicht. Da range-Objekte ihre Grenzen und Schritte kennen, ist dies in O (1) sehr einfach zu implementieren. 

0
RBF06