Dlaczego zakres (początek, koniec) nie obejmuje końca?

305
>>> range(1,11)

daje Ci

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

Dlaczego nie 1-11?

Czy po prostu postanowili zrobić to w ten sposób losowo, czy ma to jakąś wartość, której nie widzę?

MetaGuru
źródło
11
przeczytaj Dijkstra, ewd831
SilentGhost
11
Zasadniczo wybierasz jeden zestaw błędów jeden po drugim dla drugiego. Jeden zestaw może powodować wcześniejsze zakończenie pętli, drugi może spowodować wyjątek (lub przepełnienie bufora w innych językach). Po napisaniu zestawu kodu zobaczysz, że wybór zachowania range()ma sens znacznie częściej
John La Rooy,
32
Link do Dijkstra, ewd831: cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
unutbu
35
@unutbu Ten artykuł Djikstry jest często cytowany w tym temacie, ale nie ma tu nic wartościowego, ludzie używają go jako odwołania do autorytetu. Jedynym istotnym pseudo-powodem, dla którego podaje pytanie OP, jest to, że czuje, że włączenie górnej granicy staje się „nienaturalne” i „brzydkie” w szczególnym przypadku, gdy sekwencja jest pusta - jest to całkowicie subiektywna pozycja, z którą łatwo się sprzeciwić, więc nie wnosi wiele do stołu. „Eksperyment” z Mesą nie ma zbyt dużej wartości bez znajomości ich szczególnych ograniczeń lub metod oceny.
Sundar - Przywróć Monikę
6
@andreasdr Ale nawet jeśli argument kosmetyczny jest prawidłowy, czy podejście Pythona nie wprowadza nowego problemu czytelności? W powszechnym języku angielskim termin „zakres” oznacza, że ​​coś sięga od czegoś do czegoś - na przykład interwału. Że len (lista (zakres (1,2))) zwraca 1, a len (lista (zakres (2))) zwraca 2, to jest coś, czego naprawdę musisz nauczyć się trawić.
armin

Odpowiedzi:

245

Ponieważ częstsze jest wywoływanie, range(0, 10)które zwraca [0,1,2,3,4,5,6,7,8,9]10 elementów, które są równelen(range(0, 10)) . Pamiętaj, że programiści wolą indeksowanie oparte na 0.

Weź również pod uwagę następujący wspólny fragment kodu:

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

Czy widzisz, że gdyby range()doszło dokładnie do len(li)tego, byłoby to problematyczne? Programista musiałby wyraźnie odjąć 1. Wynika to również wspólny trend programistów preferując for(int i = 0; i < 10; i++)nad for(int i = 0; i <= 9; i++).

Jeśli często wywołujesz zakres z początkiem 1, możesz chcieć zdefiniować własną funkcję:

