Por que o intervalo (início, fim) não inclui final?


305
>>> range(1,11)

da-te

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

Por que não 1-11?

Eles decidiram fazer isso aleatoriamente ou tem algum valor que eu não estou vendo?


11
Leia Dijkstra, ewd831
SilentGhost

11
Basicamente, você está escolhendo um conjunto de erros off-by-one para outro. Um conjunto provavelmente fará com que seus loops sejam encerrados mais cedo, o outro provavelmente causará uma exceção (ou estouro de buffer em outros idiomas). Depois de escrever um monte de código, você verá que a escolha de comportamento range()faz sentido com muito mais frequência
John La Rooy

32
Link para Dijkstra, ewd831: cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
unutbu

35
@unutbu Esse artigo da Djikstra é freqüentemente citado neste tópico, mas não fornece nada de valor aqui, as pessoas o usam apenas como um apelo à autoridade. A única pseudo-razão relevante que ele dá para a pergunta do OP é que ele acha que incluir o limite superior se torna "antinatural" e "feio" no caso específico em que a sequência está vazia - essa é uma posição totalmente subjetiva e é fácil argumentar contra, por isso não traz muito para a mesa. O "experimento" com o Mesa não tem muito valor sem conhecer suas restrições ou métodos de avaliação.
sundar - Restabelece Monica

6
@andreasdr Mas, mesmo que o argumento cosmético seja válido, a abordagem do Python não apresenta um novo problema de legibilidade? No inglês de uso comum, o termo "intervalo" implica que algo varia de algo a algo - como um intervalo. Que len (lista (intervalo (1,2))) retorne 1 e len (lista (intervalo (2))) retorne 2 é algo que você realmente precisa aprender a digerir.
armin 24/07

Respostas:


245

Porque é mais comum chamar range(0, 10)what return [0,1,2,3,4,5,6,7,8,9]que contém 10 elementos iguais len(range(0, 10)). Lembre-se de que os programadores preferem a indexação baseada em 0.

Além disso, considere o seguinte snippet de código comum:

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

Você poderia ver que, se range()fosse exatamente len(li)isso, seria problemático? O programador precisa subtrair explicitamente 1. Isso também segue a tendência comum de programadores preferindo for(int i = 0; i < 10; i++)mais for(int i = 0; i <= 9; i++).

Se você estiver chamando o intervalo com um início de 1 com frequência, convém definir sua própria função:

>>> def range1(start, end):
...     return range(start, end+1)
...
>>> range1(1, 10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

48
Se esse fosse o raciocínio, não seriam os parâmetros range(start, count)?
Mark Ransom

3
@shogun O valor inicial é 0, ou seja, range(10)é equivalente a range(0, 10).
moinudin

4
Você range1não trabalhará com intervalos com um tamanho de etapa diferente de 1.
dimo414

6
Você explica que o intervalo (x) deve começar com 0 e x será o "comprimento do intervalo". ESTÁ BEM. Mas você não explicou por que o intervalo (x, y) deve começar com x e terminar com y-1. Se o programador deseja um loop for com variação de 1 a 3, ele precisa adicionar explicitamente 1. Isso é realmente uma questão de conveniência?
Armin

7
for i in range(len(li)):é um antipadrão. Deve-se usar enumerate.
hans

27

Embora existam algumas explicações algorítmicas úteis aqui, acho que pode ajudar a adicionar alguns argumentos simples da "vida real" sobre o porquê de funcionar dessa maneira, que achei úteis ao apresentar o assunto para jovens recém-chegados:

Com algo como 'range (1,10)', pode surgir confusão ao pensar que um par de parâmetros representa o "começo e o fim".

Na verdade, é iniciar e "parar".

Agora, se fosse o valor "final", sim, você pode esperar que esse número seja incluído como entrada final na sequência. Mas não é o "fim".

Outros chamam erroneamente esse parâmetro de "contagem" porque, se você usar apenas 'range (n)', é claro que itera 'n' vezes. Essa lógica quebra quando você adiciona o parâmetro start.

Portanto, o ponto principal é lembrar o nome: " stop ". Isso significa que é o ponto no qual, quando alcançada, a iteração será interrompida imediatamente. Não depois desse ponto.

Portanto, enquanto "start" realmente representa o primeiro valor a ser incluído, ao atingir o valor "stop", ele "quebra", em vez de continuar processando "esse também" antes de parar.

Uma analogia que usei para explicar isso às crianças é que, ironicamente, é melhor se comportar do que crianças! Não para depois que deveria - para imediatamente sem terminar o que estava fazendo. (Eles entendem isso;))

Outra analogia - quando você dirige um carro, você não passa por um sinal de stop / yield / 'give way' e acaba com ele sentado em algum lugar próximo ou atrás do seu carro. Tecnicamente, você ainda não o alcançou quando para. Não está incluído nas "coisas que você passou na sua jornada".

Espero que isso ajude a explicar o Pythonitos / Pythonitas!


Essa explicação é mais intuitiva. Obrigado
Fred

A explicação das crianças é hilária!
Antony Hatchkins

