Насколько я понимаю, функция 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()
под капотом, что делает его таким быстрым?
Ответ Мартина Питерса был выбран для его полноты, но также см. первый ответ abarnert для хорошего обсуждения того, что значит range
быть полноценной последовательностью в Python 3, и некоторой информацией / предупреждение о возможной несовместимости для оптимизации __contains__
функций в реализациях Python. другой ответ abarnert посвящен некоторым подробностям и предоставляет ссылки для тех, кто интересуется историей, стоящей за оптимизацией в Python 3 (и не хватает оптимизации xrange
в Python 2). Ответы poke и wim предоставьте соответствующий исходный код C и объяснения для тех, кто заинтересован.
9 ответов
Объект Python 3 range()
не генерирует числа сразу; это умный объект последовательности, который производит числа по требованию . Все, что он содержит, - это ваши значения start, stop и step, а затем при выполнении итерации по объекту вычисляется следующее целое число на каждой итерации.
Объект также реализует object.__contains__
ловушку, и вычисляет , является ли ваш номер частью его диапазона. Расчет - это (почти) операция с постоянным временем * . Нет необходимости сканировать все возможные целые числа в диапазоне.
Из 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, 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-битных блоках, вам не хватит памяти, прежде чем вы заметите какое-либо влияние на производительность из-за размера целых чисел, задействованных здесь.
Если вам интересно почему эта оптимизация была добавлена в range.__contains__
, и почему она не была добавлена в xrange.__contains__
в 2.7:
Во-первых, как обнаружила Эшвини Чаудхари, выпуск 1766304 был явно открыт для оптимизации [x]range.__contains__
. Патч для этого был принят и зарегистрирован для версии 3.2, но не перенесен в версию 2.7, поскольку «xrange вёл себя так долго, что я не вижу, что он покупает, чтобы зафиксировать патч так поздно». (2.7 было почти в тот момент.)
В то же время :
Первоначально xrange
был не совсем последовательным объектом. В документации 3.1 говорится:
Объекты Range имеют очень небольшое поведение: они поддерживают только индексацию, итерацию и функцию
len
.
Это было не совсем так; xrange
объект на самом деле поддерживает несколько других вещей, которые приходят автоматически с индексированием и len
, * , включая __contains__
(через линейный поиск). Но никто не думал, что в то время стоило делать их полными последовательностями.
Затем в рамках реализации абстрактных базовых классов PEP было важно выяснить, какие встроенные типы должны быть помечены как реализующие, какие ABC, и xrange
/ range
заявили, что они реализуют collections.Sequence
, хотя он все еще обрабатывает только то же самое "очень небольшое поведение". Никто не замечал эту проблему до выпуска 9213. Патч для этой проблемы не только добавил index
и count
к 3.2 range
3.2, но и переработал оптимизированный __contains__
(который разделяет ту же математику с {{ X7}} и непосредственно используется count
). ** Это изменение также коснулось 3.2 и не было перенесено в 2.x, потому что" это исправление, которое добавляет новые методы ". (На данный момент 2.7 уже прошел статус rc.)
Таким образом, было две возможности вернуть эту оптимизацию в 2.7, но оба они были отклонены.
* На самом деле, вы даже получаете итерацию бесплатно только при индексировании, но в 2.3 xrange
объекты получили пользовательский итератор.
** Первая версия фактически реализовала его и неправильно указала детали - например, она даст вам MyIntSubclass(2) in range(5) == False
. Но обновленная версия патча Даниэля Штутцбаха восстановила большую часть предыдущего кода, включая откат к универсальному, медленному _PySequence_IterSearch
, который до 3.2 range.__contains__
неявно использовался, когда оптимизация не применяется.
Чтобы добавить к ответу Мартин, это соответствующая часть источника (в 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
Основное недоразумение заключается в том, что мы думаем, что 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
является последовательностью ленивый или динамический ; он не запоминает все свои значения, он просто запоминает свои start
, stop
и step
и создает значения по запросу в __getitem__
.
(В качестве примечания, если вы print(iter(a))
, вы заметите, что range
использует тот же тип listiterator
, что и list
. Как это работает? A {{X4} } не использует ничего особенного в list
, за исключением того факта, что он обеспечивает реализацию C __getitem__
, поэтому он отлично работает и для range
.)
Теперь ничто не говорит о том, что Sequence.__contains__
должен быть постоянным временем - фактически, для очевидных примеров последовательностей, таких как list
, это не так. Но нет ничего, что говорит, что не может быть. И проще реализовать range.__contains__
, чтобы просто проверить его математически ((val - start) % step
, но с некоторой дополнительной сложностью, чтобы справиться с отрицательными шагами), чем на самом деле генерировать и тестировать все значения, так почему shouldn ' t это сделать это лучше?
Но в языке, похоже, нет ничего, что гарантирует , что это произойдет. Как указывает Ашвини Чаудхари, если вы дадите ему нецелое значение, вместо того, чтобы конвертировать в целое число и выполнять математический тест, он вернется к итерации всех значений и сравнивает их одно за другим. И только потому, что версии 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
)?
Другие ответы уже объяснили это хорошо, но я хотел бы предложить еще один эксперимент, иллюстрирующий природу объектов диапазона:
>>> 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
. Значения в диапазонах не нужно вычислять до реального использования или даже из-за дополнительной оптимизации.
Кстати, ваше целое число не такое большое, рассмотрим sys.maxsize
sys.maxsize in range(sys.maxsize)
довольно быстро
Благодаря оптимизации - легко сравнить данное целое число только с минимальным и максимальным диапазоном.
Но:
float(sys.maxsize) in range(sys.maxsize)
довольно медленно .
(в этом случае оптимизация в range
отсутствует, поэтому, если python получает неожиданное число с плавающей запятой, он сравнивает все числа)
Вы должны знать о деталях реализации, но на них не следует полагаться, потому что это может измениться в будущем.
Вот реализация в C#
. Вы можете увидеть, как Contains
работает за O (1) раз.
public struct Range
{
private readonly int _start;
private readonly int _stop;
private readonly int _step;
// other members omitted for brevity
public bool Contains(int number)
{
// precheck - if the number is not in a valid step point, return false
// for example, if start=5, step=10, stop=1000, it is obvious that 163 is not in this range (due to remainder)
if ((_start % _step + _step) % _step != (number % _step + _step) % _step)
return false;
// with the help of step sign, we can check borders in linear manner
int s = Math.Sign(_step);
// no need if/else to handle both cases - negative and positive step
return number * s >= _start * s && number * s < _stop * s;
}
}
TL ; DR
Объект, возвращаемый range()
, на самом деле является range
объектом. Этот объект реализует интерфейс итератора, так что вы можете последовательно перебирать его значения, как генератор, список или кортеж.
Но он также реализует {{X0} } интерфейс, который фактически вызывается, когда объект появляется справа от оператора in
. Метод __contains__()
возвращает bool
того, находится ли элемент в левой части in
в объекте. Поскольку range
объекты знают свои границы и шаг, это очень легко реализовать в O (1).
Используйте источник, Люк!
В CPython range(...).__contains__
(обертка метода) в конечном итоге делегирует простой расчет, который проверяет, может ли значение находиться в диапазоне. Причина скорости в том, что мы используем математическое обоснование границ, а не прямую итерацию объекта диапазона . Для объяснения используемой логики:
- Убедитесь, что число находится между
start
иstop
, и - Убедитесь, что значение шага не «перешагивает» наш номер.
Например, 994
находится в range(4, 1000, 2)
, потому что:
4 <= 994 < 1000
и(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)
Похожие вопросы
Связанные вопросы
Новые вопросы
python
Python — это мультипарадигмальный многоцелевой язык программирования с динамической типизацией. Он предназначен для быстрого изучения, понимания и использования, а также обеспечивает чистый и унифицированный синтаксис. Обратите внимание, что Python 2 официально не поддерживается с 01.01.2020. Если у вас есть вопросы о версии Python, добавьте тег [python-2.7] или [python-3.x]. При использовании варианта Python (например, Jython, PyPy) или библиотеки (например, Pandas, NumPy) укажите это в тегах.