Насколько я понимаю, 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
быть полноценной последовательностью в Python 3, и некоторую информацию / предупреждение относительно потенциальной несогласованности для __contains__
оптимизации функций во всех реализациях Python. , Другой ответ abarnert входит в некоторые подробности и предоставляет ссылки для тех, кто интересуется историей, стоящей за оптимизацией в Python 3 (и отсутствием оптимизации xrange
в Python 2). Ответы poke и wim предоставляют соответствующий исходный код на C и объяснения для тех, кто заинтересован.
источник
bool
илиlong
типом, с другими типами объектов это сойдет с ума. Попробуйте:100000000000000.0 in range(1000000000000001)
range
это генератор?xrange
же, как Python3range
?xrange()
объектов @Superbest нет__contains__
метода, поэтому проверка элементов должна проходить по всем элементам. Кроме того, есть несколько других измененийrange()
, например, он поддерживает нарезку (которая снова возвращаетrange
объект), а также имеетcount
иindex
методы для обеспечения его совместимости сcollections.Sequence
ABC.Ответы:
Объект Python 3
range()
не генерирует числа сразу; это умный объект последовательности, который производит числа по требованию . Все, что он содержит, - это ваши значения start, stop и step, а затем при выполнении итерации по объекту вычисляется следующее целое число на каждой итерации.Объект также реализует
object.__contains__
ловушку и вычисляет, является ли ваш номер частью его диапазона. Расчет - это (почти) операция с постоянным временем * . Нет необходимости сканировать все возможные целые числа в диапазоне.Из
range()
объектной документации :Итак, как минимум, ваш
range()
объект будет делать:В нем по-прежнему отсутствуют некоторые вещи, которые реально
range()
поддерживаются (такие как методы.index()
или.count()
, хэширование, тестирование на равенство или срезы), но они должны дать вам представление.Я также упростил
__contains__
реализацию, чтобы сосредоточиться только на целочисленных тестах; если вы даете реальномуrange()
объекту нецелое значение (включая подклассыint
), запускается медленное сканирование, чтобы увидеть, есть ли совпадение, так же, как если бы вы использовали тест сдерживания для списка всех содержащихся значений. Это было сделано для продолжения поддержки других числовых типов, которые, как оказалось, поддерживают тестирование на равенство с целыми числами, но не ожидается, что они также будут поддерживать целочисленную арифметику. Посмотрите оригинальную версию Python, в которой реализован тест на содержание.* Почти постоянное время, потому что целые числа Python не ограничены, и поэтому математические операции также растут во времени с ростом N, что делает эту операцию O (log N). Поскольку все это выполняется в оптимизированном коде C, а Python хранит целочисленные значения в 30-битных блоках, вам не хватит памяти, прежде чем вы заметите какое-либо влияние на производительность из-за размера целых чисел, задействованных здесь.
источник
__getitem__
и__len__
,__iter__
реализация на самом деле не нужна.xrangeiterator
был добавлен специально , потому что не было достаточно быстро. И затем где-то в 3.x (я не уверен, было ли это 3.0 или 3.2), он был брошен, и они используют тот жеlistiterator
тип, которыйlist
использует.def __init__(self, *start_stop_step)
и разобрал его оттуда; способ, которым аргументы помечены теперь, теперь немного сбивает с толку. Тем не менее +1; Вы все еще определенно объяснили поведение.range
старше*args
(гораздо меньшеargclinic
API, который позволяет функциям C-API иметь полные подписи Python). Несколько других старых функций (и несколько более новых функций, напримерxrange
,slice
иitertools.islice
для согласованности) работают аналогичным образом, но, по большей части, Guido и остальные основные разработчики, похоже, согласны с вами. Документы 2.0+ даже описываютrange
и друзей, как если бы они были перегрузками в стиле C ++, а не показывают действительную запутанную подпись.argclinic
дискуссии Гвидо , когда Ник Коглан придумал способ датьrange
однозначное определение : «Пожалуйста, не делайте так, чтобы людям было проще скопировать мое худшее дизайнерское решение». Итак, я уверен, что он согласен, чтоrange
это сбивает с толку, как написано.Основное недоразумение заключается в том, что
range
мы думаем, что это генератор. Это не. На самом деле, это не какой-то итератор.Вы можете сказать это довольно легко:
Если бы это был генератор, повторение его однажды исчерпало бы его:
Что на
range
самом деле, это последовательность, как список. Вы даже можете проверить это:Это означает, что он должен следовать всем правилам последовательности:
Разница между a
range
и alist
состоит в том, что arange
является ленивой или динамической последовательностью; он не помнит все свои ценности, он просто запоминает ееstart
,stop
иstep
, и создает значения по запросу на__getitem__
.(В качестве примечания, если
print(iter(a))
вы заметите, чтоrange
используется тот жеlistiterator
тип, что иlist
. Как это работает? Alistiterator
не использует ничего особенного,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
)?источник
range
перечислено (наряду сlist
иtuple
) как тип последовательности .xrange
тоже последовательность. Смотри 2.7 документа . Фактически, это всегда было почти последовательностью..__iter__()
методом, который будет возвращать итератор. Это в свою очередь может быть использовано только один раз.range
что не проверяет типы, когда это не целое число, поскольку всегда возможно, что у типа есть__eq__
совместимый типint
. Конечно,str
очевидно, что это не сработает, но они не хотели замедлять работу, явно проверяя все типы, которых там нет (и, в конце концов,str
подкласс мог бы переопределить__eq__
и содержаться вrange
).Используйте источник , Люк!
В CPython
range(...).__contains__
(обертка метода) в конечном итоге делегирует простой расчет, который проверяет, может ли значение находиться в диапазоне. Причина скорости здесь в том, что мы используем математическое обоснование границ, а не прямую итерацию объекта диапазона . Для объяснения используемой логики:start
иstop
, иНапример,
994
вrange(4, 1000, 2)
так как :4 <= 994 < 1000
, а также(994 - 4) % 2 == 0
,Полный код C приведен ниже, он немного более подробный из-за деталей управления памятью и подсчета ссылок, но основная идея здесь есть:
«Мясо» идеи упоминается в строке :
В заключение: посмотрите на
range_contains
функцию внизу фрагмента кода. Если точная проверка типа не удалась, мы не используем описанный умный алгоритм, вместо этого возвращаясь к тупому итерационному поиску диапазона, используя_PySequence_IterSearch
! Вы можете проверить это поведение в интерпретаторе (я использую v3.5.0 здесь):источник
Чтобы добавить ответ Martijn, это соответствующая часть источника (в C, поскольку объект диапазона написан в нативном коде):
Таким образом, для
PyLong
объектов (который находитсяint
в Python 3), он будет использоватьrange_contains_long
функцию для определения результата. И эта функция по существу проверяет,ob
находится ли он в указанном диапазоне (хотя в C это выглядит немного сложнее).Если это не
int
объект, он возвращается к итерации, пока не найдет значение (или нет).Вся логика может быть переведена на псевдо-Python следующим образом:
источник
Если вам интересно, почему эта оптимизация была добавлена
range.__contains__
и почему она не была добавленаxrange.__contains__
в 2.7:Во-первых, как обнаружила Эшвини Чаудхари, проблема 1766304 была открыта для оптимизации
[x]range.__contains__
. Патч для этого был принят и проверен на 3.2 , но не перенесен в 2.7, потому что «xrange вел себя так долго, что я не вижу, что он покупает, чтобы зафиксировать патч так поздно». (2.7 было почти в тот момент.)В то же время:
Изначально это
xrange
был не совсем последовательный объект. Как сказано в документах 3.1 :Это было не совсем так;
xrange
объект фактически поддержали несколько других вещей , которые приходят автоматически с индексацией иlen
, * в том числе__contains__
( с помощью линейного поиска). Но никто не думал, что в то время стоило делать их полными последовательностями.Затем, в рамках реализации PEP абстрактных базовых классов , было важно выяснить, какие встроенные типы должны быть помечены как реализующие, какие ABC и
xrange
/range
заявлены для реализацииcollections.Sequence
, даже несмотря на то, что они все еще обрабатывают только то же «очень небольшое поведение». Никто не заметил этой проблемы до выпуска 9213 . Патч для этой проблемы не только добавлен, ноindex
иcount
к 3.2range
, он также переработал оптимизированный__contains__
(который имеет ту же математикуindex
, и непосредственно используетсяcount
). ** Это изменение также коснулось 3.2 и не было перенесено в 2.x, потому что «это исправление, которое добавляет новые методы». (На данный момент 2.7 уже прошел статус rc.)Таким образом, было две возможности вернуть эту оптимизацию в 2.7, но оба они были отклонены.
* На самом деле, вы даже получаете итерацию бесплатно только с индексированием, но в 2.3
xrange
объектах есть пользовательский итератор.** Первая версия фактически реализовала его, и неправильно поняла детали - например, это даст вам
MyIntSubclass(2) in range(5) == False
. Но обновленная версия патча Даниэля Штутцбаха восстановила большую часть предыдущего кода, включая откат к общему медленному,_PySequence_IterSearch
которое до 3.2range.__contains__
использовалось неявно, когда оптимизация не применяется.источник
xrange.__contains__
, похоже, что они не перенесли его в Python 2 просто для того, чтобы оставить элемент неожиданности для пользователей, и было слишком поздно, o_O.count
Иindex
патч был добавлен позже. Файл в то время: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.c2to3
а не через код с двумя версиями с помощью таких библиотек, какsix
, поэтому у нас есть такие вещи,dict.viewkeys
которые никто никогда не будет использовать), и были несколько изменений, которые только что вышли слишком поздно в 3.2, но по большей части 2.7 был довольно впечатляющим "последним 2.x когда-либо" релизом.Другие ответы уже объяснили это хорошо, но я хотел бы предложить еще один эксперимент, иллюстрирующий природу объектов диапазона:
Как вы можете видеть, объект диапазона - это объект, который запоминает свой диапазон и может использоваться много раз (даже при переборе по нему), а не просто одноразовый генератор.
источник
Это все о ленивом подходе к оценке и некоторой дополнительной оптимизации в
range
. Значения в диапазонах не нужно вычислять до реального использования или даже из-за дополнительной оптимизации.Кстати, ваше целое число не такое большое, рассмотрим
sys.maxsize
sys.maxsize in range(sys.maxsize)
довольно быстроблагодаря оптимизации - легко сравнить данное целое число только с минимальным и максимальным диапазоном.
но:
Decimal(sys.maxsize) in range(sys.maxsize)
довольно медленно .(в этом случае оптимизация отсутствует
range
, поэтому, если python получит неожиданное десятичное число, python сравнит все числа)Вы должны знать о деталях реализации, но на них не следует полагаться, потому что это может измениться в будущем.
источник
float(sys.maxsize) != sys.maxsize)
хотяsys.maxsize-float(sys.maxsize) == 0
.TL; DR
Возвращаемый объект на
range()
самом деле являетсяrange
объектом. Этот объект реализует интерфейс итератора, так что вы можете последовательно перебирать его значения, как генератор, список или кортеж.Но он также реализует
__contains__
интерфейс, который на самом деле вызывается, когда объект появляется справа отin
оператора. В__contains__()
методе возвращаетbool
от наличия или отсутствия пункта в левой-сторонеin
находится в объекте. Посколькуrange
объекты знают свои границы и шаг, это очень легко реализовать в O (1).источник
Возьмите пример, 997 находится в диапазоне (4, 1000, 3), потому что:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
источник
Попробуйте
x-1 in (i for i in range(x))
для большихx
значений, которые используют понимание генератора, чтобы избежать вызоваrange.__contains__
оптимизации.источник