Dlaczego "10000000000000000000 w zakresie (100000000000000001)" jest tak szybki w Pythonie 3?

Rozumiem, że range() funkcja, która jest w rzeczywistości typem obiektu w Pythonie 3 , generuje swoją zawartość w locie, podobnie jak generator.

W tym przypadku spodziewałbym się, że następująca linia zajmie zbyt dużo czasu, ponieważ aby określić, czy 1 kwadrylion jest w zakresie, trzeba by wygenerować wartości kwadryliona:

1000000000000000 in range(1000000000000001)

Ponadto: wydaje się, że bez względu na to, ile zer dodam, obliczenia mniej więcej tyle samo czasu (w zasadzie natychmiastowe).

Próbowałem też takich rzeczy, ale obliczenia wciąż są prawie natychmiastowe: {]}

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

Jeśli próbuję zaimplementować własną funkcję range, wynik nie jest taki miły!!

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

Co robi obiekt range() pod maską, który sprawia, że jest tak szybki?


Odpowiedź Martijn Pieters została wybrana ze względu na jej kompletność, ale Zobacz także pierwsza odpowiedź abarnerta aby dobrze omówić, co oznacza dla range być pełnoprawnym sekwencją w Pythonie 3, i kilka informacji/ostrzeżeń dotyczących potencjalnej niespójności dla __contains__ optymalizacji funkcji we wszystkich implementacjach Pythona. inna odpowiedź abarnerta jest bardziej szczegółowa i zawiera linki dla osób zainteresowanych historią optymalizacji w Pythonie 3 (i brakiem optymalizacji xrange w Pythonie 2). Odpowiedzi by poke i by wim podaj odpowiedni kod źródłowy C i wyjaśnienia dla zainteresowanych.

Author: Rick Teachey, 2015-05-06

7 answers

Obiekt Python 3 range() nie wytwarza liczb od razu; jest to inteligentny obiekt sekwencyjny, który wytwarza liczby na żądanie. Wszystko, co zawiera to twoje wartości start, stop i step, a następnie podczas iteracji nad obiektem następna liczba całkowita jest obliczana przy każdej iteracji.

Obiekt implementuje również object.__contains__ hook i oblicza, jeśli liczba jest częścią jej zakresu. Obliczanie jest operacją o (1) w czasie stałym. Nigdy nie ma potrzeby skanowania wszystkie możliwe liczby całkowite w zakresie.

Z range() Dokumentacja obiektu :

Zaletą typu range w stosunku do zwykłego list lub tuple jest to, że obiekt range zawsze będzie pobierał tę samą (małą) ilość pamięci, bez względu na rozmiar zakresu, który reprezentuje (ponieważ przechowuje tylkostart, stop i step wartości, obliczanie poszczególnych pozycji i podpranżów w razie potrzeby).

Więc przynajmniej Twój range() obiekt do:

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

Nadal brakuje kilku rzeczy, które obsługuje prawdziwa range() (takich jak metody .index() lub .count(), haszowanie, testowanie równości lub krojenie), ale powinno dać ci pomysł.

Uprościłem również implementację __contains__, aby skupiać się tylko na testach całkowitych; jeśli podasz rzeczywistemu obiektowi range() wartość nie-całkowitą( w tym podklasy int), powolne skanowanie jest inicjowane, aby sprawdzić, czy istnieje dopasowanie, tak jak jeśli użyjesz testu przechowawczego przeciwko liście wszystkich zawartych w nim danych. wartości. Zostało to zrobione w celu dalszego wspierania innych typów liczbowych, które po prostu się zdarzyć, aby wspierać testowanie równości z liczbami całkowitymi, ale nie oczekuje się, aby wspierać arytmetykę całkowitą, jak również. Zobacz oryginalną wersję Pythona , w której zaimplementowano test przechowawczy.

 1427
Author: Martijn Pieters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-20 20:42:07

Fundamentalnym nieporozumieniem jest myślenie, że jest generatorem. Nie jest. W rzeczywistości nie jest to żaden iterator.

Możesz to powiedzieć dość łatwo:

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

Gdyby był generatorem, iteracja raz by go wyczerpała:

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

Czym range w rzeczywistości jest sekwencja, tak jak lista. Możesz nawet przetestować to:

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