>>> def range1(start, end):
...     return range(start, end+1)
...
>>> range1(1, 10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
moinudin
źródło
48
Gdyby takie było uzasadnienie, czy parametry nie byłyby takie range(start, count)?
Mark Ransom,
3
@shogun Domyślna wartość początkowa to 0, czyli range(10)jest równa range(0, 10).
moinudin
4
Twoje range1nie będzie działać z zakresami, które mają inny rozmiar kroku niż 1.
dimo414
6
Wyjaśniasz, że zakres (x) powinien zaczynać się od 0, a x będzie „długością zakresu”. OK. Ale nie wyjaśniłeś, dlaczego zakres (x, y) powinien zaczynać się od x, a kończyć na y-1. Jeśli programista chce pętli for z i od 1 do 3, musi wyraźnie dodać 1. Czy to naprawdę dotyczy wygody?
armin
7
for i in range(len(li)):jest raczej antypatternem. Należy użyć enumerate.
hans
27

Chociaż istnieją tutaj pewne użyteczne wyjaśnienia algorytmiczne, myślę, że może pomóc dodanie prostego rozumowania w „prawdziwym życiu”, dlaczego działa to w ten sposób, co znalazłem przydatne, gdy przedstawiłem ten temat młodym nowicjuszom:

Przy czymś takim jak „zakres (1,10)” może pojawić się zamieszanie w wyniku myślenia, że ​​para parametrów reprezentuje „początek i koniec”.

W rzeczywistości zaczyna się i „zatrzymuje”.

Teraz gdyby tak było wartości „koniec”, a następnie, tak, możesz spodziewać się, że liczba będzie uwzględniona jako ostatecznego wejścia w sekwencji. Ale to nie jest „koniec”.

Inni błędnie nazywają ten parametr „count”, ponieważ jeśli użyjesz tylko „range (n)”, wówczas oczywiście iteruje się „n” razy. Ta logika ulega awarii po dodaniu parametru początkowego.

Kluczową kwestią jest zapamiętanie jego nazwy: „ stop ”. Oznacza to, że w tym momencie iteracja natychmiast się zatrzyma. Nie po tym punkcie.

Tak więc, chociaż „start” rzeczywiście reprezentuje pierwszą wartość, którą należy uwzględnić, to po osiągnięciu wartości „stop” „pęka”, a nie kontynuuje przetwarzanie „również tej” przed zatrzymaniem.

Jedną z analogii, którą wykorzystałem, tłumacząc to dzieciom, jest to, że, jak na ironię, lepiej się zachowuje niż dzieci! Nie kończy się po tym, jak powinien - zatrzymuje się natychmiast, nie kończąc tego, co robił. (Dostają to;))

Kolejna analogia - kiedy prowadzisz samochód, nie mijamy znaku stop / ustępowania / „ustąp” i kończy się to, że siedzisz gdzieś obok lub za samochodem. Technicznie rzecz biorąc, nadal nie osiągnąłeś tego, kiedy przestałeś. Nie jest uwzględnione w „rzeczach, które przekazałeś podczas podróży”.

Mam nadzieję, że niektóre z nich pomogą wyjaśnić Pythonitos / Pythonitas!

dingle
źródło
To wyjaśnienie jest bardziej intuicyjne. Dzięki
Fred,
Wyjaśnienie dla dzieci jest po prostu przezabawne!
Antony Hatchkins,
1
Próbujesz nałożyć szminkę na świnię. Rozróżnienie między „stopem” a „końcem” jest absurdalne. Jeśli przejdę od 1 do 7, nie zaliczyłem 7. Po prostu wadą Pythona jest stosowanie różnych konwencji dla pozycji początkowej i końcowej. W innych językach, w tym ludzkich, „od X do Y” oznacza „od X do Y”. W Pythonie „X: Y” oznacza „X: Y-1”. Jeśli masz spotkanie od 9 do 11, czy mówisz ludziom, że od 9 do 12 lub od 8 do 11?
bzip2
24

Ekskluzywne zakresy mają pewne zalety:

Po pierwsze, każdy element range(0,n)jest prawidłowym indeksem dla list długościn .

Również range(0,n)ma długość n, a nie n+1jakim zakresie integracyjnego będzie.

sepp2k
źródło
18

Działa dobrze w połączeniu z indeksowaniem zerowym i len(). Na przykład, jeśli masz 10 pozycji na liście x, są one ponumerowane 0–9. range(len(x))daje ci 0-9.

Oczywiście, ludzie będą wam to bardziej pythonowy zrobić for item in xlub for index, item in enumerate(x)zamiast for i in range(len(x)).

Krojenie również działa w ten sposób: foo[1:4]to pozycje 1-3 foo(pamiętając, że pozycja 1 jest tak naprawdę drugą pozycją ze względu na indeksowanie zerowe). Aby zachować spójność, oba powinny działać w ten sam sposób.

Myślę o tym jako: „pierwszy numer, którego chcesz, a następnie pierwszy numer, którego nie chcesz”. Jeśli chcesz 1-10, pierwszą liczbą, której nie chcesz, jest 11, więc jest range(1, 11).

Jeśli w konkretnej aplikacji staje się nieporęczny, wystarczy napisać małą funkcję pomocniczą, która dodaje 1 do indeksu końcowego i wywołań range().