1
Você está tentando colocar batom em um porco. A distinção entre "parar" e "fim" é absurda. Se eu passo de 1 a 7, não passei por 7. É simplesmente uma falha do Python ter convenções diferentes para as posições de início e parada. Em outros idiomas, incluindo os humanos, "de X a Y" significa "de X a Y". No Python, "X: Y" significa "X: Y-1". Se você tem uma reunião das 9 às 11, você diz às pessoas que são das 9 às 12 ou das 8 às 11?
bzip2

24

Intervalos exclusivos têm alguns benefícios:

Por um lado, cada item range(0,n)é um índice válido para listas de comprimento n.

Também range(0,n)tem um comprimento de n, e não n+1um intervalo inclusivo.


18

Funciona bem em combinação com indexação baseada em zero e len(). Por exemplo, se você tiver 10 itens em uma lista x, eles serão numerados de 0 a 9. range(len(x))dá 0-9.

Obviamente, as pessoas dirão que é mais um Pythonic a fazer for item in xou for index, item in enumerate(x)melhor for i in range(len(x)).

O fatiamento também funciona da seguinte maneira: foo[1:4]são os itens 1-3 de foo(lembrando que o item 1 é realmente o segundo item devido à indexação com base em zero). Por consistência, os dois devem funcionar da mesma maneira.

Penso nisso como: "o primeiro número desejado, seguido pelo primeiro número que você não deseja". Se você quer 1-10, o primeiro número que você não quer é 11, então érange(1, 11) .

Se ficar complicado em um aplicativo específico, é fácil escrever uma pequena função auxiliar que adiciona 1 ao índice e chamadas finais range().


1
Concorde em fatiar. 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

talvez o exemplo enumerar deve ler for index, item in enumerate(x)a confusão evitar
seans

@seans Obrigado, corrigido.
Kindall

12

Também é útil para dividir intervalos; range(a,b)pode ser dividido em range(a, x)e range(x, b), enquanto que com o intervalo inclusivo você escreveria um x-1ou outro x+1. Embora raramente seja necessário dividir intervalos, você tende a dividir listas com bastante frequência, e esse é um dos motivos pelos quais o corte de uma lista l[a:b]inclui o a-ésimo elemento, mas não o a-ésimo. Então, rangeter a mesma propriedade a torna bastante consistente.


11

O comprimento do intervalo é o valor superior menos o valor inferior.

É muito parecido com algo como:

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

em uma linguagem de estilo C.

Também como a linha de Ruby:

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

No entanto, Ruby reconhece que muitas vezes você deseja incluir o valor do terminal e oferece a sintaxe alternativa:

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

17
Gah! Eu não uso Ruby, mas posso imaginar que é difícil distinguir 1..10vs 1...10ao ler código!
moinudin

6
@marcog - quando você sabe que as duas formas existem os olhos sintonizar com a diferença :)
Skilldrick

11
O operador de gama do Ruby é perfeitamente intuitivo. Quanto maior o formato, menor a sequência. tosse
Russell Borogove

4
@ Russell, talvez 1 ............ 20 deva dar o mesmo intervalo que 1..10. Agora, isso seria um pouco de açúcar sintático pelo qual vale a pena mudar. ;)
kevpie

4
@Russell O ponto extra de aperta o último item fora da faixa :)
Skilldrick

5

Basicamente, em range(n)iterações python nvezes, que é de natureza exclusiva e é por isso que não fornece o último valor quando está sendo impresso, podemos criar uma função que fornece valor inclusivo, significa que também imprimirá o último valor mencionado no intervalo.

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

4

Considere o código

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

A idéia é que você obtenha uma lista de comprimento y-x, que você pode (como você vê acima) iterar.

Leia os documentos python para obter o range - eles consideram a iteração de loop for a principal maneira de usar.


1

É apenas mais conveniente argumentar em muitos casos.

Basicamente, poderíamos pensar em um intervalo como um intervalo entre starte end. Se start <= end, a duração do intervalo entre eles é end - start. Se lenfoi realmente definido como o comprimento, você teria:

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

No entanto, contamos os números inteiros incluídos no intervalo em vez de medir a duração do intervalo. Para manter a propriedade acima verdadeira, devemos incluir um dos pontos de extremidade e excluir o outro.

Adicionar o stepparâmetro é como introduzir uma unidade de comprimento. Nesse caso, você esperaria

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

para comprimento. Para obter a contagem, basta usar a divisão inteira.


Essas defesas da inconsistência do Python são hilárias. Se eu quisesse o intervalo entre dois números, por que eu usaria a subtração para obter a diferença em vez do intervalo? É inconsistente usar diferentes convenções de indexação para posições inicial e final. Por que você precisaria escrever "5:22" para obter as posições de 5 a 21?
bzip2

Não é do Python, é bastante comum. Em C, Java, Ruby, o nome dele
Arseny 17/01

Eu quis dizer que é comum a indexação, não que os outros idiomas tenham necessariamente o mesmo tipo exato de objeto.
Arseny
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.