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?

MetaGuru
fonte
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]
moinudin
fonte
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!

dingles
fonte
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.

sepp2k
fonte
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().

kindall
fonte
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.

xuanji
fonte
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
Skilldrick
fonte
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()
Ashish Dixit
fonte
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.

Robert
fonte
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.

Arseny
fonte
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