¿Por qué el rango (inicio, fin) no incluye final?

305
>>> range(1,11)

te dio

[1,2,3,4,5,6,7,8,9,10]

¿Por qué no 1-11?

¿Simplemente decidieron hacerlo así al azar o tiene algún valor que no estoy viendo?

MetaGuru
fuente
11
leer Dijkstra, ewd831
SilentGhost
11
Básicamente, está eligiendo un conjunto de errores off-by-one para otro. Es más probable que un conjunto provoque que sus bucles terminen antes de tiempo, el otro puede causar una excepción (o desbordamiento del búfer en otros idiomas). Una vez que haya escrito un montón de código, verá que la elección del comportamiento range()tiene mucho más sentido
John La Rooy
32
Enlace a Dijkstra, ewd831: cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
unutbu
35
@unutbu Ese artículo de Djikstra se cita con frecuencia en este tema pero no proporciona nada de valor aquí, la gente lo usa solo como un llamado a la autoridad. La única pseudo razón relevante que da para la pregunta del OP es que siente que incluir el límite superior se vuelve "antinatural" y "feo" en el caso particular donde la secuencia está vacía: esa es una posición totalmente subjetiva y fácilmente discutible en contra, así que no trae mucho a la mesa. El "experimento" con Mesa tampoco tiene mucho valor sin conocer sus limitaciones particulares o métodos de evaluación.
sundar - Restablecer Monica
66
@andreasdr Pero incluso si el argumento cosmético es válido, ¿el enfoque de Python no introduce un nuevo problema de legibilidad? En inglés de uso común, el término "rango" implica que algo varía de algo a algo, como un intervalo. Que len (list (range (1,2))) devuelve 1 y len (list (range (2))) devuelve 2 es algo que realmente debes aprender a digerir.
armin

Respuestas:

245

Porque es más común llamar a range(0, 10)return [0,1,2,3,4,5,6,7,8,9]que contiene 10 elementos que son iguales len(range(0, 10)). Recuerde que los programadores prefieren la indexación basada en 0.

Además, considere el siguiente fragmento de código común:

for i in range(len(li)):
    pass

¿Podría ver que si range()fuera exactamente len(li)eso sería problemático? El programador tendría que restar explícitamente 1. Esto también sigue la tendencia común de los programadores prefieren for(int i = 0; i < 10; i++)más for(int i = 0; i <= 9; i++).

Si llama al rango con un inicio de 1 con frecuencia, es posible que desee definir su propia función:

>>> def range1(start, end):
...     return range(start, end+1)
...
>>> range1(1, 10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
moinudin
fuente
48
Si ese fuera el razonamiento, ¿no serían los parámetros range(start, count)?
Mark Ransom
3
@shogun El valor inicial predeterminado es 0, range(10)es decir, es equivalente a range(0, 10).
moinudin
44
Su range1no va a funcionar con rangos que tienen un tamaño de paso diferente a 1.
dimo414
66
Explica que el rango (x) debe comenzar con 0 yx será la "longitud del rango". OKAY. Pero no explicó por qué el rango (x, y) debería comenzar con x y terminar con y-1. Si el programador quiere un ciclo for con un rango de 1 a 3, tiene que agregar explícitamente 1. ¿Se trata realmente de conveniencia?
armin
77
for i in range(len(li)):Es más bien un antipatrón. Uno debería usar enumerate.
Hans
27

Aunque hay algunas explicaciones algorítmicas útiles aquí, creo que puede ayudar agregar un razonamiento simple de la `` vida real '' sobre por qué funciona de esta manera, lo que he encontrado útil al presentar el tema a los recién llegados:

Con algo como 'rango (1,10)' puede surgir confusión al pensar que un par de parámetros representa el "inicio y el final".

En realidad es comenzar y "parar".

Ahora, si fuera el valor "final", entonces, sí, es de esperar que ese número se incluya como la entrada final en la secuencia. Pero no es el "fin".

Otros llaman erróneamente a ese parámetro "cuenta" porque si solo usa 'range (n)', entonces, por supuesto, itera 'n' veces. Esta lógica se rompe cuando agrega el parámetro de inicio.

Entonces, el punto clave es recordar su nombre: " stop ". Eso significa que es el punto en el que, cuando se alcanza, la iteración se detendrá de inmediato. No después de ese punto.

Entonces, si bien "inicio" representa el primer valor que se incluirá, al alcanzar el valor de "detención" se "rompe" en lugar de continuar procesando "ese también" antes de detenerse.

Una analogía que he usado para explicar esto a los niños es que, irónicamente, ¡se comporta mejor que los niños! No se detiene después de lo que se supone que debe hacerlo; se detiene inmediatamente sin terminar lo que estaba haciendo. (Consiguen esto;))

Otra analogía: cuando conduces un automóvil no pasas una señal de alto / rendimiento / 'ceder' y terminas sentado en algún lugar al lado o detrás de tu automóvil. Técnicamente todavía no lo has alcanzado cuando te detienes. No está incluido en las "cosas que pasó en su viaje".

¡Espero que algo de eso ayude a explicar a Pythonitos / Pythonitas!

dingles
fuente
Esta explicación es más intuitiva. Gracias
Fred
¡La explicación de los niños es simplemente graciosa!
Antony Hatchkins
1
Estás tratando de poner lápiz labial en un cerdo. La distinción entre "alto" y "fin" es absurda. Si paso del 1 al 7, no he pasado el 7. Es simplemente una falla de Python tener diferentes convenciones para las posiciones de inicio y parada. En otros idiomas, incluidos los humanos, "de X a Y" significa "de X a Y". En Python, "X: Y" significa "X: Y-1". Si tiene una reunión del 9 al 11, ¿le dice a la gente que es del 9 al 12 o del 8 al 11?
bzip2
24

