Por que não há nenhuma função xrange em Python3?

Recentemente eu comecei a usar Python3 e a's falta de xrange dói.

Exemplo simples:

1) Python2:

from time import time as t
def count():
  st = t()
  [x for x in xrange(10000000) if x%4 == 0]
  et = t()
  print et-st
count()

2) Python3:

from time import time as t

def xrange(x):

    return iter(range(x))

def count():
    st = t()
    [x for x in xrange(10000000) if x%4 == 0]
    et = t()
    print (et-st)
count()

Os resultados são, respectivamente:

1) 1.53888392448 2) 3.215819835662842

Porquê? Quero dizer, porque é que o xrange's foi removido? É uma ferramenta tão boa para aprender. Para os iniciantes, tal como eu, como todos nós fomos em algum momento. Porquê removê-lo? Alguém pode me apontar para o PEP apropriado, eu posso't encontrá-lo.

Saúde.

Solução

Algumas medidas de desempenho, utilizando timeit em vez de tentar fazê-lo manualmente com time.

Primeiro, Apple 2.7.2 64-bit:

In [37]: %timeit collections.deque((x for x in xrange(10000000) if x%4 == 0), maxlen=0)
1 loops, best of 3: 1.05 s per loop

Agora, python.org 3.3.0 64 bits:

In [83]: %timeit collections.deque((x for x in range(10000000) if x%4 == 0), maxlen=0)
1 loops, best of 3: 1.32 s per loop

In [84]: %timeit collections.deque((x for x in xrange(10000000) if x%4 == 0), maxlen=0)
1 loops, best of 3: 1.31 s per loop

In [85]: %timeit collections.deque((x for x in iter(range(10000000)) if x%4 == 0), maxlen=0) 
1 loops, best of 3: 1.33 s per loop

Aparentemente, o 3.x range é realmente um pouco mais lento que o 2.x xrange. E a função OP's xrange não tem nada a ver com isso. (Não é surpreendente, pois uma chamada única para o slot __iter__ é'provavelmente não será visível entre 10000000 chamadas para o que quer que aconteça no laço, mas alguém o mencionou como uma possibilidade).

Mas é apenas 30% mais lento. Como é que o OP ficou 2x tão lento? Bem, se eu repetir os mesmos testes com o Python de 32 bits, eu recebo 1.58 vs. 3.12. Então meu palpite é que este é mais um daqueles casos em que o 3.x foi otimizado para desempenho de 64 bits de maneiras que prejudicam os 32 bits.

Mas será que isso realmente importa? Veja isto, com 3.3.0 de 64 bits novamente:

In [86]: %timeit [x for x in range(10000000) if x%4 == 0]
1 loops, best of 3: 3.65 s per loop

Portanto, construir a "lista" leva mais do dobro do tempo que toda a iteração.

E quanto ao "consome muito mais recursos do que o Python 2.6+" dos meus testes, parece que um 3.x range tem exatamente o mesmo tamanho de um 2.x xrange-e, mesmo que fosse 10x tão grande, construir a lista desnecessária ainda é cerca de 10000000x mais um problema do que qualquer coisa que a iteração de range poderia fazer.

E que tal um laço explícito para em vez do laço C dentro do deque?

In [87]: def consume(x):
   ....:     for i in x:
   ....:         pass
In [88]: %timeit consume(x for x in range(10000000) if x%4 == 0)
1 loops, best of 3: 1.85 s per loop

Assim, quase tanto tempo desperdiçado na declaração "por" como no trabalho real de iteração do "alcance".

Se você'está preocupado em otimizar a iteração de um objeto de intervalo, você'provavelmente está procurando no lugar errado.


Enquanto isso, você continua perguntando por que xrange foi removido, não importa quantas vezes as pessoas te dizem a mesma coisa, mas I'vai repetir novamente: Não foi removido: foi renomeado para "alcance", e o 2.x "alcance" é o que foi removido.

Aqui's alguma prova de que o objeto 3.3 range é um descendente direto do objeto 2.x xrange (e não da função 2.x range): a fonte para 3.3 range e 2.7 xrange. Você pode até mesmo ver o change history (ligado, acredito, à mudança que substituiu a última instância da string "xrange" em qualquer lugar no arquivo).

Então, porque é mais lento?

Bem, para começar, eles'adicionaram muitas novas funcionalidades. Por outro, eles'fizeram todo o tipo de alterações por todo o lado (especialmente dentro da iteração) que têm efeitos secundários menores. E aí'tem havido muito trabalho para otimizar dramaticamente vários casos importantes, mesmo se às vezes pessimiza um pouco os casos menos importantes. Somando tudo isso, e I'não me surpreende que iterar um "intervalo" o mais rápido possível seja agora um pouco mais lento. It'é um daqueles casos menos importantes em que ninguém se importaria o suficiente para se concentrar. Ninguém provavelmente nunca terá um caso de uso na vida real onde essa diferença de performance é o hotspot em seu código.

Comentários (5)

Python3's range is Python2's xrange. Não há necessidade de envolver um iter em torno dele. Para obter uma lista real em Python3, você precisa utilizar list(range(...))

Se você quer algo que funcione com Python2 e Python3, tente isto

try:
    xrange
except NameError:
    xrange = range
Comentários (3)

Python 3's range funciona como Python 2's xrange. I'não tenho certeza porque você'está vendo uma desaceleração, já que o iterador retornado pela sua função xrange é exatamente o que você'obteria se você iterasse sobre range diretamente.

I'não sou capaz de reproduzir a desaceleração no meu sistema. Aqui'é como eu testei:

Python 2, com xrange:

Python 2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import timeit
>>> timeit.timeit("[x for x in xrange(1000000) if x%4]",number=100)
18.631936646865853

Python 3, com range é um pouco mais rápido:

Python 3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import timeit
>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=100)
17.31399508687869

Aprendi recentemente que o Python 3's tipo range tem algumas outras características limpas, como o suporte para fatiar: range(10,100,2)[5:25:5] é range(20, 60, 10)!

Comentários (15)