Entendo que a função range()
, que na verdade é um tipo de objeto no Python 3 , gera seu conteúdo em tempo real, semelhante a um gerador.
Sendo este o caso, eu teria esperado que a linha a seguir levasse uma quantidade excessiva de tempo, porque para determinar se 1 quadrilhão está no intervalo, um quadrilhão de valores teria que ser gerado:
1000000000000000 in range(1000000000000001)
Além disso: parece que, não importa quantos zeros eu acrescente, o cálculo mais ou menos leva a mesma quantidade de tempo (basicamente instantâneo).
Eu também tentei coisas assim, mas o cálculo ainda é quase instantâneo:
1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens
Se eu tentar implementar minha própria função de faixa, o resultado não é tão bom !!
def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return
O que o objeto range()
está fazendo sob o capô, o que o torna tão rápido?
A resposta de Martijn Pieters foi escolhida pela sua inteireza, mas também ver a primeira resposta de abarnert para uma boa discussão do que significa para range
ser um full-fledge sequence em Python 3, e algumas informações/avisos sobre possíveis inconsistências para a otimização de função __contains__
através de implementações em Python. outra resposta de abarnert entra em mais detalhes e fornece links para aqueles interessados na história por trás da otimização em Python 3 (e falta de otimização de xrange
em Python 2). Respostas por poke e por wim fornecer o código fonte relevante C e explicações para aqueles que estão interessados.
O objeto Python 3 range()
não produz números imediatamente; é um objeto de sequência inteligente que produz números sob demanda. Tudo o que ele contém são seus valores de início, parada e etapa e, conforme você percorre o objeto, o próximo inteiro é calculado a cada iteração.
O objeto também implementa o gancho object.__contains__
, e calculates se o seu número fizer parte de seu intervalo. O cálculo é uma operação de tempo constante O(1). Nunca é necessário verificar todos os inteiros possíveis no intervalo.
Da documentação do objeto range()
:
A vantagem do tipo
range
sobre umalist
ouTuple
regular é que um objeto de intervalo sempre terá a mesma quantidade (pequena) de memória, independentemente do tamanho do intervalo que representa (pois armazena apenas os valoresstart
,stop
estep
, calculando itens individuais e sub-intervalos conforme necessário).
Então, no mínimo, seu objeto range()
faria:
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
Isso ainda está faltando várias coisas que um range()
real suporta (como os métodos .index()
ou .count()
, hashing, teste de igualdade ou fatiamento), mas deve lhe dar uma idéia.
Eu também simplifiquei a implementação de __contains__
para focar somente em testes inteiros; se você der a um objeto range()
real um valor não-inteiro (incluindo subclasses de int
), uma varredura lenta será iniciada para ver se há uma correspondência, como se você usasse um teste de contenção contra uma lista de todos os valores contidos. Isso foi feito para continuar a oferecer suporte a outros tipos numéricos que apenas suportam testes de igualdade com números inteiros, mas também não devem suportar a aritmética inteira. Veja o original problema em Python que implementou o teste de contenção.
No CPython, range(...).__contains__
(um wrapper de método) acabará delegando a um cálculo simples que verifica se o valor pode estar no intervalo. A razão para a velocidade aqui é que estamos usando o raciocínio matemático sobre os limites, em vez de uma iteração direta do objeto de intervalo. Para explicar a lógica usada:
start
e stop
ePor exemplo, 994
está em range(4, 1000, 2)
porque:
4 <= 994 < 1000
e(994 - 4) % 2 == 0
.O código C completo está incluído abaixo, que é um pouco mais detalhado por causa do gerenciamento de memória e dos detalhes da contagem de referência, mas a ideia básica está lá:
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);
}
A "carne" da ideia é mencionada em a linha :
/* result = ((int(ob) - start) % step) == 0 */
Como nota final - observe a função range_contains
na parte inferior do snippet de código. Se a verificação do tipo exato falhar, então não usaremos o algoritmo inteligente descrito, em vez disso, voltaremos para uma pesquisa de iteração estúpida do intervalo usando _PySequence_IterSearch
! Você pode verificar esse comportamento no interpretador (estou usando a v3.5.0 aqui):
>>> 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)
Para adicionar à resposta de Martijn, esta é a parte relevante de a fonte (em C, como o objeto de intervalo é escrito em código nativo):
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);
}
Portanto, para objetos PyLong
(que é int
no Python 3), ele usará a função range_contains_long
para determinar o resultado. E essa função essencialmente verifica se ob
está no intervalo especificado (embora pareça um pouco mais complexo em C).
Se não for um objeto int
, voltará a iterar até encontrar o valor (ou não).
Toda a lógica poderia ser traduzida para pseudo-Python assim:
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
Se você está se perguntando por que esta otimização foi adicionada ao range.__contains__
, e por que ela não foi adicionada ao xrange.__contains__
em 2.7:
Primeiro, como Ashwini Chaudhary descobriu, edição 1766304 foi aberta explicitamente para otimizar [x]range.__contains__
. Um patch para isso foi aceito e verificado em 3.2 , mas não backported para 2.7 porque "xrange tem se comportado assim há tanto tempo que eu não vejo o que nos compra para cometer o patch tão tarde. " (2.7 estava quase fora naquele momento.)
Enquanto isso:
Originalmente, xrange
era um objeto não-bastante-sequência. Como o 3.1 docs diz:
Objetos de intervalo têm muito pouco comportamento: eles suportam apenas indexação, iteração e a função
len
.
Isso não era bem verdade; um objeto xrange
na verdade suportava algumas outras coisas que vêm automaticamente com indexação e len
,* incluindo __contains__
(via pesquisa linear). Mas ninguém achou que valeria a pena fazer sequências completas na época.
Então, como parte da implementação do Abstract Base Classes PEP, era importante descobrir quais tipos internos deveriam ser marcados como implementando quais ABCs, e xrange
/range
declarado para implementar collections.Sequence
, mesmo que ainda manipulasse o mesmo "muito pouco comportamento". Ninguém percebeu esse problema até edição 9213 . O patch para esse problema não só adicionou index
e count
a 3.2's range
, ele também re-trabalhou o __contains__
otimizado (que compartilha a mesma matemática com index
, e é usado diretamente pelo count
).** Esta mudança entrou em 3.2 também, e não foi backported para 2.x, porque "é um bugfix que adiciona novos métodos". (Neste ponto, 2,7 já estava passado status rc.)
Então, houve duas chances de obter essa otimização backported para 2.7, mas ambos foram rejeitados.
* Na verdade, você ainda obtém iteração gratuitamente com len
e indexação, mas em 2.3xrange
os objetos possuem um iterador customizado. O que eles perderam em 3.x, que usa o mesmo tipo listiterator
como list
.
** A primeira versão, na verdade, reimplementou-a e ficou com os detalhes errados - por exemplo, ela forneceria MyIntSubclass(2) in range(5) == False
. Mas a versão atualizada do patch de Daniel Stutzbach restaurou a maior parte do código anterior, incluindo o fallback para o _PySequence_IterSearch
genérico e lento que o range.__contains__
pré-3.2 estava usando implicitamente quando a otimização não se aplica.
As outras respostas já explicaram bem, mas gostaria de oferecer outro experimento ilustrando a natureza dos objetos de alcance:
>>> 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]
Como você pode ver, um objeto de intervalo é um objeto que lembra seu alcance e pode ser usado muitas vezes (até mesmo durante iterações), e não apenas um gerador de uma só vez.
É tudo sobre uma abordagem preguiçosa para a avaliação e alguma otimização extra de range
. Valores em intervalos não precisam ser computados até o uso real, ou ainda mais devido à otimização extra.
By the way, seu inteiro não é tão grande, considere sys.maxsize
sys.maxsize in range(sys.maxsize)
é bem rápido
devido à otimização - é fácil comparar dado inteiro apenas com min e max de intervalo.
mas:
float(sys.maxsize) in range(sys.maxsize)
é bem lento .
(nesse caso, não há otimização em range
, portanto, se python receber flutuação inesperada, o python comparará todos os números)
Você deve estar ciente de um detalhe de implementação, mas não deve ser confiável, porque isso pode mudar no futuro.
Aqui está implementação semelhante em C#
. Você pode ver como Contains
feito em O(1) hora.
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;
}
}
O objeto retornado por range()
é na verdade um objeto range
. Este objeto implementa a interface do iterador para que você possa iterar seus valores sequencialmente, assim como um gerador, mas também implementa a interface __contains__
que é realmente chamada quando um objeto aparece no lado direito do operador in
. O método __contains__()
retorna um bool de se o item está ou não no objeto. Como os objetos range
conhecem seus limites e stride, isso é muito fácil de implementar em O (1).