Tengo entendido que la range()
función, que en realidad es un tipo de objeto en Python 3 , genera su contenido sobre la marcha, similar a un generador.
Siendo este el caso, hubiera esperado que la siguiente línea tomara una cantidad excesiva de tiempo, porque para determinar si 1 billón está en el rango, se tendrían que generar un billón de valores:
1000000000000000 in range(1000000000000001)
Además: parece que no importa cuántos ceros agregue, el cálculo lleva más o menos la misma cantidad de tiempo (básicamente instantáneo).
También he intentado cosas como esta, pero el cálculo sigue siendo casi instantáneo:
1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens
¡Si trato de implementar mi propia función de rango, el resultado no es tan bueno!
def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return
¿Qué está range()
haciendo el objeto debajo del capó que lo hace tan rápido?
La respuesta de Martijn Pieters fue elegida por su integridad, pero también puede ver la primera respuesta de abarnert para una buena discusión sobre lo que significa range
ser una secuencia completa en Python 3, y alguna información / advertencia sobre la inconsistencia potencial para la __contains__
optimización de la función en las implementaciones de Python . La otra respuesta de abarnert entra en más detalles y proporciona enlaces para aquellos interesados en la historia detrás de la optimización en Python 3 (y la falta de optimización xrange
en Python 2). Las respuestas por poke y por wim proporcionan el código fuente C relevante y explicaciones para aquellos que estén interesados.
fuente
bool
olong
tipo, con otros tipos de objetos se volverá loco. Prueba con:100000000000000.0 in range(1000000000000001)
range
es un generador?xrange
lo mismo que python3range
?xrange()
objetos @Superbest no tienen ningún__contains__
método, por lo que la verificación de elementos debe recorrer todos los elementos. Además, hay algunos otros cambios enrange()
, al igual que apoya el corte en lonchas (que a su vez devuelve unrange
objeto) y ahora también tienecount
yindex
métodos para que sea compatible concollections.Sequence
ABC.Respuestas:
El
range()
objeto Python 3 no produce números de inmediato; Es un objeto de secuencia inteligente que produce números a pedido . Todo lo que contiene son sus valores de inicio, parada y paso, luego, a medida que itera sobre el objeto, se calcula el siguiente entero en cada iteración.El objeto también implementa el
object.__contains__
gancho y calcula si su número es parte de su rango. El cálculo es una operación (casi) de tiempo constante * . Nunca es necesario escanear todos los enteros posibles en el rango.De la
range()
documentación del objeto :Entonces, como mínimo, su
range()
objeto haría:Todavía faltan varias cosas que un
range()
soporte real (como los métodos.index()
o.count()
, el hash, la prueba de igualdad o el corte), pero deberían darle una idea.También simplifiqué la
__contains__
implementación para centrarme solo en pruebas de enteros; Si le da a unrange()
objeto real un valor no entero (incluidas las subclases deint
), se inicia una exploración lenta para ver si hay una coincidencia, al igual que si utiliza una prueba de contención en una lista de todos los valores contenidos. Esto se hizo para continuar admitiendo otros tipos numéricos que simplemente admiten pruebas de igualdad con enteros, pero no se espera que también admitan la aritmética de enteros. Vea el problema original de Python que implementó la prueba de contención.* Tiempo casi constante porque los enteros de Python son ilimitados y, por lo tanto, las operaciones matemáticas también crecen en el tiempo a medida que N crece, lo que hace que sea una operación O (log N). Como todo se ejecuta en código C optimizado y Python almacena valores enteros en fragmentos de 30 bits, se quedará sin memoria antes de ver algún impacto en el rendimiento debido al tamaño de los enteros involucrados aquí.
fuente
__getitem__
y__len__
, la__iter__
implementación es realmente innecesaria.xrangeiterator
se agregó un especial específicamente porque no fue lo suficientemente rápido. Y luego en algún lugar en 3.x (no estoy seguro de si era 3.0 o 3.2) fue arrojado y usan el mismolistiterator
tipo quelist
usa.def __init__(self, *start_stop_step)
y lo analizaría desde allí; La forma en que los argumentos están etiquetados ahora es un poco confusa. Sin embargo, +1; todavía definitivamente explicaste el comportamiento.range
es anterior a*args
(mucho menos laargclinic
API que permite que las funciones de C-API tengan firmas completas de Python). Algunas otras funciones antiguas (y algunas funciones más nuevas, comoxrange
,slice
yitertools.islice
, por coherencia) funcionan de la misma manera, pero en su mayor parte, Guido y el resto de los desarrolladores centrales parecen estar de acuerdo con usted. Los documentos 2.0+ incluso describen a susrange
amigos como si fueran sobrecargas de estilo C ++ en lugar de mostrar la firma confusa real.argclinic
discusión, cuando Nick Coghlan ideó una manera de permitir definirrange
sin ambigüedades: "Por favor, no facilite que la gente copie mi peor decisión de diseño". Entonces, estoy bastante seguro de que él está de acuerdo en querange
es confuso como está escrito.El malentendido fundamental aquí es pensar que
range
es un generador. No es. De hecho, no es ningún tipo de iterador.Puedes decir esto con bastante facilidad:
Si fuera un generador, iterarlo una vez lo agotaría:
Lo que en
range
realidad es, es una secuencia, como una lista. Incluso puedes probar esto:Esto significa que tiene que seguir todas las reglas de ser una secuencia:
La diferencia entre ay
range
alist
es que arange
es una secuencia perezosa o dinámica ; que no recuerda todos sus valores, sólo recuerda sustart
,stop
ystep
, y crea los valores de la demanda en__getitem__
.(Como nota al margen, si lo hace
print(iter(a))
, notará querange
usa el mismolistiterator
tipo quelist
. ¿Cómo funciona? Alistiterator
no usa nada especial,list
excepto por el hecho de que proporciona una implementación de C__getitem__
, por lo que funciona bien pararange
también.)Ahora, no hay nada que diga que
Sequence.__contains__
tiene que ser tiempo constante; de hecho, para ejemplos obvios de secuencias comolist
, no lo es. Pero no hay nada que diga que no puede ser. Y es más fácil de implementarrange.__contains__
simplemente verificarlo matemáticamente ((val - start) % step
pero con cierta complejidad adicional para lidiar con los pasos negativos) que generar y probar todos los valores, entonces, ¿por qué no debería hacerlo de la mejor manera?Pero no parece haber nada en el lenguaje que garantice que esto suceda. Como señala Ashwini Chaudhari, si le da un valor no integral, en lugar de convertir a entero y hacer la prueba matemática, recurrirá a iterar todos los valores y compararlos uno por uno. Y solo porque las versiones CPython 3.2+ y PyPy 3.x contienen esta optimización, y es una buena idea obvia y fácil de hacer, no hay razón para que IronPython o NewKickAssPython 3.x no puedan dejarla de lado. (Y, de hecho, CPython 3.0-3.1 no lo incluyó).
Si
range
realmente fuera un generador,my_crappy_range
entonces, no tendría sentido probar de__contains__
esta manera, o al menos la forma en que tiene sentido no sería obvio. Si ya había iterado los primeros 3 valores, ¿sigue1
siendoin
el generador? ¿Deben las pruebas1
hacer que itere y consuma todos los valores hasta1
(o hasta el primer valor>= 1
)?fuente
range
list
tuple
cuenta ya que aparece (junto con y ) como un tipo de secuencia .xrange
es una secuencia. Ver 2.7 docs . De hecho, siempre fue casi una secuencia..__iter__()
método que devolverá un iterador. A su vez, solo se puede usar una vez.range
no está comprobando tipos cuando no es un número entero, ya que siempre es posible que un tipo tenga un tipo__eq__
compatibleint
. Claro,str
obviamente no funcionará, pero no querían ralentizar las cosas al verificar explícitamente todos los tipos que no pueden estar allí (y después de todo, unastr
subclase podría anular__eq__
y estar contenida en elrange
).¡Usa la fuente , Luke!
En CPython,
range(...).__contains__
(un contenedor de métodos) eventualmente delegará en un cálculo simple que verifica si el valor puede estar en el rango. La razón de la velocidad aquí es que estamos usando un razonamiento matemático sobre los límites, en lugar de una iteración directa del objeto de rango . Para explicar la lógica utilizada:start
ystop
, yPor ejemplo,
994
está enrange(4, 1000, 2)
porque:4 <= 994 < 1000
y(994 - 4) % 2 == 0
.El código C completo se incluye a continuación, que es un poco más detallado debido a la administración de memoria y los detalles de conteo de referencias, pero la idea básica está ahí:
La "carne" de la idea se menciona en la línea :
Como nota final, mire la
range_contains
función en la parte inferior del fragmento de código. Si la verificación de tipo exacta falla, entonces no usamos el algoritmo inteligente descrito, sino que recurrimos a una tonta búsqueda de iteración del rango usando_PySequence_IterSearch
! Puede verificar este comportamiento en el intérprete (estoy usando v3.5.0 aquí):fuente
Para agregar a la respuesta de Martijn, esta es la parte relevante de la fuente (en C, ya que el objeto de rango está escrito en código nativo):
Entonces, para los
PyLong
objetos (que estáint
en Python 3), usará larange_contains_long
función para determinar el resultado. Y esa función esencialmente comprueba siob
está en el rango especificado (aunque parece un poco más complejo en C).Si no es un
int
objeto, vuelve a iterar hasta que encuentre el valor (o no).Toda la lógica podría traducirse a pseudo-Python de esta manera:
fuente
Si se pregunta por qué se agregó esta optimización
range.__contains__
y por qué no se agregóxrange.__contains__
en 2.7:Primero, como descubrió Ashwini Chaudhary, el número 1766304 se abrió explícitamente para optimizar
[x]range.__contains__
. Se aceptó un parche para esto y se registró en 3.2 , pero no se agregó a 2.7 porque "xrange se ha comportado así durante tanto tiempo que no veo lo que nos compra para enviar el parche tan tarde". (2.7 estaba casi fuera en ese punto).Mientras tanto:
Originalmente,
xrange
era un objeto que no era de secuencia completa. Como dicen los documentos 3.1 :Esto no era del todo cierto; un
xrange
objeto en realidad admite algunas otras cosas que vienen automáticamente con la indexación y *len
, incluido (a través de la búsqueda lineal). Pero nadie pensó que valía la pena hacerlas secuencias completas en ese momento.__contains__
Luego, como parte de la implementación de la PEP de las clases base abstractas , fue importante determinar qué tipos incorporados deberían marcarse como implementando qué ABC, y
xrange
/ orange
según afirmaron implementarcollections.Sequence
, a pesar de que todavía manejaba el mismo "comportamiento muy pequeño". Nadie notó ese problema hasta el número 9213 . El parche para ese problema no solo se agregóindex
ycount
en 3.2range
, también reestructuró el optimizado__contains__
(que comparte las mismas matemáticasindex
y lo usa directamentecount
). ** Este cambio también fue para 3.2, y no fue retrocedido a 2.x, porque "es una corrección de errores que agrega nuevos métodos". (En este punto, 2.7 ya había pasado el estado de RC).Por lo tanto, hubo dos oportunidades para que esta optimización se transfiriera a 2.7, pero ambos fueron rechazados.
* De hecho, incluso obtienes iteraciones gratis solo con indexación, pero en 2.3 los
xrange
objetos tienen un iterador personalizado.** La primera versión en realidad lo reimplementó y se equivocaron los detalles, por ejemplo, te daría
MyIntSubclass(2) in range(5) == False
. Pero la versión actualizada del parche de Daniel Stutzbach restauró la mayor parte del código anterior, incluido el retroceso al genérico, lento_PySequence_IterSearch
que pre-3.2range.__contains__
estaba usando implícitamente cuando la optimización no se aplica.fuente
xrange.__contains__
, parece que no lo respaldaron en Python 2 solo para dejar un elemento de sorpresa para los usuarios y ya era demasiado tarde o_O. El parchecount
y se agregó más tarde. Archivo en ese momento: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.cindex
2to3
de un código de versión dual con la ayuda de bibliotecas comosix
, por eso obtuvimos cosas comodict.viewkeys
esa que nadie usará nunca), y hubo algunos cambios que llegaron demasiado tarde en 3.2, pero en su mayor parte 2.7 fue un lanzamiento impresionante "último 2.x nunca".Las otras respuestas ya lo explicaron bien, pero me gustaría ofrecer otro experimento que ilustre la naturaleza de los objetos de rango:
Como puede ver, un objeto de rango es un objeto que recuerda su rango y se puede usar muchas veces (incluso al iterar sobre él), no solo un generador de una sola vez.
fuente
Se trata de un enfoque perezoso para la evaluación y una optimización adicional de
range
. Los valores en rangos no necesitan ser calculados hasta el uso real, o incluso más debido a una optimización adicional.Por cierto, tu número entero no es tan grande, considera
sys.maxsize
sys.maxsize in range(sys.maxsize)
es bastante rápidodebido a la optimización: es fácil comparar un entero dado solo con el rango mínimo y máximo.
pero:
Decimal(sys.maxsize) in range(sys.maxsize)
es bastante lento .(en este caso, no hay optimización
range
, por lo que si Python recibe un decimal inesperado, Python comparará todos los números)Debe conocer los detalles de la implementación, pero no debe confiar en ellos, ya que esto puede cambiar en el futuro.
fuente
float(sys.maxsize) != sys.maxsize)
aunquesys.maxsize-float(sys.maxsize) == 0
.TL; DR
El objeto devuelto por
range()
es en realidad unrange
objeto. Este objeto implementa la interfaz del iterador para que pueda iterar sobre sus valores secuencialmente, al igual que un generador, una lista o una tupla.Pero también implementa la
__contains__
interfaz, que en realidad es lo que se llama cuando aparece un objeto en el lado derecho delin
operador. El__contains__()
método devuelvebool
si el elemento en el lado izquierdo delin
está o no en el objeto. Dado que losrange
objetos conocen sus límites y zancadas, esto es muy fácil de implementar en O (1).fuente
Tomemos un ejemplo, 997 está dentro del rango (4, 1000, 3) porque:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
fuente
Pruebe
x-1 in (i for i in range(x))
conx
valores grandes , que utilizan una comprensión del generador para evitar invocar larange.__contains__
optimización.fuente