Entendo que a range()
função, que na verdade é um tipo de objeto no Python 3 , gera seu conteúdo rapidamente, semelhante a um gerador.
Sendo esse o caso, eu esperaria que a linha a seguir levasse um tempo excessivo, porque, para determinar se 1 quadrilhão está no intervalo, seria necessário gerar um quadrilhão de valores:
1000000000000000 in range(1000000000000001)
Além disso: parece que não importa quantos zeros adiciono, o cálculo leva mais ou menos 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 range()
objeto está fazendo sob o capô que o torna tão rápido?
A resposta de Martijn Pieters foi escolhida por sua integralidade, mas também é a primeira resposta de abarnert para uma boa discussão sobre o que significa range
ser uma sequência completa no Python 3 e algumas informações / avisos sobre inconsistência potencial para __contains__
otimização de funções nas implementações do Python . A outra resposta de abarnert entra em mais detalhes e fornece links para os interessados na história por trás da otimização no Python 3 (e falta de otimização xrange
no Python 2). Respostas por cutucar e por wim fornecem o código fonte C relevante e explicações para aqueles que estão interessados.
fonte
bool
oulong
, com outros tipos de objetos ele ficará louco. Tente com:100000000000000.0 in range(1000000000000001)
range
é um gerador?xrange
igual ao Python3range
?xrange()
objetos @Superbest não têm__contains__
método, portanto a verificação do item precisa percorrer todos os itens. Além disso, existem algumas outras mudanças emrange()
, como ele suporta corte (que novamente retorna umrange
objeto) e agora também temcount
eindex
métodos para torná-lo compatível com ocollections.Sequence
ABC.Respostas:
O
range()
objeto Python 3 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 os valores de início, parada e etapa, e à medida que você itera sobre o objeto, o próximo número inteiro é calculado a cada iteração.O objeto também implementa o
object.__contains__
gancho e calcula se o seu número faz parte do seu intervalo. O cálculo é uma operação em tempo constante (próximo) * . Nunca é necessário verificar todos os números possíveis no intervalo.Na
range()
documentação do objeto :Portanto, no mínimo, seu
range()
objeto faria:Isso ainda está faltando várias coisas que um verdadeiro
range()
suportes (como os.index()
ou.count()
métodos, hashing, testes igualdade, ou corte), mas deve dar-lhe uma idéia.Também simplifiquei a
__contains__
implementação para focar apenas em testes inteiros; se você atribuir a umrange()
objeto real um valor não inteiro (incluindo subclasses deint
), uma varredura lenta será iniciada para verificar se há uma correspondência, como se você usasse um teste de contenção em uma lista de todos os valores contidos. Isso foi feito para continuar a oferecer suporte a outros tipos numéricos que, por acaso, oferecem suporte ao teste de igualdade com números inteiros, mas não é esperado que eles também suportem aritmética inteira. Veja o problema original do Python que implementou o teste de contenção.* Tempo quase constante, porque os números inteiros do Python são ilimitados e, portanto, as operações matemáticas também crescem com o tempo, à medida que N cresce, tornando essa operação O (log N). Como tudo é executado no código C otimizado e o Python armazena valores inteiros em partes de 30 bits, você fica sem memória antes de ver qualquer impacto no desempenho devido ao tamanho dos números inteiros envolvidos aqui.
fonte
__getitem__
e__len__
, a__iter__
implementação é realmente desnecessária.xrangeiterator
foi adicionado especificamente porque não era rápido o suficiente. E então em algum lugar no 3.x (não tenho certeza se era 3.0 ou 3.2) foi lançado e eles usam o mesmolistiterator
tipo quelist
usa.def __init__(self, *start_stop_step)
e analisaria a partir daí; a maneira como os argumentos são rotulados agora é agora meio confusa. No entanto, +1; você ainda definitivamente explicou o comportamento.range
é mais antigo que*args
(muito menos aargclinic
API que permite que as funções da C-API tenham assinaturas completas do Python). Algumas outras funções antigas (e algumas funções mais recentes, comoxrange
,,slice
eitertools.islice
, por consistência) funcionam da mesma maneira, mas, na maioria das vezes, Guido e o restante dos desenvolvedores principais parecem concordar com você. Os documentos 2.0+ até descrevemrange
e amigos como se fossem sobrecargas no estilo C ++, em vez de mostrar a assinatura confusa.argclinic
discussão, quando Nick Coghlan encontrou uma maneira de definirrange
sem ambiguidade: "Por favor, não facilite as pessoas copiarem minha pior decisão de design". Então, eu tenho certeza que ele concorda querange
é confuso como está escrito.O mal-entendido fundamental aqui é pensar que
range
é um gerador. Não é. De fato, não é nenhum tipo de iterador.Você pode dizer isso facilmente:
Se fosse um gerador, iterá-lo uma vez o esgotaria:
O que
range
realmente é, é uma sequência, como uma lista. Você pode até testar isso:Isso significa que ele deve seguir todas as regras de ser uma sequência:
A diferença entre a
range
e alist
é que arange
é uma sequência lenta ou dinâmica ; ele não se lembra de todos os seus valores, ele apenas se lembra de seustart
,stop
estep
, e cria os valores sob demanda on__getitem__
.(Como uma observação lateral, se você
print(iter(a))
notar querange
usa o mesmolistiterator
tipo delist
. Como isso funciona? Alistiterator
não usa nada de especial,list
exceto pelo fato de fornecer uma implementação C de__getitem__
, portanto funciona bem pararange
também.)Agora, não há nada que diga que
Sequence.__contains__
deve haver tempo constante - de fato, para exemplos óbvios de sequências comolist
, não é. Mas não há nada que diga que não pode ser. E é mais fácil implementar implementarrange.__contains__
apenas verificá-lo matematicamente ((val - start) % step
mas com alguma complexidade extra para lidar com etapas negativas) do que realmente gerar e testar todos os valores; por que não fazer da melhor maneira?Mas parece não haver nada no idioma que garanta que isso aconteça. Como Ashwini Chaudhari aponta, se você der um valor não integral, em vez de converter para número inteiro e fazer o teste matemático, ele voltará a iterar todos os valores e compará-los um por um. E apenas porque as versões do CPython 3.2+ e PyPy 3.x contêm essa otimização, e é uma boa ideia óbvia e fácil de fazer, não há razão para que o IronPython ou o NewKickAssPython 3.x não possam deixar de fora. (E, de fato, o CPython 3.0-3.1 não o incluiu.)
Se,
range
na verdade, fosse um gerador,my_crappy_range
não faria sentido testar__contains__
dessa maneira, ou pelo menos a maneira como faz sentido não seria óbvia. Se você já iterou os três primeiros valores,1
ainda éin
o gerador? O teste deve1
fazer com que ele itere e consuma todos os valores até1
(ou até o primeiro valor>= 1
)?fonte
range
está listado (junto comlist
etuple
) como um tipo de sequência .xrange
é uma sequência. Veja 2.7 documentos . De fato, sempre foi uma quase sequência..__iter__()
método que retornará um iterador. Por sua vez, pode ser usado apenas uma vez.range
não está verificando tipos quando não é um número inteiro, já que é sempre possível que um tipo tenha um__eq__
que seja compatívelint
. Claro,str
obviamente não vai funcionar, mas eles não queriam desacelerar as coisas, verificando explicitamente todos os tipos que não podem estar lá (e, afinal, umastr
subclasse pode substituir__eq__
e estar contida norange
).Use a fonte , Luke!
No CPython,
range(...).__contains__
(um invólucro de método) acabará delegando um cálculo simples que verifica se o valor pode estar no intervalo. A razão para a velocidade aqui é que estamos usando um raciocínio matemático sobre os limites, em vez de uma iteração direta do objeto range . Para explicar a lógica usada:start
estop
, ePor exemplo,
994
está dentrorange(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 devido ao gerenciamento de memória e aos detalhes da contagem de referência, mas a idéia básica está lá:
A "carne" da ideia é mencionada na linha :
Como nota final - observe a
range_contains
função na parte inferior do trecho de código. Se a verificação exata do tipo falhar, não usaremos o algoritmo inteligente descrito, em vez disso, voltaremos a uma pesquisa de iteração idiota do intervalo usando_PySequence_IterSearch
! Você pode verificar esse comportamento no intérprete (estou usando a v3.5.0 aqui):fonte
Para adicionar à resposta de Martijn, esta é a parte relevante da fonte (em C, como o objeto range é escrito no código nativo):
Portanto, para
PyLong
objetos (que estáint
no Python 3), ele usará arange_contains_long
função para determinar o resultado. E essa função essencialmente verifica seob
está no intervalo especificado (embora pareça um pouco mais complexo em C).Se não for um
int
objeto, ele volta à iteração até encontrar o valor (ou não).Toda a lógica pode ser traduzida para pseudo-Python assim:
fonte
Se você está se perguntando por que essa otimização foi adicionada
range.__contains__
e por que não foi adicionadaxrange.__contains__
no 2.7:Primeiro, como Ashwini Chaudhary descobriu, a edição 1766304 foi aberta explicitamente para otimizar
[x]range.__contains__
. Um patch para isso foi aceito e registrado no 3.2 , mas não foi portado para 2.7 porque "o xrange se comportou assim por tanto tempo que não vejo o que é necessário para cometer o patch tão tarde". (2,7 estava quase fora nesse ponto.)Enquanto isso:
Originalmente,
xrange
era um objeto que não era exatamente uma sequência. Como dizem os documentos 3.1 :Isso não era bem verdade; um
xrange
objeto realmente suporta algumas outras coisas que vêm automaticamente com a indexação elen
, * incluindo__contains__
(por meio de pesquisa linear). Mas ninguém pensou que valia a pena fazer sequências completas na época.Em seguida, como parte da implementação do PEP das Classes base abstratas , era importante descobrir quais tipos internos deveriam ser marcados como implementando quais ABCs e
xrange
/range
alegavam implementarcollections.Sequence
, mesmo que ainda tratasse apenas o mesmo "muito pouco comportamento". Ninguém percebeu esse problema até a edição 9213 . O remendo para o problema que não só adicionadoindex
ecount
a 3,2 darange
, também re-trabalhado a optimizado__contains__
(que partes da mesma matemática comindex
, e é utilizado directamente porcount
). ** Essa alteração também foi para o 3.2 e não foi suportada para 2.x, porque "é uma correção de bug que adiciona novos métodos". (Nesse ponto, 2.7 já tinha passado do status rc.)Portanto, havia duas chances de obter essa otimização backported para 2,7, mas ambas foram rejeitadas.
* De fato, você obtém iteração de graça apenas com a indexação, mas na versão 2.3 os
xrange
objetos têm um iterador personalizado.** A primeira versão realmente a reimplementou e os detalhes foram errados - por exemplo, isso daria a você
MyIntSubclass(2) in range(5) == False
. Mas a versão atualizada de Daniel Stutzbach do patch restaurou a maior parte do código anterior, incluindo o fallback para o genérico, lento_PySequence_IterSearch
que o pré-3.2range.__contains__
estava usando implicitamente quando a otimização não se aplica.fonte
xrange.__contains__
, parece que eles não fizeram o backport para o Python 2 apenas para deixar um elemento de surpresa para os usuários e já era tarde demais o_O. Acount
eindex
remendo foi adicionado mais tarde. Arquivo naquela época: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.c2to3
não via código de versão dupla com a ajuda de bibliotecas comosix
, e é por isso que temos coisas comodict.viewkeys
essas que ninguém nunca vai usar), e havia algumas mudanças que vieram tarde demais no 3.2, mas na maioria das vezes o 2.7 foi um lançamento impressionante "último 2.x de sempre".As outras respostas já explicaram bem, mas eu gostaria de oferecer outro experimento que ilustra a natureza dos objetos de alcance:
Como você pode ver, um objeto de intervalo é um objeto que se lembra do seu alcance e pode ser usado várias vezes (mesmo enquanto iterando sobre ele), não apenas um gerador único.
fonte
É uma abordagem preguiçosa para a avaliação e uma otimização extra do
range
. Os valores nos intervalos não precisam ser calculados até o uso real, ou ainda mais devido à otimização extra.A propósito, seu número inteiro não é tão grande, considere
sys.maxsize
sys.maxsize in range(sys.maxsize)
é bem rápidodevido à otimização - é fácil comparar um número inteiro fornecido apenas com o mínimo e o máximo do intervalo.
mas:
Decimal(sys.maxsize) in range(sys.maxsize)
é bem lento .(nesse caso, não há otimização
range
, portanto, se o python receber um decimal inesperado, o python comparará todos os números)Você deve estar ciente de um detalhe da implementação, mas não deve ser invocado, pois isso pode mudar no futuro.
fonte
float(sys.maxsize) != sys.maxsize)
apesar de tudosys.maxsize-float(sys.maxsize) == 0
.TL; DR
O objeto retornado por
range()
é realmente umrange
objeto. Este objeto implementa a interface do iterador para que você possa iterar sobre seus valores sequencialmente, como um gerador, lista ou tupla.Mas também implementa a
__contains__
interface que é na verdade chamada quando um objeto aparece no lado direito doin
operador. O__contains__()
método retorna abool
se o item no lado esquerdo doin
objeto está ou não . Como osrange
objetos conhecem seus limites e avanços, isso é muito fácil de implementar em O (1).fonte
Tomemos um exemplo, 997 está no intervalo (4, 1000, 3) porque:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
fonte
Tente valores
x-1 in (i for i in range(x))
grandesx
, que usam uma compreensão do gerador para evitar arange.__contains__
otimização.fonte