Rozumiem, że range()
funkcja, która w Pythonie 3 jest typem obiektu , generuje zawartość w locie, podobnie jak generator.
W takim przypadku oczekiwałbym, że następujący wiersz zajmie nadmiernie dużo czasu, ponieważ w celu ustalenia, czy 1 biliard mieści się w zakresie, należałoby wygenerować biliardy:
1000000000000000 in range(1000000000000001)
Co więcej: wydaje się, że bez względu na to, ile zer dodam, obliczenia mniej więcej zajmują tyle samo czasu (w zasadzie natychmiastowe).
Próbowałem też takich rzeczy, ale obliczenia są prawie natychmiastowe:
1000000000000000000000 in range(0,1000000000000000000001,10) # count by tens
Jeśli spróbuję zaimplementować własną funkcję zakresu, wynik nie będzie tak dobry !!
def my_crappy_range(N):
i = 0
while i < N:
yield i
i += 1
return
Co range()
robi obiekt pod maską, który sprawia, że jest tak szybki?
Odpowiedź Martijna Pietersa została wybrana ze względu na jej kompletność, ale zobacz także pierwszą odpowiedź abarnerta, aby uzyskać dobrą dyskusję na temat tego, co to znaczy range
być pełnoprawną sekwencją w Pythonie 3, oraz pewne informacje / ostrzeżenia dotyczące potencjalnej niespójności w zakresie __contains__
optymalizacji funkcji w różnych implementacjach Pythona . Inna odpowiedź abarnerta zawiera bardziej szczegółowe informacje i zawiera łącza dla osób zainteresowanych historią stojącą za optymalizacją w Pythonie 3 (i brakiem optymalizacji xrange
w Pythonie 2). Odpowiedzi poke i wim zawierają odpowiedni kod źródłowy C i wyjaśnienia dla zainteresowanych.
źródło
bool
lublong
typ, w przypadku innych typów obiektów zwariuje. Spróbuj z:100000000000000.0 in range(1000000000000001)
range
to generator?xrange
taki sam jak Python3range
?xrange()
obiekty nie mają__contains__
metody, więc sprawdzanie elementów musi przebiegać przez wszystkie elementy. Dodatkowo istnieje kilka innych zmianrange()
, jak obsługuje krojenie (który ponownie zwracarange
obiekt) i teraz też macount
iindex
metody, aby był kompatybilny zcollections.Sequence
ABC.Odpowiedzi:
Obiekt Python 3
range()
nie generuje liczb od razu; jest to inteligentny obiekt sekwencji, który wytwarza liczby na żądanie . Wszystko, co zawiera, to wartości początkowe, stop i krokowe, a następnie podczas iteracji nad obiektem następna liczba całkowita jest obliczana dla każdej iteracji.Obiekt implementuje również
object.__contains__
hak i oblicza, czy Twój numer należy do jego zakresu. Obliczanie jest operacją (prawie) o stałym czasie * . Nigdy nie ma potrzeby skanowania wszystkich możliwych liczb całkowitych w zakresie.Z
range()
dokumentacji obiektu :Zatem przynajmniej twój
range()
obiekt wykonałby:W dalszym ciągu brakuje kilku rzeczy, które prawdziwe
range()
wsparcie (takich jak metody.index()
lub.count()
, mieszanie, testowanie równości lub krojenie), ale powinien dać ci pomysł.Uprościłem także
__contains__
implementację, aby skupić się wyłącznie na testach na liczbach całkowitych; jeśli podasz rzeczywistemurange()
obiektowi wartość niecałkowitą (łącznie z podklasamiint
), zainicjowane zostanie powolne skanowanie w celu sprawdzenia, czy istnieje dopasowanie, tak jak w przypadku testu szczelności względem listy wszystkich zawartych wartości. Zrobiono to, aby nadal obsługiwać inne typy liczbowe, które akurat obsługują testowanie równości za pomocą liczb całkowitych, ale nie oczekuje się, aby wspierały również arytmetykę liczb całkowitych. Zobacz oryginalny problem w języku Python, który implementował test powstrzymywania.* Niemal stały czas, ponieważ liczby całkowite w języku Python są nieograniczone, więc operacje matematyczne również rosną w czasie, gdy N rośnie, co czyni tę operację O (log N). Ponieważ wszystko jest wykonywane w zoptymalizowanym kodzie C, a Python przechowuje wartości całkowite w 30-bitowych porcjach, zabraknie pamięci, zanim zobaczysz jakikolwiek wpływ na wydajność z powodu wielkości zaangażowanych tutaj liczb całkowitych.
źródło
__getitem__
i__len__
,__iter__
implementacja jest w rzeczywistości niepotrzebna.xrangeiterator
dodano specjalną wersję , ponieważ nie była wystarczająco szybka. A potem gdzieś w 3.x (nie jestem pewien, czy to był 3.0 czy 3.2) został odrzucony i używają tego samegolistiterator
typu, którylist
używa.def __init__(self, *start_stop_step)
i parsowałbym go stamtąd; sposób, w jaki argumenty są teraz oznaczane, jest nieco mylący. Niemniej jednak +1; nadal zdecydowanie wyjaśniłeś zachowanie.range
jest starszy niż*args
(znacznie mniejargclinic
API, który pozwala funkcjom C-API mieć pełne podpisy w języku Python). Kilka innych starych funkcje (i kilka nowsze funkcje, jakxrange
,slice
iitertools.islice
, dla konsystencji) działają tak samo, ale w przeważającej części, Guido i reszta deweloperów podstawowych wydaje się z tobą zgodzić. Dokumenty w wersji 2.0+ opisują nawetrange
znajomych, jakby były przeciążeniem w stylu C ++, zamiast pokazywać mylący podpis.argclinic
dyskusji Guido , kiedy Nick Coghlan wymyślił sposóbrange
jednoznacznego zdefiniowania : „Nie ułatwiaj ludziom kopiowania mojej najgorszej decyzji projektowej”. Jestem więc prawie pewien, że zgadza się, żerange
jest to mylące, jak napisano.Podstawowym nieporozumieniem jest myślenie, że
range
jest generatorem. To nie jest. W rzeczywistości nie jest to żaden iterator.Możesz to łatwo powiedzieć:
Gdyby to był generator, iteracja go raz by go wyczerpała:
To, co
range
tak naprawdę jest, to sekwencja, podobnie jak lista. Możesz nawet to przetestować:Oznacza to, że musi przestrzegać wszystkich zasad bycia sekwencją:
Różnica między a
range
alist
polega na tym, że arange
jest sekwencją leniwą lub dynamiczną ; nie pamiętam wszystkich jego wartości, to po prostu pamięta jejstart
,stop
istep
, i tworzy wartości od zapotrzebowania na__getitem__
.(Na marginesie, jeśli zauważysz
print(iter(a))
, żerange
używa tego samegolistiterator
typu colist
. Jak to działa? Alistiterator
nie używa niczego specjalnegolist
poza tym, że zapewnia implementację C__getitem__
, więc działa dobrze dlarange
też.)Teraz nic nie mówi o tym, że
Sequence.__contains__
musi to być stały czas - w rzeczywistości, dla oczywistych przykładów takich sekwencjilist
, nie jest. Ale nic nie mówi, że nie może być. I łatwiej jest wdrożyć,range.__contains__
aby sprawdzić to matematycznie ((val - start) % step
ale z pewną dodatkową złożonością, aby poradzić sobie z negatywnymi krokami) niż faktycznie wygenerować i przetestować wszystkie wartości, więc dlaczego nie zrobić tego lepiej?Ale wydaje się, że w języku nie ma nic, co gwarantowałoby, że tak się stanie. Jak zauważa Ashwini Chaudhari, jeśli podasz mu wartość niecałkowitą, zamiast konwersji na liczbę całkowitą i wykonania testu matematycznego, wróci do iteracji wszystkich wartości i porównywania ich jeden po drugim. I tylko dlatego, że wersje CPython 3.2+ i PyPy 3.x zawierają tę optymalizację, a jest to oczywisty dobry pomysł i łatwy do zrobienia, nie ma powodu, aby IronPython lub NewKickAssPython 3.x nie mógł tego pominąć. (I faktycznie CPython 3.0-3.1 go nie zawierał.)
Gdyby
range
rzeczywiście był generatorem,my_crappy_range
testowanie w__contains__
ten sposób nie miałoby sensu , a przynajmniej sposób, w jaki ma sens, nie byłby oczywisty. Jeśli już iterowałeś już pierwsze 3 wartości, to czy generator jest1
nadalin
? Czy testowanie w celu1
spowodowania iteracji i zużycia wszystkich wartości do1
(lub do pierwszej wartości>= 1
)?źródło
range
jest wymieniony (wraz zlist
ituple
) jako typ sekwencji .xrange
jest też sekwencją. Zobacz dokumenty 2.7 . W rzeczywistości zawsze była to prawie sekwencja..__iter__()
metodą, która zwróci iterator. Z kolei można go użyć tylko raz.range
nie sprawdza typów, gdy nie jest liczbą całkowitą, ponieważ zawsze jest możliwe, że typ__eq__
jest zgodny zint
. Pewnie,str
oczywiście nie zadziała, ale nie chcieli spowolnić rzeczy poprzez jawne sprawdzenie wszystkich typów, które nie mogą tam być (a przecieżstr
podklasa mogłaby zastąpić__eq__
i być zawarta wrange
).Użyj źródła , Luke!
W CPython
range(...).__contains__
(opakowanie metody) ostatecznie deleguje się do prostego obliczenia, które sprawdza, czy wartość może być w zakresie. Powodem prędkości jest tutaj matematyczne rozumowanie granic, a nie bezpośrednia iteracja obiektu zasięgu . Aby wyjaśnić zastosowaną logikę:start
istop
, aNa przykład
994
jest,range(4, 1000, 2)
ponieważ:4 <= 994 < 1000
, i(994 - 4) % 2 == 0
.Pełny kod C znajduje się poniżej, co jest nieco bardziej szczegółowe ze względu na zarządzanie pamięcią i liczenie referencji, ale podstawowa idea jest taka:
„Mięso” pomysłu wspomniano w wierszu :
Na koniec - spójrz na
range_contains
funkcję u dołu fragmentu kodu. Jeśli dokładna kontrola typu nie powiedzie się, nie używamy opisanego sprytnego algorytmu, zamiast tego wracamy do głupiego wyszukiwania iteracji zakresu za pomocą_PySequence_IterSearch
! Możesz sprawdzić to zachowanie w tłumaczu (korzystam z wersji 3.5.0 tutaj):źródło
Aby dodać do odpowiedzi Martijna, jest to odpowiednia część źródła (w C, ponieważ obiekt zakresu jest zapisany w kodzie natywnym):
Więc dla
PyLong
obiektów (które jestint
w Pythonie 3) użyjerange_contains_long
funkcji do ustalenia wyniku. Ta funkcja zasadniczo sprawdza, czyob
znajduje się w określonym zakresie (chociaż wygląda nieco bardziej skomplikowanie w C).Jeśli to nie jest
int
obiekt, wraca do iteracji, dopóki nie znajdzie wartości (lub nie).Całą logikę można przetłumaczyć na pseudo-Python w następujący sposób:
źródło
Jeśli zastanawiasz się, dlaczego dodano tę optymalizację
range.__contains__
i dlaczego nie dodano jejxrange.__contains__
w wersji 2.7:Po pierwsze, jak odkrył Ashwini Chaudhary, numer 1766304 został jawnie otwarty w celu optymalizacji
[x]range.__contains__
. Poprawka do tego została zaakceptowana i zarejestrowana w wersji 3.2 , ale nie została przeniesiona do wersji 2.7, ponieważ „xrange zachowywał się w ten sposób przez tak długi czas, że nie widzę, co kupuje nam, aby wprowadzić łatkę tak późno”. (2.7 było prawie na tym etapie.)W międzyczasie:
Pierwotnie
xrange
był to obiekt niezupełnie sekwencyjny. Jak mówią dokumenty 3.1 :To nie była do końca prawda;
xrange
obiekt faktycznie obsługiwane kilka innych rzeczy, które przychodzą automatycznie i indeksowanialen
, * w tym__contains__
(poprzez przeszukiwanie liniowe). Ale nikt nie sądził, że warto było robić w tym czasie pełne sekwencje.Następnie, w ramach implementacji PEP abstrakcyjnych klas podstawowych , ważne było, aby dowiedzieć się, które wbudowane typy powinny zostać oznaczone jako implementujące, które ABC i
xrange
/ lubrange
twierdziły, że mają zaimplementowaćcollections.Sequence
, mimo że nadal obsługiwały tylko to samo „bardzo małe zachowanie”. Nikt nie zauważył tego problemu do wydania 9213 . Łatka dla tego problemu nie tylko została dodanaindex
icount
do wersji 3.2range
, ale także przerobiła zoptymalizowaną__contains__
(która dzieli tę samą matematykęindex
i jest bezpośrednio używanacount
). ** Ta zmiana dotyczyła również wersji 3.2 i nie została przeniesiona do wersji 2.x, ponieważ „jest to poprawka błędów, która dodaje nowe metody”. (W tym momencie 2.7 był już w stanie rc.)Były więc dwie szanse na przeniesienie tej optymalizacji do wersji 2.7, ale obie zostały odrzucone.
* W rzeczywistości masz nawet iterację za darmo z samym indeksowaniem, ale w 2.3
xrange
obiektach masz niestandardowy iterator.** Pierwsza wersja faktycznie go ponownie wdrożyła i podała błędne dane - np. Dałaby ci
MyIntSubclass(2) in range(5) == False
. Ale zaktualizowana wersja łatki Daniela Stutzbacha przywróciła większość poprzedniego kodu, w tym powrót do ogólnego, powolnego_PySequence_IterSearch
,range.__contains__
którego domyślnie używała wersja wcześniejsza niż 3.2, gdy optymalizacja nie ma zastosowania.źródło
xrange.__contains__
, wygląda na to, że nie dokonali backportowania go do Pythona 2, aby zostawić element zaskoczenia dla użytkowników i było już za późno.count
Iindex
plaster dodano później. Plik w tym czasie: hg.python.org/cpython/file/d599a3f2e72d/Objects/rangeobject.c2to3
zamiast za pomocą podwójnego kodu przy pomocy bibliotek takich jaksix
, dlatego dostaliśmy takie rzeczy,dict.viewkeys
których nikt nigdy nie użyje), i były kilka zmian, które pojawiły się zbyt późno w wersji 3.2, ale w przeważającej części 2.7 było imponującą wersją „ostatniej wersji 2.x”.Inne odpowiedzi już to dobrze wyjaśniły, ale chciałbym zaoferować kolejny eksperyment ilustrujący naturę obiektów zasięgu:
Jak widać, obiekt zasięgu to obiekt, który pamięta swój zasięg i może być używany wiele razy (nawet podczas iteracji nad nim), a nie tylko generator jednorazowy.
źródło
To wszystko o leniwe podejście do oceny i jakimś dodatkowym optymalizacji z
range
. Wartości w zakresach nie muszą być obliczane do czasu rzeczywistego użycia, a nawet dalej ze względu na dodatkową optymalizację.Nawiasem mówiąc, twoja liczba całkowita nie jest tak duża, zastanów się
sys.maxsize
sys.maxsize in range(sys.maxsize)
jest dość szybkidzięki optymalizacji - łatwo jest porównać podaną liczbę całkowitą tylko z minimalnym i maksymalnym zakresem.
ale:
Decimal(sys.maxsize) in range(sys.maxsize)
jest dość wolny .(w tym przypadku nie ma optymalizacji
range
, więc jeśli python otrzyma nieoczekiwany po przecinku, python porówna wszystkie liczby)Powinieneś być świadomy szczegółów implementacji, ale nie powinieneś na nich polegać, ponieważ może to ulec zmianie w przyszłości.
źródło
float(sys.maxsize) != sys.maxsize)
Mimo to na większości maszynsys.maxsize-float(sys.maxsize) == 0
.TL; DR
Obiekt zwracany przez
range()
to tak naprawdęrange
obiekt. Ten obiekt implementuje interfejs iteratora, dzięki czemu możesz kolejno iterować jego wartości, podobnie jak generator, lista lub krotka.Ale to także implementuje
__contains__
interfejs, który jest rzeczywiście to, co jest wywoływana, gdy po prawej strony pojawi się przedmiotin
operatora. Że__contains__()
metoda zwracabool
na to, czy pozycji na lewej-stroniein
jest w obiekcie. Ponieważrange
obiekty znają swoje granice i krok, bardzo łatwo jest to zaimplementować w O (1).źródło
Weźmy przykład, 997 jest w zasięgu (4, 1000, 3), ponieważ:
4 <= 997 < 1000, and (997 - 4) % 3 == 0.
źródło
Wypróbuj
x-1 in (i for i in range(x))
dużex
wartości, które używają zrozumienia generatora, aby uniknąć wywoływaniarange.__contains__
optymalizacji.źródło