¿Por qué no hay función xrange en Python3?


273

Recientemente comencé a usar Python3 y me duele la falta de xrange.

Ejemplo simple:

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()

Los resultados son, respectivamente:

1) 1.53888392448 2) 3.215819835662842

¿Porqué es eso? Quiero decir, ¿por qué se ha eliminado xrange? Es una gran herramienta para aprender. Para los principiantes, como yo, como todos estábamos en algún momento. ¿Por qué eliminarlo? ¿Alguien puede señalarme la PEP adecuada? No puedo encontrarla.

Salud.


231
rangeen Python 3.x es xrangede Python 2.x. De hecho, Python 2.x's rangefue eliminado.
Anorov

27
PD, nunca deberías tener tiempo time. Además de ser más fácil de usar y más difícil de equivocarse, y de repetir las pruebas por usted, timeitse ocupa de todo tipo de cosas que no recordará, ni sabrá cómo cuidar (como deshabilitar el GC), y puede usar un reloj con miles de veces mejor resolución.
abarnert

77
Además, ¿por qué estás probando el tiempo para filtrar el rangesobre x%4 == 0? ¿Por qué no solo probar list(xrange())vs list(range()), para que haya tan poco trabajo extraño como sea posible? (Por ejemplo, ¿cómo sabe que 3.x no funciona x%4más lentamente?) De hecho, ¿por qué está creando una gran listcantidad de memoria, que implica una gran cantidad de asignación de memoria (que, además de ser lenta, también es increíblemente variable) ?
abarnert

55
Consulte docs.python.org/3.0/whatsnew/3.0.html , sección "Vistas e iteradores en lugar de listas": "range () ahora se comporta como xrange () solía comportarse, excepto que funciona con valores de tamaño arbitrario. Este último ya no existe." Entonces, rango ahora devuelve un iterador. iter(range)Es redundante.
ToolmakerSteve

9
Lo sentimos, me di cuenta que citar el documento de cambio no lo hace cegadoramente obvio. Para cualquier otra persona que esté confundida y no quiera leer la respuesta largamente aceptada y todos sus comentarios: Donde sea que esté usando xrange en python 2, use range en python 3. Hace lo que xrange solía hacer, que es devolver un iterador Si necesita los resultados en una lista, hágalo list(range(..)). Eso es equivalente al rango de python 2. O para decirlo de otra manera: a xrange se le ha cambiado el nombre de rango, porque es el mejor valor predeterminado; no era necesario tener ambos, hazlo list(range)si realmente necesitas una lista. .
ToolmakerSteve

Respuestas:


175

Algunas medidas de rendimiento, utilizando en timeitlugar de intentar hacerlo manualmente time.

Primero, Apple 2.7.2 de 64 bits:

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

Ahora, python.org 3.3.0 de 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

Al parecer, 3.x rangerealidad es un poco más lento que 2.x xrange. Y la xrangefunción del OP no tiene nada que ver con eso. (No es sorprendente, ya que __iter__no es probable que una llamada única a la ranura sea visible entre 10000000 llamadas a lo que ocurra en el bucle, pero alguien lo mencionó como una posibilidad).

Pero es solo un 30% más lento. ¿Cómo llegó el OP 2 veces más lento? Bueno, si repito las mismas pruebas con Python de 32 bits, obtengo 1.58 vs. 3.12. Entonces, supongo que este es otro de esos casos en los que 3.x ha sido optimizado para un rendimiento de 64 bits en formas que perjudican a 32 bits.

pero, realmente importa? Mira esto, con 3.3.0 64 bits nuevamente:

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

Por lo tanto, construir el listtoma más del doble de tiempo que la iteración completa.

Y en cuanto a "consume muchos más recursos que Python 2.6+", según mis pruebas, parece que un 3.x rangetiene exactamente el mismo tamaño que un 2.x xrange, e, incluso si fuera 10 veces más grande, construir la lista innecesaria sigue siendo aproximadamente 10000000x un problema mayor que cualquier cosa que la iteración de rango pueda hacer.

¿Y qué hay de un forbucle explícito en lugar del bucle C dentro 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

Entonces, casi tanto tiempo perdido en la fordeclaración como en el trabajo real de iterar el range.

Si le preocupa optimizar la iteración de un objeto de rango, probablemente esté buscando en el lugar equivocado.


Mientras tanto, sigues preguntando por qué xrange se eliminó, no importa cuántas veces la gente le diga lo mismo, pero lo repetiré nuevamente: no se eliminó: se cambió el nombre a range, y el 2.x rangees lo que se eliminó.