Oznacza to, że musi przestrzegać wszystkich zasad bycia sekwencją:

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

Różnica między a range i a list to, że a range jest leniwym lub dynamicznym ciągiem; nie pamięta wszystkich swoich wartości, tylko pamięta swoje start, stop, i step, i tworzy wartości na żądanie na __getitem__.

(na marginesie, jeśli print(iter(a)), zauważysz, że range używa tego samego typu listiterator, co list. Jak to działa? A listiterator nie używa niczego specjalnego w list z wyjątkiem faktu, że dostarcza implementację C __getitem__, więc działa dobrze dla range też.)


Teraz, nie ma nic, co mówi, że Sequence.__contains__ musi być stały czas-w rzeczywistości, dla oczywistych przykładów sekwencji, takich jak list, nie jest. ale nie ma nic, co mówi, że nie może {46]} być. I łatwiej jest zaimplementowaćrange.__contains__ po prostu sprawdzić to matematycznie ((val - start) % step, ale z pewną dodatkową złożonością do radzenia sobie z negatywnymi krokami) niż faktycznie generować i testować wszystkie wartości, więc dlaczego nie powinno to robić lepiej?

Ale nie wydaje się być wszystko, co w języku gwarantuje to się stanie. Jak podkreśla Ashwini Chaudhari, jeśli nadasz jej wartość nieintegralną, zamiast przekształcić ją w liczbę całkowitą i wykonać test matematyczny, powróci do iteracji wszystkich wartości i porównania ich jeden po drugim. I tylko dlatego, że CPython 3.2+ i PyPy 3.wersje x zawierają tę optymalizację i jest to oczywisty dobry pomysł i łatwy do zrobienia, nie ma powodu, aby IronPython lub NewKickAssPython 3.x couldn ' t leave it Wynocha. (A w rzeczywistości CPython 3.0-3.1 nie zawierał tego.)


Gdyby range rzeczywiście były generatorem, jak my_crappy_range, to testowanie __contains__ w ten sposób nie miałoby sensu, a przynajmniej sposób, w jaki to ma sens, nie byłby oczywisty. Jeśli już iterowałeś pierwsze 3 wartości, to czy 1 nadal in jest generatorem? Czy testowanie dla 1 spowoduje iterację i pochłonie wszystkie wartości do 1 (lub do pierwszej wartości >= 1)?

 597
Author: abarnert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-01 05:46:10

Użyj źródła , Luke!

W Cpythonie, range(...).__contains__ (wrapper metody)ostatecznie deleguje się do prostego obliczenia, które sprawdza, czy wartość może być w zakresie. Powodem prędkości jest to, że używamy matematycznego rozumowania o granicach, a nie bezpośredniej iteracji obiektu range. Aby wyjaśnić zastosowaną logikę:

  1. sprawdź, czy liczba jest pomiędzy start i stop, oraz
  2. sprawdź, czy wartość kroku nie "step over" nasz numer.

Na przykład, 994 jest w range(4, 1000, 2) ponieważ:

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

Pełny kod C znajduje się poniżej, co jest nieco bardziej wyraziste ze względu na zarządzanie pamięcią i szczegóły zliczania odniesień, ale podstawowa idea jest tam:

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

"mięso" idei jest wymienione w linii :

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

Jako ostatnia uwaga-spójrz na funkcję range_contains u dołu fragmentu kodu. Jeśli dokładne sprawdzenie typu nie powiedzie się, wtedy nie używamy opisanego sprytnego algorytmu, zamiast tego wracamy do głupiego wyszukiwania iteracji zakresu za pomocą _PySequence_IterSearch! Możesz sprawdzić to zachowanie w interpreterze (używam tutaj 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)
 293
Author: wim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-19 18:16:11

Aby dodać do odpowiedzi Martijna, jest to odpowiednia część źródła (W C, ponieważ obiekt range jest napisany w kodzie natywnym):

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

Tak więc dla obiektów PyLong (co jest {[3] } w Pythonie 3), będzie on używał funkcji range_contains_long do określenia wyniku. I ta funkcja zasadniczo sprawdza, czy ob znajduje się w podanym zakresie (chociaż w C wygląda to nieco bardziej skomplikowanie).

