it-swarm.com.ru

Почему "1000000000000000 в диапазоне (1000000000000001)" так быстро в Python 3?

Насколько я понимаю, функция range(), которая на самом деле является тип объекта в Python 3 , генерирует свое содержимое на лету, подобно генератору. 

В этом случае я ожидал, что следующая строка займет неоправданное количество времени, потому что для определения того, находится ли 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() под капотом, что делает его таким быстрым? 


Ответ Martijn Pieters был выбран для его полноты, но также см. Первый ответ abarnert для хорошего обсуждения того, что значит для range быть полноценной sequence в Python 3, а также некоторая информация/предупреждение о возможной несогласованности для оптимизации функции __contains__ во всех реализациях Python. другой ответ abarnert углубляется в некоторые подробности и предоставляет ссылки для тех, кто интересуется историей, стоящей за оптимизацией в Python 3 (и отсутствием оптимизации xrange в Python 2). Ответы by poke и by wim предоставляют соответствующий исходный код C и пояснения для тех, кто заинтересован. 

1572
Rick Teachey

Объект Python 3 range() не генерирует числа сразу; это умный объект последовательности, который производит числа по требованию. Все, что он содержит, это ваши значения начала, остановки и шага, а затем при выполнении итерации по объекту вычисляется следующее целое число на каждой итерации.

Объект также реализует хук object.__contains__ , и вычисляет, если ваш номер является частью его диапазона. Вычисление является операцией O(1) с постоянным временем. Нет необходимости сканировать все возможные целые числа в диапазоне.

Из range() объектной документации :

Преимущество типа range по сравнению с обычными list или Tuple состоит в том, что объект диапазона всегда будет занимать один и тот же (небольшой) объем памяти, независимо от размера представляемого диапазона (поскольку он хранит только значения start, stop и 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

Разница между range и list заключается в том, что range является lazy или dynamic sequence; он не запоминает все свои значения, он просто запоминает свои start, stop и step и создает значения по запросу в __getitem__.

(В качестве примечания: если вы print(iter(a)), вы заметите, что range использует тот же тип listiterator, что и list. Как это работает? listiterator не использует ничего особенного в list, за исключением того факта, что он обеспечивает реализацию на C __getitem__, поэтому он отлично работает и для range.)


Теперь нет ничего, что говорит о том, что Sequence.__contains__ должен быть постоянным временем - фактически, для очевидных примеров последовательностей, таких как list, это не так. Но нет ничего, что говорит это не может быть. И проще реализовать range.__contains__, чтобы просто проверить его математически ((val - start) % step, но с некоторой дополнительной сложностью, чтобы справиться с отрицательными шагами), чем на самом деле генерировать и тестировать все значения, так почему не следует он делает это лучше ?

Но, кажется, в языке нет ничего, что гарантирует это произойдет. Как указывает Ашвини Чаудхари, если вы дадите ему нецелое значение, вместо преобразования в целое число и выполнения математического теста, то вернетесь к итерации всех значений и сопоставлению их одно за другим. И только потому, что версии 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)?

648
abarnert

Используйте источник , Люк!

В 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)
311
wim

Чтобы добавить ответ 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 (который int в Python 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
116
poke

Если вам интересно почему эта оптимизация была добавлена ​​в range.__contains__, и почему она не была добавлена ​​в xrange.__contains__ в 2.7:

Во-первых, как обнаружила Эшвини Чаудхари, выпуск 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 . Патч для этой проблемы не только добавил index и count к 3.2 range, но также переработал оптимизированный __contains__ (который использует ту же математику с index и непосредственно используется count).** Это изменение также вошло в 3.2 и не было перенесено в 2.x, потому что «это исправление, которое добавляет новые методы». (На данный момент 2.7 уже прошел статус rc.)

Таким образом, было две возможности вернуть эту оптимизацию в 2.7, но оба они были отклонены.


* На самом деле, вы даже получаете итерацию бесплатно с len и индексацией, но в 2.3xrange объекты получили собственный итератор. Который они тогда потеряли в 3.x, который использует тот же тип listiterator как list.

** Первая версия фактически реализовала его и неправильно указала детали, например, это дало бы вам MyIntSubclass(2) in range(5) == False. Но обновленная версия патча Даниэля Штутцбаха восстановила большую часть предыдущего кода, включая откат к общему медленному _PySequence_IterSearch, который неявно использовался до 3.2 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]

Как вы можете видеть, объект диапазона - это объект, который запоминает свой диапазон и может использоваться много раз (даже при переборе по нему), а не просто одноразовый генератор.

40
Stefan Pochmann

Все дело в ленивом подходе к оценке и некоторой дополнительной оптимизации range. Значения в диапазонах не нужно вычислять до реального использования или даже дальше из-за дополнительной оптимизации.

Кстати, ваше целое число не такое большое, рассмотрим sys.maxsize

sys.maxsize in range(sys.maxsize) довольно быстро

благодаря оптимизации - легко сравнить данное целое число только с минимальным и максимальным диапазоном.

но:

float(sys.maxsize) in range(sys.maxsize) довольно медленно .

(в этом случае оптимизация в range отсутствует, поэтому, если python получает неожиданное значение с плавающей запятой, python сравнивает все числа)

Вы должны знать о деталях реализации, но на них не следует полагаться, потому что это может измениться в будущем.

11
Sławomir Lenart

Вот аналогичная реализация в C#. Вы можете увидеть, как Contains делается за O(1) время.

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