kindall
źródło
1
Zgadzam się na krojenie. 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
może wymieniony przykład powinien przeczytać, for index, item in enumerate(x)aby uniknąć zamieszania
seans
@seans Dzięki, naprawiono.
kindall
12

Jest także przydatny do dzielenia zakresów; range(a,b)można podzielić na range(a, x)a range(x, b), natomiast z zakresu integracyjnego byś napisać albo x-1albo x+1. Chociaż rzadko trzeba dzielić zakresy, często dzielisz listy, co jest jednym z powodów, dla których krojenie listy l[a:b]zawiera a-ty element, ale nie b-ty. Zatem rangeposiadanie tej samej właściwości sprawia, że ​​jest ładnie spójna.

Xuanji
źródło
11

Długość zakresu to górna wartość minus dolna wartość.

Jest bardzo podobny do czegoś takiego:

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

w języku C.

Podobnie jak zakres Ruby:

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

Jednak Ruby rozpoznaje, że wiele razy będziesz chciał dołączyć wartość końcową i oferuje alternatywną składnię:

1..10 #this is also a range from 1 to 10
Skilldrick
źródło
17
Gah! Nie używam Ruby, ale mogę sobie wyobrazić, że 1..10vs 1...10będąc ciężko rozróżnić podczas czytania kodu!
moinudin
6
@marcog - gdy wiesz, że istnieją dwie formy, twoje oczy
dostrzegają
11
Operator zakresu Ruby jest całkowicie intuicyjny. Im dłuższa forma, tym krótsza sekwencja. kaszel
Russell Borogove,
4
@ Russell, może 1 ........... 20 powinien dać taki sam zakres jak 1..10. To byłby cukier syntaktyczny, na który warto się zamienić. ;)
kevpie
4
@ Russell Dodatkowa kropka wyciska ostatni przedmiot spoza zakresu :)
Skilldrick
5

Zasadniczo w pythonie range(n)iteruje się nczasy, które mają charakter wyłączny, dlatego nie podaje ostatniej wartości podczas drukowania, możemy stworzyć funkcję, która daje wartość włączającą, co oznacza, że ​​wypisze również ostatnią wartość wymienioną w zakresie.

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
źródło
4

Rozważ kod

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

Chodzi o to, że otrzymujesz listę długości y-x, którą możesz (jak widać powyżej) iterować.

Przeczytaj dokumentację Pythona dotyczącą zakresu - uważają oni, że iteracja pętli for jest podstawowym przypadkiem użycia.

Robert
źródło
1

W wielu przypadkach wygodniej jest rozumować.

Zasadniczo moglibyśmy myśleć o zakresie jako odstępie między starti end. Jeśli start <= endodstęp między nimi wynosi end - start. Gdyby lenfaktycznie zdefiniowano jako długość, miałbyś:

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

Liczymy jednak liczby całkowite zawarte w zakresie, zamiast mierzyć długość przedziału. Aby powyższa właściwość była prawdziwa, powinniśmy uwzględnić jeden z punktów końcowych, a drugi wykluczyć.

Dodanie stepparametru jest jak wprowadzenie jednostki długości. W takim przypadku można się spodziewać

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

na długość. Aby uzyskać liczbę, wystarczy użyć podziału na liczby całkowite.

Arsen
źródło
Te mechanizmy niespójności Pythona są przezabawne. Gdybym chciał odstępu między dwiema liczbami, dlaczego miałbym używać odejmowania, aby uzyskać różnicę zamiast przedziału? Niespójne jest stosowanie różnych konwencji indeksowania pozycji początkowej i końcowej. Dlaczego trzeba pisać „5:22”, aby uzyskać pozycje od 5 do 21?
bzip2
To nie jest Python, to dość powszechne na całym forum. W C, Java, Ruby, nazywasz to
Arseny
Chciałem powiedzieć, że indeksowanie jest powszechne, a nie, że inne języki muszą mieć dokładnie ten sam rodzaj obiektu
Arseny