Aquí hay alguna prueba de que el rangeobjeto 3.3 es un descendiente directo del xrangeobjeto 2.x (y no de la rangefunción 2.x ): la fuente de 3.3range y 2.7xrange . Incluso puede ver el historial de cambios (vinculado, creo, al cambio que reemplazó la última instancia de la cadena "xrange" en cualquier parte del archivo).

Entonces, ¿por qué es más lento?

Bueno, por un lado, han agregado muchas características nuevas. Por otro lado, han realizado todo tipo de cambios en todo el lugar (especialmente dentro de la iteración) que tienen efectos secundarios menores. Y ha habido mucho trabajo para optimizar dramáticamente varios casos importantes, incluso si a veces pesimiza ligeramente los casos menos importantes. Agregue todo esto, y no me sorprende que iterar lo rangemás rápido posible ahora sea un poco más lento. Es uno de esos casos menos importantes en los que nadie se preocuparía lo suficiente. Es probable que nadie tenga un caso de uso real en el que esta diferencia de rendimiento sea el punto clave en su código.


Pero es solo un 30% más lento. Aún más lento, pero un gran compañero de respuesta, algo en lo que pensar. Sin embargo, no responde mi pregunta: ¿por qué se eliminó xrange? Piénselo de esta manera: si tuviera una aplicación dependiente del rendimiento basada en multiprocesamiento sabiendo cuánta cola necesita consumir cada vez, ¿el 30% marcaría la diferencia o no? Verás, dices que no importa, pero cada vez que uso el rango escucho que el sonido de un gran y angustioso ventilador significa que la CPU está encendida, lo peor, mientras que xrange no lo hace. Piensa en ello;)
catalesia

9
@catalesia: Una vez más, no se eliminó, solo se renombró range. El rangeobjeto en 3.3 es un descendiente directo del xrangeobjeto en 2.7, no de la rangefunción en 2.7. Es como preguntar mientras itertools.imapse eliminó a favor de map. No hay respuesta, porque tal cosa no sucedió.
abarnert

1
@catalesia: Presumiblemente, los cambios menores en el rendimiento no son el resultado de una decisión directa de diseño para hacer que los rangos sean más lentos, sino un efecto secundario de 4 años de cambios en Python que han hecho muchas cosas más rápido, algunas cosas un poco más lentas (y otras cosas más rápido en x86_64 pero más lento en x86, o más rápido en algunos casos de uso pero más lento en otros, etc.). Probablemente, a nadie le preocupaba una diferencia del 30% en ambos sentidos en cuanto al tiempo que lleva iterar un rangerato sin hacer nada más.
abarnert

1
"Probablemente, a nadie le preocupaba una diferencia del 30% en ambos sentidos en cuanto al tiempo que toma iterar un rango sin hacer nada más ". Exactamente.
catalesia

18
@catalesia: Sí, exactamente. Pero pareces pensar que eso significa lo contrario de lo que dice. No es un caso de uso que a nadie le importe, por lo que nadie se dio cuenta de que se volvió un 30% más lento. ¿Y qué? Si puede encontrar un programa de la vida real que se ejecute más lentamente en Python 3.3 que en 2.7 (o 2.6) debido a esto, la gente se preocupará. Si no puedes, no lo harán, y tú tampoco deberías hacerlo.
abarnert

141

El rango de Python3 es el xrange de Python2. No hay necesidad de rodearlo. Para obtener una lista real en Python3, debe usarlist(range(...))

Si quieres algo que funcione con Python2 y Python3, prueba esto

try:
    xrange
except NameError:
    xrange = range

1
A veces necesitas un código que funcione tanto en Python 2 como en 3. Esta es una buena solución.
Greg Glockner

3
El problema es que con esto, el código que usa ambos rangey xrangese comportará de manera diferente. No es suficiente hacer esto, también habría que asegurarse de no asumir nunca que rangeestá devolviendo una lista (como lo haría en Python 2).
LangeHaare

Puedes usar xrange de este proyecto. Hay una futurizeherramienta para convertir automáticamente su código fuente: python-future.org/…
guettli

17

El rangetipo de Python 3 funciona igual que el de Python 2 xrange. No estoy seguro de por qué está viendo una desaceleración, ya que el iterador devuelto por su xrangefunción es exactamente lo que obtendría si iterararange directamente.

No puedo reproducir la desaceleración en mi sistema. Así es como lo probé:

Python 2, con 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, con rangees un poco más 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