Jeśli nie jest to obiekt int, wraca do iteracji, dopóki nie znajdzie wartości (lub nie).

Całą logikę można by przetłumaczyć na pseudo-Python w następujący sposób:

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
 108
Author: poke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-07 06:25:48

Jeśli zastanawiasz się dlaczego ta optymalizacja została dodana do range.__contains__, a dlaczego nie została dodana do w 2.7:

Po pierwsze, jak odkrył Ashwini Chaudhary, numer 1766304 został otwarty jawnie, aby zoptymalizować [x]range.__contains__. Patch do tego został zaakceptowany i sprawdzony dla 3.2, ale nie został ponownie przeniesiony do 2.7, ponieważ " xrange zachowywał się tak przez tak długi czas, że nie widzę, co kupi nam zatwierdzenie patcha tak późno."(2.7 było prawie na tym punkt.)

Tymczasem:

Pierwotnie, xrange był obiektem niezupełnie sekwencyjnym. Jak 3.1 docs mówią:

Obiekty Range zachowują się bardzo słabo: obsługują tylko indeksowanie, iterację i funkcję len.

To nie do końca prawda; obiekt xrange faktycznie obsługiwał kilka innych rzeczy, które przychodzą automatycznie z indeksowaniem i len,* W tym __contains__ (poprzez wyszukiwanie liniowe). Ale nikt nie pomyślał, że warto je robić pełne sekwencje w tym czasie.

Następnie, w ramach implementacji abstrakcyjnych klas bazowych PEP, ważne było, aby dowiedzieć się, które typy wbudowane powinny być oznaczone jako implementujące które ABCs i xrange/range twierdził, że zaimplementował collections.Sequence, mimo że nadal obsługiwał tylko to samo "bardzo małe zachowanie". Nikt nie zauważył tego problemu, dopóki numer 9213 . Łatka do tego problemu nie tylko dodała index i count do 3.2 range, ale także ponownie zadziałała zoptymalizowana __contains__ (która dzieli tę samą matematykę z index i jest bezpośrednio używana przez count).**ta zmiana weszła również na 3.2 i nie została przeniesiona do 2.x, ponieważ "jest to poprawka, która dodaje nowe metody". (W tym momencie, 2.7 był już po RC status.)

Były dwie szanse na przywrócenie tej optymalizacji do 2.7, ale obie zostały odrzucone.


* w rzeczywistości otrzymujesz nawet iterację za darmo z len i indeksowaniem, ale w 2.3 xrange obiekty mają własny iterator. Który następnie przegrali w 3.x, który używa tego samego typu listiterator co list.

** pierwsza wersja faktycznie go zaimplementowała i pomyliła szczegóły-np. dałaby Ci MyIntSubclass(2) in range(5) == False. Jednak zaktualizowana wersja poprawki Daniela Stutzbacha przywróciła większość poprzedniego kodu, włączając w to powrót do ogólnego, powolnego _PySequence_IterSearch, którego przed 3.2 range.__contains__ domyślnie używała, gdy optymalizacja nie ma zastosowania.

 82
Author: abarnert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-06 22:32:12

Inne odpowiedzi już dobrze to wyjaśniły, ale chciałbym zaproponować kolejny eksperyment ilustrujący naturę obiektów range:

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

Jak widać, obiekt range jest obiektem, który zapamiętuje swój zakres i może być używany wiele razy( nawet podczas iteracji nad nim), a nie tylko jednorazowym generatorem.

 36
Author: Stefan Pochmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-22 12:18:24

Chodzi o leniwe podejście do oceny i dodatkową optymalizację range. Wartości w zakresach nie muszą być obliczane do rzeczywistego użycia, a nawet dalsze ze względu na dodatkową optymalizację.

Przy okazji twoja liczba całkowita nie jest taka duża, rozważ sys.maxsize

sys.maxsize in range(sys.maxsize) jest dość szybki

Ze względu na optymalizację - łatwo porównać daną liczbę całkowitą tylko z min i max zakresu.

Ale:

float(sys.maxsize) in range(sys.maxsize) jest dość powolny .

(w tym przypadku nie ma optymalizacji w range, więc ponieważ otrzymamy nieoczekiwany float, python porówna wszystkie liczby)

 4
Author: Sławomir Lenart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-16 10:47:58