Las gamas exclusivas tienen algunos beneficios:

Por un lado, cada elemento range(0,n)es un índice válido para listas de longitud n.

También range(0,n)tiene una longitud de n, n+1que no sería un rango inclusivo.

sepp2k
fuente
18

Funciona bien en combinación con indexación basada en cero y len(). Por ejemplo, si tiene 10 elementos en una lista x, están numerados del 0 al 9. range(len(x))te da 0-9.

Por supuesto, la gente te dirá que es más Pythonic hacer for item in xo for index, item in enumerate(x)no for i in range(len(x)).

La división también funciona de esa manera: foo[1:4]son los elementos 1-3 de foo(teniendo en cuenta que el elemento 1 es en realidad el segundo elemento debido a la indexación basada en cero). Por coherencia, ambos deberían funcionar de la misma manera.

Pienso en ello como: "el primer número que desea, seguido del primer número que no desea". Si quieres 1-10, el primer número que no quieres es 11, entonces es range(1, 11).

Si se vuelve engorroso en una aplicación en particular, es bastante fácil escribir una pequeña función auxiliar que agregue 1 al índice final y las llamadas range().

un poco
fuente
1
De acuerdo en cortar. w = 'abc'; w[:] == w[0:len(w)]; w[:-1] == w[0:len(w)-1];
kevpie
def full_range(start,stop): return range(start,stop+1) ## helper function
nobar
tal vez el ejemplo de enumeración debería leer for index, item in enumerate(x)para evitar confusiones
marns
@seans Gracias, arreglado.
poco
12

También es útil para dividir rangos; range(a,b)se puede dividir en range(a, x)y range(x, b), mientras que con un rango inclusivo escribiría x-1o x+1. Si bien rara vez necesita dividir rangos, tiende a dividir las listas con bastante frecuencia, que es una de las razones por las que dividir una lista l[a:b]incluye el elemento a-th pero no el b-th. Entonces rangetener la misma propiedad lo hace muy consistente.

xuanji
fuente
11

La longitud del rango es el valor superior menos el valor inferior.

Es muy similar a algo como:

for (var i = 1; i < 11; i++) {
    //i goes from 1 to 10 in here
}

en un lenguaje de estilo C.

También como la gama de Ruby:

1...11 #this is a range from 1 to 10

Sin embargo, Ruby reconoce que muchas veces querrás incluir el valor del terminal y ofrece la sintaxis alternativa:

1..10 #this is also a range from 1 to 10
Drilldrick
fuente
17
Gah! No consumo Ruby, pero puedo imaginar que 1..10vs 1...10siendo difícil distinguir entre el momento de lectura de códigos!
moinudin
66
@marcog: cuando sabes que existen las dos formas, tus ojos se sintonizan con la diferencia :)
Skilldrick
11
El operador de rango de Ruby es perfectamente intuitivo. La forma más larga te da la secuencia más corta. tos
Russell Borogove
44
@Russell, tal vez 1 ............ 20 debería dar el mismo rango que 1..10. Ahora eso sería algo de azúcar sintáctico por el que vale la pena cambiar. ;)
kevpie
44
@Russell El punto extra exprime el último elemento fuera del rango :)
Skilldrick
5

Básicamente, en python range(n)itera nveces, lo cual es de naturaleza exclusiva, por eso no da el último valor cuando se imprime, podemos crear una función que proporcione un valor inclusivo, lo que significa que también imprimirá el último valor mencionado en el rango.

def main():
    for i in inclusive_range(25):
        print(i, sep=" ")


def inclusive_range(*args):
    numargs = len(args)
    if numargs == 0:
        raise TypeError("you need to write at least a value")
    elif numargs == 1:
        stop = args[0]
        start = 0
        step = 1
    elif numargs == 2:
        (start, stop) = args
        step = 1
    elif numargs == 3:
        (start, stop, step) = args
    else:
        raise TypeError("Inclusive range was expected at most 3 arguments,got {}".format(numargs))
    i = start
    while i <= stop:
        yield i
        i += step


if __name__ == "__main__":
    main()
Ashish Dixit
fuente
4

Considera el código

for i in range(10):
    print "You'll see this 10 times", i

La idea es que obtenga una lista de longitud y-x, que puede (como puede ver arriba) repetir.

Lea sobre los documentos de Python para el rango: consideran que la iteración de bucle es el caso de uso principal.

Robert
fuente
1

Es más conveniente razonar sobre esto en muchos casos.

Básicamente, podríamos pensar en un rango como un intervalo entre starty end. Si start <= end, la longitud del intervalo entre ellos es end - start. Si en lenrealidad se definiera como la longitud, tendrías:

len(range(start, end)) == start - end

Sin embargo, contamos los enteros incluidos en el rango en lugar de medir la longitud del intervalo. Para mantener verdadera la propiedad anterior, debemos incluir uno de los puntos finales y excluir el otro.

Agregar el stepparámetro es como introducir una unidad de longitud. En ese caso, esperarías

len(range(start, end, step)) == (start - end) / step

por longitud. Para obtener el recuento, solo usa la división de enteros.

Arsenio
fuente
Estas defensas de la inconsistencia de Python son hilarantes. Si quisiera el intervalo entre dos números, ¿por qué usaría la resta para obtener la diferencia en lugar del intervalo? No es coherente utilizar diferentes convenciones de indexación para las posiciones de inicio y finalización. ¿Por qué necesitarías escribir "5:22" para obtener las posiciones 5 a 21?
bzip2
No es Python, es bastante común en todos los ámbitos. En C, Java, Ruby, lo que sea
Arseny
Quise decir que es común para la indexación, no que los otros idiomas tengan necesariamente el mismo tipo exacto de objeto
Arseny