Kompleksowe wyjasnienie range w Python


Poznaj najbardziej popularny generator w Python. Kompleksowe wyjaśnienie range.


Jeśli piszesz w Python od jakiegoś czasu lub jeśli przerobiłeś dobre kursy czy książki to na pewno spotkałeś się z instrukcją range. W dużym uproszczeniu range generuję listę liczb, po których możemy iterować. Instrukcja ta występuje zarówno w Python2 jak i Python3. W obu idea jest ta sama, ale różnią się jednym bardzo ważnym detalem, który omówimy później. Poznaj kompleksowe wyjaśnienie range w Python.

Podstawowe użycie range

Jak wspomniałem range w python służy do generowania listy liczb. Wystarczy, że jako parametr podana zostanie wartość maksymalna. Podstawowe użycie może wyglądać zatem następująco:

range(10)

Tylko, że takie użycie nic nam nie da. W ten sposób zostanie utworzony iterowalny obiekt ( podobnie jak generagtor ), po którym możemy przechodzić. Spróbujmy zatem wypisać 10 liczb. Musimy do tego skorzystać z list comprehension, które swoją drogą zasługują na oddzielny wpis.

print([a for a in range(10)])

Wynik kodu będzie identyczny jak ten niżej. Który jest czytelniejszy?

print([0, 2, 3, 4, 5, 6, 7, 8, 9])

Jako, że jest to już lista, nie musimy już iterować po tych elementach.  Wyobraźcie sobie jak wyglądałby kod, gdybyśmy chcieli wypisać 100, 1000 lub kilkaset tysięcy liczb.

Range z start i stop

Nie zawsze oczekujemy, że lista będzie iterowana od 0. Na szczęście implementacja range przewiduje i to. Wystarczy podać dwa parametry, gdzie pierwszy będzie oznaczał początek a drugi koniec. Zatem jeśli chcemy wypisać liczby od 5 do 9 wystarczy fragment kodu

print([a for a in range(5, 10)])

Range z start, stop i step

Innym, częstym problemem korzystania z listy jest zwiększenie kolejnych wartości o inną wartość niż jeden. Na to również jest sposób. Implementacja range przewiduje również trzeci, opcjonalny parametr, który mówi o ile przeskoczyć z każdym kolejnym krokiem. Tak więc jeśli chcemy wypisać nieparzyste liczby od 1 do 10 wystarczy dodać

print([a for a in range(1, 10, 2)])

Lista malejąca

Jako, że każdy z parametrów może być dodatni lub ujemny można zrobić również odliczanie w dół

for a in range(5, 0, -1):
    if a > 1:
        print('Wpis o range na blogu ddeby pojawi się za {} dni'.format(a))
    elif a == 1:
        print('Wpis o range na blogu ddeby pojawi się za {} dzień'.format(a))
    else:
        print('Wpis o range na blogu ddeby pojawi się dziś')

Implementacja range

Jak widać implementacja range wygląda więc następująco

range([start], stop, [step]) 

Gdzie:

  • start - to początek, czyli pierwszy indeks
  • Stop - wartość końcowa, która nie jest zawarta w wyniku
  • Step - różnica pomiędzy kolejnymi iteracjami

Dzięki takiemu rozwiązaniu oferuje zdecydowanie większość przypadków, które są potrzebne.

Ważne:

Wszystkie parametry muszą być liczbami całkowitymi

Wszystkie parametry mogą być dodatnie lub ujemne

Range w python2 i python3

Wspomniałem wcześniej, że range występował w obu wersjach Pythona. Żeby zrozumieć różnicę sprawdźmy co zwraca range w Python2.

range(10)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Jak widzimy jest to lista. W Python w celu zaoszczędzenia pamięci na dużą skalę używane są generatory. Generatory mają tę zaletę, że są “lazy load”, dzięki czemu ładowane do pamięci są dopiero w momencie odwołania się. Żeby range również był “lazy load” wystarczy dodać x. A więc odpowiednik  range z Python3 w Python2 wygląda następująco

xrange(10)  # xrange(10)

Proste, ale jednak range jest nieco bardziej intuicyjne. Podejrzewam, że właśnie dlatego range w Python3 zastąpił xrange z Python2 i wycofano się z samego xrange. Dlatego własnie na początku wpisu musielismy skorzystać z list comprehension. Oczywiście jest to duże uproszczenie i przedstawiona wiedza jest wiedzą w pigułce.

Przykład range do tabliczki mnożenia w Python

def table(n):
    for i in range(n):
        print([j for j in range(n)])

Range z liczbami rzeczywistymi

Range jest bardzo ciekawą konstrukcją w Python niestety jest przygotowany jedynie pod liczby całkowite. Spróbujmy zatem napisać odpowiednik funkcję frange, czyli range dla liczb typu float

def frange(start, stop, step):
    while start < stop:
        yield start
        start += step

print([a for a in frange(1, 5, 0.5)])  # [1, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]

Jest to jedna z wielu możliwych implementacji, ale zależało mi głownie na tym, żeby implementacja była jako generator. W tej implementacji wymagane są wszystkie parametry, gdyż obsługa zwłaszcza opcjonalnego atrybutu start niepotrzebnie utrudniłaby zrozumienie kodu.

A Wy do czego wykorzystujecie range?

Range to nie generator!

Edit. Muszę bardzo podziękować dla użytkownika mitcom , bo faktycznie zauważył błąd we wpisie. Edytuję go więc, żeby nie wprowadzać ludzi w błąd. Faktycznie range nie jest generatorem ale również jest " lazy load" . Żeby to sprawdzić wystarczy, że spróbujemy wykonać metodę next na obiekcie typu range

numbers = range(20)
next(numbers)  # TypeError: 'range' object is not an iterator

Spróbujmy wykonać tę samą operację na generatorze

gen_numbers = (a for a in range(20))
next(gen_numbers)  # 0
next(gen_numbers)  # 1

Dodatkowo range ma zdefiniowaną długość, czego nie można powiedzieć o generatorze

gen_numbers = (a for a in range(20))
len(gen_numbers)  # TypeError: object of type 'generator' has no len()

numbers = range(20)
len(numbers)  # 20

Mam nadzieję, że nie wprowadziłem Was w błąd. Range faktycznie nie jest generatorem ale nie jest od razu ładowany do pamięci.

Aug 27, 2019

Najnowsze wpisy

Zobacz wszystkie