Hace poco aprendí que el rangetipo de Python 3 tiene algunas otras características interesantes, como el soporte para cortar: ¡ range(10,100,2)[5:25:5]es range(20, 60, 10)!


Quizás la desaceleración proviene de la búsqueda de lo nuevo xrangetantas veces, ¿o se hace solo una vez?
askewchan

¿Un iterador realmente aumenta la velocidad de todos modos? Pensé que solo había salvado la memoria.
askewchan

3
@catalesia Creo que el punto aquí es que xrangese no se elimina, simplemente cambió el nombre .
askewchan

1
@Blckknght: Saludos, pero todavía apesta tener una explicación como: "Establecer literales y comprensiones [19] [20] [hecho] {x} significa conjunto ([x]); {x, y} significa conjunto ([ x, y]). {F (x) para x en S si P (x)} significa conjunto (F (x) para x en S si P (x)). NB. {rango (x)} significa conjunto ( [rango (x)]), NO establecido (rango (x)). No hay un literal para un conjunto vacío; use set () (o {1} y {2} :-). No hay un congelado literal; también lo son rara vez se necesita " .
catalesia

3
La mayor victoria en 3.x range, en lo que a mí respecta, es el tiempo constante __contains__. Los novatos solían escribir 300000 in xrange(1000000)y eso hizo que iterara todo xrange(o al menos el primer 30%), por lo que tuvimos que explicar por qué era una mala idea, a pesar de que se ve tan pitón. Ahora, es pitónico.
abarnert

1

Una forma de arreglar su código python2 es:

import sys

if sys.version_info >= (3, 0):
    def xrange(*args, **kwargs):
        return iter(range(*args, **kwargs))

1
El punto está en python3 xrange no está definido, por lo que el código heredado que usó xrange se rompe.
Andrew Paté

no, simplemente defina range = xrangecomo está en el comentario de @John La Roy
mimi.vx

@ mimi.vx No estoy seguro de que range = xrange funcione en Python3 porque xrange no está definido. Mi comentario se refiere al caso en el que tiene un código antiguo que contiene llamadas xrange Y está tratando de ejecutarlo en python3.
Andrew Paté

1
Ah, mi mal ... xrange = range...
cambié

range ES un iterador, y de todos modos, esta sería una idea terrible, incluso si no lo fuera, porque primero tiene que desempaquetar todo el rango y pierde las ventajas de usar un iterador para este tipo de cosas. Entonces, la respuesta correcta no es "rango = xrange" es "xrange = range"
Shayne

0

xrange de Python 2 es un generador e implementa un iterador, mientras que el rango es solo una función. En Python3 no sé por qué se dejó caer la xrange.


No, el rango no es un interador. No puede hacer next () con esta estructura. Para más información, puedes consultar aquí treyhunner.com/2018/02/python-range-is-not-an-iterator
Michel Fernandes

Muchas gracias por la aclaración. Pero volveré a exponer la intención del comentario original, y es que PY3 range()es el equivalente de PY2 xrange(). Y así en PY3 xrange()es redundante.
Stephen Rauch

-2

comp: ~ $ python Python 2.7.6 (predeterminado, 22 de junio de 2015, 17:58:13) [GCC 4.8.2] en linux2

>>> import timeit
>>> timeit.timeit("[x for x in xrange(1000000) if x%4]",number=100)

5.656799077987671

>>> timeit.timeit("[x for x in xrange(1000000) if x%4]",number=100)

5.579368829727173

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=100)

21.54827117919922

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=100)

22.014557123184204

Con timeit number = 1 param:

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=1)

0.2245171070098877

>>> timeit.timeit("[x for x in xrange(1000000) if x%4]",number=1)

0.10750913619995117

comp: ~ $ python3 Python 3.4.3 (predeterminado, 14 de octubre de 2015, 20:28:29) [GCC 4.8.4] en Linux

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=100)

9.113872020003328

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=100)

9.07014398300089

Con timeit number = 1,2,3,4 param funciona de forma rápida y lineal:

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=1)

0.09329321900440846

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=2)

0.18501482300052885

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=3)

0.2703447980020428

>>> timeit.timeit("[x for x in range(1000000) if x%4]",number=4)

0.36209142999723554

Entonces parece que si medimos 1 ciclo de ciclo de ejecución como timeit.timeit ("[x para x en el rango (1000000) si x% 4]", número = 1) (como realmente usamos en código real) python3 funciona lo suficientemente rápido, pero en bucles repetidos, python 2 xrange () gana en velocidad contra range () desde python 3.


pero esto es por el lenguaje mismo ... nada que ver con xrange / range.
mimi.vx
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.