Perché l'intervallo (inizio, fine) non include la fine?


305
>>> range(1,11)

ti dà

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

Perché non 1-11?

Hanno appena deciso di farlo in modo casuale o ha un valore che non vedo?


11
leggi Dijkstra, ewd831
SilentGhost il

11
Fondamentalmente stai scegliendo un set di bug off-by-one per un altro. È più probabile che un set causi la chiusura anticipata dei loop, mentre è probabile che l'altro causi un'eccezione (o buffer overflow in altre lingue). Dopo aver scritto un sacco di codice, vedrai che la scelta del comportamento range()ha un senso molto più spesso
John La Rooy,


35
@unutbu Quell'articolo di Djikstra è spesso citato in questo argomento ma qui non fornisce nulla di valore, le persone lo usano solo come appello all'autorità. L'unica pseudo-ragione pertinente che fornisce alla domanda del PO è che capisce che includere il limite superiore diventa "innaturale" e "brutto" nel caso particolare in cui la sequenza è vuota - è una posizione totalmente soggettiva e facilmente contestabile, quindi non porta molto al tavolo. L '"esperimento" con Mesa non ha molto valore senza conoscere i loro particolari vincoli o metodi di valutazione.
Sundar - Ripristina Monica il

6
@andreasdr Ma anche se l'argomento cosmetico è valido, l'approccio di Python non introduce un nuovo problema di leggibilità? Nell'inglese di uso comune il termine "intervallo" implica che qualcosa varia da qualcosa a qualcosa, come un intervallo. Quel len (list (range (1,2))) restituisce 1 e len (list (range (2)) restituisce 2) è qualcosa che devi davvero imparare a digerire.
Armin,

Risposte:


245

Perché è più comune chiamare range(0, 10)che restituisce [0,1,2,3,4,5,6,7,8,9]che contiene 10 elementi uguali len(range(0, 10)). Ricorda che i programmatori preferiscono l'indicizzazione basata su 0.

Inoltre, considera il seguente frammento di codice comune:

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

Potresti vedere che se range()si arrivasse esattamente a len(li)questo sarebbe problematico? Il programmatore avrebbe bisogno di sottrarre in modo esplicito 1. La presente segue anche la tendenza comune di programmatori preferendo for(int i = 0; i < 10; i++)sopra for(int i = 0; i <= 9; i++).

Se stai chiamando l'intervallo con un inizio di 1 frequentemente, potresti voler definire la tua funzione:

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

48
Se questo fosse il ragionamento, i parametri non sarebbero range(start, count)?
Mark Ransom,

3
@shogun Il valore iniziale predefinito è 0, cioè range(10)è equivalente a range(0, 10).
Moinudin,

4
Il tuo range1non funzionerà con intervalli che hanno una dimensione del gradino diversa da 1.
dimo414,

6
Spieghi che l'intervallo (x) dovrebbe iniziare con 0 e x sarà la "lunghezza dell'intervallo". OK. Ma non hai spiegato perché l'intervallo (x, y) dovrebbe iniziare con xe terminare con y-1. Se il programmatore desidera un for-loop con un intervallo compreso tra 1 e 3, deve aggiungere esplicitamente 1. Si tratta davvero di convenienza?
Armin,

7
for i in range(len(li)):è piuttosto un antipasto. Uno dovrebbe usare enumerate.
han

27

Sebbene ci siano alcune utili spiegazioni algoritmiche qui, penso che possa aiutare ad aggiungere alcuni semplici ragionamenti sulla "vita reale" sul perché funziona in questo modo, che ho trovato utile quando ho presentato l'argomento ai giovani nuovi arrivati:

Con qualcosa come 'range (1,10)' la confusione può derivare dal pensare che la coppia di parametri rappresenti "inizio e fine".

In realtà è start e "stop".

Ora, se fosse il valore di "fine" allora sì, ci si potrebbe aspettare che il numero sarebbe stato incluso come la voce finale della sequenza. Ma non è la "fine".

Altri erroneamente chiamano quel parametro "count" perché se usi sempre 'range (n)' allora, ovviamente, itera 'n' volte. Questa logica si interrompe quando si aggiunge il parametro start.

Quindi il punto chiave è ricordare il suo nome: " stop ". Ciò significa che è il punto in cui, una volta raggiunta, l'iterazione si interromperà immediatamente. Non dopo quel punto.

Pertanto, sebbene "start" rappresenti effettivamente il primo valore da includere, al raggiungimento del valore "stop" esso "si interrompe" anziché continuare a elaborare "anche quello" prima di arrestarsi.

Un'analogia che ho usato per spiegare questo ai bambini è che, ironicamente, si comporta meglio dei bambini! Non si ferma dopo che dovrebbe - si ferma immediatamente senza finire quello che stava facendo. (Ottengono questo;))

Un'altra analogia: quando si guida un'auto non si passa un segnale di stop / yield / "cedere" e si finisce con esso seduto da qualche parte accanto o dietro l'auto. Tecnicamente non l'hai ancora raggiunto quando ti fermi. Non è incluso nelle "cose ​​che hai passato nel tuo viaggio".

Spero che questo aiuti a spiegare a Pythonitos / Pythonitas!


Questa spiegazione è più intuitiva. Grazie
Fred,

La spiegazione dei bambini è semplicemente divertente!
Antony Hatchkins,

1
Stai cercando di mettere il rossetto su un maiale. La distinzione tra "stop" e "end" è assurda. Se vado da 1 a 7, non ho superato 7. È semplicemente un difetto di Python avere convenzioni diverse per le posizioni di inizio e fine. In altre lingue, comprese quelle umane, "da X a Y" significa "da X a Y". In Python, "X: Y" significa "X: Y-1". Se hai una riunione dalle 9 alle 11, dici alla gente che è dalle 9 alle 12 o dalle 8 alle 11?
bzip2,

24

Le gamme esclusive offrono alcuni vantaggi:

Per prima cosa ogni elemento in range(0,n)è un indice valido per elenchi di lunghezza n.

Ha anche range(0,n)una lunghezza di n, non n+1quale sarebbe un intervallo inclusivo.


18

Funziona bene in combinazione con indicizzazione a base zero e len(). Ad esempio, se in un elenco xsono presenti 10 voci , queste sono numerate da 0 a 9. range(len(x))ti dà 0-9.

Naturalmente, le persone ti diranno che è più Pythonic da fare for item in xo for index, item in enumerate(x)piuttosto che for i in range(len(x)).

Anche l'affettare funziona in questo modo: foo[1:4]sono gli articoli 1-3 di foo(tenendo presente che l'articolo 1 è in realtà il secondo oggetto a causa dell'indicizzazione in base zero). Per coerenza, entrambi dovrebbero funzionare allo stesso modo.

Lo penso come: "il primo numero che vuoi, seguito dal primo numero che non vuoi". Se vuoi 1-10, il primo numero che non vuoi è 11, quindi è range(1, 11).

Se diventa ingombrante in una particolare applicazione, è abbastanza facile scrivere una piccola funzione di aiuto che aggiunge 1 all'indice finale e chiama range().


1
Concordare sull'affettare. w = 'abc'; w[:] == w[0:len(w)]; w[:-1] == w[0:len(w)-1];
kevpie il

def full_range(start,stop): return range(start,stop+1) ## helper function
nobar,

forse l'esempio enumerato dovrebbe essere letto for index, item in enumerate(x)per evitare confusione
afferma il

@seans Grazie, risolto.
kindall

12

È anche utile per dividere gli intervalli; range(a,b)può essere suddiviso in range(a, x)e range(x, b), mentre con un intervallo inclusivo scriverebbe x-1o x+1. Mentre raramente hai bisogno di dividere gli intervalli, tendi a dividere gli elenchi abbastanza spesso, il che è uno dei motivi per cui un elenco l[a:b]include l'elemento a-th ma non il b-th. Quindi rangeavere la stessa proprietà lo rende piacevolmente coerente.


11

La lunghezza dell'intervallo è il valore superiore meno il valore inferiore.

È molto simile a qualcosa del tipo:

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

in un linguaggio di tipo C.

Piace anche la gamma di Ruby:

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

Tuttavia, Ruby riconosce che molte volte vorrai includere il valore del terminale e offre la sintassi alternativa:

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

17
Gah! Io non uso Ruby, ma posso immaginare che 1..10vs 1...10essere difficile distinguere tra quando si legge il codice!
Moinudin,

6
@marcog - quando sai che esistono le due forme i tuoi occhi si sintonizzano con la differenza :)
Skilldrick

11
Il range operator di Ruby è perfettamente intuitivo. La forma più lunga ti dà la sequenza più breve. tosse
Russell Borogove il

4
@Russell, forse 1 ............ 20 dovrebbe fornire lo stesso intervallo di 1..10. Ora sarebbe uno zucchero sintattico che vale la pena cambiare. ;)
kevpie il

4
@Russell Il punto in più spreme l'ultimo oggetto fuori dal range :)
Skilldrick

5

Fondamentalmente nei tempi di range(n)iterazione di Python n, che è di natura esclusiva ed è per questo che non dà l'ultimo valore quando viene stampato, possiamo creare una funzione che dia valore inclusivo significa che stamperà anche l'ultimo valore menzionato nell'intervallo.

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

Considera il codice

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

L'idea è che ottieni un elenco di lunghezza y-x, che puoi (come vedi sopra) scorrere più volte.

Leggi i documenti di Python per range - considerano l'iterazione for-loop la base utente principale.


1

In molti casi è solo più conveniente ragionare.

Fondamentalmente, potremmo pensare a un intervallo come a un intervallo tra starte end. Se start <= endla lunghezza dell'intervallo tra loro è end - start. Se lenfosse effettivamente definito come lunghezza, avresti:

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

Tuttavia, contiamo gli interi inclusi nell'intervallo invece di misurare la lunghezza dell'intervallo. Per mantenere vera la proprietà di cui sopra, dovremmo includere uno degli endpoint ed escludere l'altro.

Aggiungere il stepparametro è come introdurre un'unità di lunghezza. In tal caso, ti aspetteresti

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

per lunghezza. Per ottenere il conteggio, basta usare la divisione intera.


Queste difese dell'incoerenza di Python sono esilaranti. Se volessi l'intervallo tra due numeri, perché dovrei usare la sottrazione per ottenere la differenza anziché l'intervallo? Non è coerente utilizzare convenzioni di indicizzazione diverse per le posizioni iniziale e finale. Perché dovresti scrivere "5:22" per ottenere le posizioni da 5 a 21?
bzip2

Non è di Python, è abbastanza comune su tutta la linea. In C, Java, Ruby, lo chiami
Arseny il

Volevo dire che è comune per l'indicizzazione, non che le altre lingue abbiano necessariamente lo stesso tipo esatto di oggetto
Arseny
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.