Właśnie zdałem sobie sprawę, że w Pythonie, jeśli ktoś pisze
for i in a:
i += 1
Elementy oryginalnej listy w a
rzeczywistości nie będą miały żadnego wpływu, ponieważ zmienna i
okazuje się być tylko kopią oryginalnego elementu w a
.
Aby zmodyfikować oryginalny element,
for index, i in enumerate(a):
a[index] += 1
byłoby potrzebne.
Byłem naprawdę zaskoczony tym zachowaniem. Wydaje się to być bardzo sprzeczne z intuicją, pozornie różne od innych języków i spowodowało błędy w moim kodzie, które musiałem debugować przez długi czas dzisiaj.
Wcześniej czytałem Samouczek języka Python. Dla pewności sprawdziłem teraz książkę i nawet nie wspomina o tym zachowaniu.
Jakie jest uzasadnienie tego projektu? Czy oczekuje się, że będzie to standardowa praktyka w wielu językach, aby samouczek był przekonany, że czytelnicy powinni to rozumieć naturalnie? W jakich innych językach występuje to samo zachowanie podczas iteracji, na które powinienem zwrócić uwagę w przyszłości?
i
jest niezmienne lub przeprowadzasz niemutującą operację. Z zagnieżdżoną listąfor i in a: a.append(1)
miałoby inne zachowanie; Python nie kopiuje zagnieżdżonych list. Jakkolwiek liczby całkowite są niezmienne, a dodawanie zwraca nowy obiekt, nie zmienia to starego.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. To samo w C #i = i + 1
się wpłynąća
?Odpowiedzi:
Ja już Ostatnio odpowiedziałem na podobne pytanie i bardzo ważne jest, aby zdać sobie sprawę, że
+=
mogą mieć różne znaczenia:Jeśli typ danych implementuje dodawanie w miejscu (tj. Działa poprawnie
__iadd__
funkcję), wówczas dane,i
do których się odnosi, są aktualizowane (nie ma znaczenia, czy znajduje się na liście, czy gdzie indziej).Jeśli typ danych nie implementuje
__iadd__
metody,i += x
instrukcja jest tylko cukrem syntaktycznymi = i + x
, więc tworzona jest nowa wartość i przypisywana do nazwy zmienneji
.Jeśli typ danych implementuje,
__iadd__
ale robi coś dziwnego. Możliwe, że jest aktualizowany ... lub nie - zależy to od tego, co tam jest zaimplementowane.Pythonowe liczby całkowite, zmiennoprzecinkowe, ciągi nie są implementowane,
__iadd__
więc nie będą one aktualizowane w miejscu. Jednak inne typy danych, takie jaknumpy.array
lublist
s, implementują go i zachowują się tak, jak się spodziewałeś. Zatem podczas iteracji nie jest to kwestia kopiowania ani braku kopiowania (zwykle nie wykonuje kopii dlalist
si ituple
s - ale to również zależy od implementacji kontenerów__iter__
i__getitem__
metody!) - jest to bardziej kwestia typu danych masz w sobiea
.źródło
Wyjaśnienie - terminologia
Python nie rozróżnia pojęć odniesienia i wskaźnika . Zwykle używają po prostu odwołania do odwołania , ale jeśli porównasz z językami takimi jak C ++, które mają takie rozróżnienie - jest to znacznie bliżej wskaźnika .
Ponieważ pytający wyraźnie pochodzi z tła C ++, a ponieważ to rozróżnienie - które jest wymagane do wyjaśnienia - nie istnieje w Pythonie, zdecydowałem się użyć terminologii C ++, która brzmi:
void foo(int x);
jest sygnaturą funkcji, która otrzymuje liczbę całkowitą według wartości .void foo(int* x);
jest sygnaturą funkcji, która otrzymuje liczbę całkowitą przez wskaźnik .void foo(int& x);
jest sygnaturą funkcji, która otrzymuje liczbę całkowitą przez odniesienie .Co masz na myśli mówiąc „różni się od innych języków”? Większość języków, o których wiem, że obsługuje dla każdej pętli, kopiuje element, chyba że wyraźnie postanowiono inaczej.
Specjalnie dla Pythona (choć wiele z tych powodów może dotyczyć innych języków o podobnych koncepcjach architektonicznych lub filozoficznych):
Takie zachowanie może powodować błędy dla osób, które nie są tego świadome, ale alternatywne zachowanie może powodować błędy nawet dla tych, którzy są tego świadomi . Kiedy przypisujesz zmienną (
i
), zwykle nie zatrzymujesz się i rozważasz wszystkie inne zmienne, które zostałyby zmienione z tego powodu (a
). Ograniczenie zakresu, nad którym pracujesz, jest głównym czynnikiem zapobiegającym kodowi spaghetti, dlatego iteracja po kopii jest zwykle domyślna nawet w językach, które obsługują iterację przez odniesienie.Zmienne w języku Python są zawsze pojedynczym wskaźnikiem, więc iteracja przy kopiowaniu jest tania - tańsza niż iteracja przez odniesienie, co wymagałoby dodatkowego odroczenia przy każdym dostępie do wartości.
Python nie ma pojęcia zmiennych odniesienia, takich jak - na przykład - C ++. Oznacza to, że wszystkie zmienne w Pythonie są w rzeczywistości referencjami, ale w tym sensie, że są wskaźnikami, a nie zakulisowymi stałymi referencjami, takimi jak
type& name
argumenty C ++ . Ponieważ ta koncepcja nie istnieje w Pythonie, implementacja iteracji przez odniesienie - nie mówiąc już o ustawieniu jej jako domyślnej! - będzie wymagać dodania większej złożoności do kodu bajtowego.for
Instrukcja Pythona działa nie tylko na tablicach, ale na bardziej ogólnej koncepcji generatorów. Za kulisami Python wywołujeiter
tablice, aby uzyskać obiekt, który - gdy go wywołujesznext
- zwraca następny element lubraise
saStopIteration
. Istnieje kilka sposobów implementacji generatorów w Pythonie i byłoby znacznie trudniej zaimplementować je dla iteracji przez odniesienie.źródło
*it = ...
- ale tego rodzaju składnia już wskazuje, że modyfikujesz coś gdzie indziej - co sprawia, że powód nr 1 jest mniejszym problemem. Powody # 2 i # 3 również nie mają zastosowania, ponieważ w C ++ kopiowanie jest kosztowne i istnieje pojęcie zmiennych odniesienia. Co do przyczyny 4 - możliwość zwrócenia referencji pozwala na prostą implementację we wszystkich przypadkach.Żadna z odpowiedzi tutaj nie daje żadnego kodu do pracy, aby naprawdę zilustrować, dlaczego tak się dzieje w krainie Python. I fajnie jest patrzeć głębiej, więc proszę bardzo.
Głównym powodem, dla którego nie działa to tak, jak się spodziewasz, jest to, że podczas pisania w Pythonie:
nie robi tego, co myślisz. Liczby całkowite są niezmienne. Można to zobaczyć, patrząc na obiekt znajdujący się w Pythonie:
Funkcja id reprezentuje unikalną i stałą wartość obiektu w czasie jego życia. Pod względem koncepcyjnym luźno mapuje na adres pamięci w C / C ++. Uruchamianie powyższego kodu:
Oznacza to, że pierwszy
a
nie jest już taki sam jak drugia
, ponieważ ich identyfikatory są różne. W rzeczywistości znajdują się w różnych miejscach w pamięci.Z przedmiotem jednak wszystko działa inaczej. Zastąpiłem
+=
tutaj operatora:Uruchomienie tego powoduje następujące wyniki:
Zauważ, że atrybut id w tym przypadku jest w rzeczywistości taki sam dla obu iteracji, nawet jeśli wartość obiektu jest inna (możesz również znaleźć
id
wartość int, którą posiada obiekt, który zmieniałby się w miarę mutacji - ponieważ liczby całkowite są niezmienne).Porównaj to z uruchomieniem tego samego ćwiczenia z niezmiennym obiektem:
To daje:
Kilka rzeczy do zauważenia. Po pierwsze, w pętli z
+=
nie dodajesz już do oryginalnego obiektu. W tym przypadku, ponieważ ints należą do niezmiennych typów w Pythonie , python używa innego identyfikatora. Warto również zauważyć, że Python używa tego samego instrumentu bazowegoid
dla wielu zmiennych o tej samej niezmiennej wartości:tl; dr - Python ma garść niezmiennych typów, które powodują zachowanie, które widzisz. Dla wszystkich typów zmiennych twoje oczekiwania są poprawne.
źródło
@ Odpowiedź Idana dobrze wyjaśnia, dlaczego Python nie traktuje zmiennej pętli jako wskaźnika tak jak w C, ale warto wyjaśnić bardziej szczegółowo, w jaki sposób rozpakowywane są fragmenty kodu, ponieważ w Pythonie wiele prostych pozornie bitów kodu będą w rzeczywistości wywołaniami metod wbudowanych . Weźmy swój pierwszy przykład
Są dwie rzeczy do rozpakowania:
for _ in _:
składnia i_ += _
składnia. Aby najpierw wziąć pętlę for, podobnie jak inne języki, Python mafor-each
pętlę, która jest zasadniczo cukrem składniowym dla wzorca iteratora. W Pythonie iterator to obiekt, który definiuje.__next__(self)
metodę, która zwraca bieżący element w sekwencji, przechodzi do następnego i podniesie wartość,StopIteration
gdy nie będzie już więcej elementów w sekwencji. Iterowalny jest obiektem, który określa.__iter__(self)
, która zwraca iteracyjnej.(Uwaga: an
Iterator
jest równieżIterable
i zwraca się po swojej.__iter__(self)
metodzie).Python zwykle ma wbudowaną funkcję, która deleguje do niestandardowej metody podwójnego podkreślenia. Więc ma to,
iter(o)
co rozwiązuje,o.__iter__()
anext(o)
które rozwiązujeo.__next__()
. Uwaga: te wbudowane funkcje często próbują zastosować rozsądną domyślną definicję, jeśli metoda, którą delegują, nie jest zdefiniowana. Na przykładlen(o)
zwykle rozwiązuje,o.__len__()
ale jeśli ta metoda nie jest zdefiniowana, spróbujeiter(o).__len__()
.A dla pętli jest zasadniczo definiowane
next()
,iter()
i więcej podstawowych struktur sterowania. Ogólnie kodrozpakuje się do czegoś takiego
Więc w tym przypadku
zostaje rozpakowany do
Druga połowa to
i += 1
. Ogólnie%ASSIGN% += %EXPR%
jest rozpakowywany do%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Tutaj__iadd__(self, other)
dodaje się w miejscu i zwraca się.(Uwaga: Jest to kolejny przypadek, w którym Python wybierze alternatywę, jeśli główna metoda nie zostanie zdefiniowana. Jeśli obiekt nie zaimplementuje
__iadd__
, zacznie się opierać__add__
. W rzeczywistości robi to w tym przypadku, gdyint
nie implementuje__iadd__
- co ma sens, ponieważ są niezmienne i dlatego nie można ich modyfikować).Twój kod tutaj wygląda
gdzie możemy zdefiniować
W twoim drugim fragmencie kodu dzieje się coś więcej. Dwie nowe rzeczy, o których musimy wiedzieć, to:
%ARG%[%KEY%] = %VALUE%
rozpakowywanie(%ARG%).__setitem__(%KEY%, %VALUE%)
i%ARG%[%KEY%]
rozpakowywanie(%ARG%).__getitem__(%KEY%)
. Łącząc tę wiedzę,a[ix] += 1
rozpakowujemy sięa.__setitem__(ix, a.__getitem__(ix).__add__(1))
(ponownie:__add__
zamiast__iadd__
dlatego, że__iadd__
nie jest zaimplementowana przez ints). Nasz końcowy kod wygląda następująco:Aby odpowiedzieć na twoje pytanie, dlaczego pierwszy nie modyfikuje listy, a drugi tak, w naszym pierwszym fragmencie,
i
znext(_a_iter)
którego otrzymujemy , co oznacza,i
że będzie toint
. Ponieważint
nie można modyfikować w miejscu,i += 1
nic nie robi na liście. W naszym drugim przypadku ponownie nie modyfikujemy,int
ale modyfikujemy listę, dzwoniąc__setitem__
.Powodem tego całego skomplikowanego ćwiczenia jest to, że myślę, że uczy następującej lekcji o Pythonie:
Metody podwójnego podkreślenia stanowią przeszkodę na początku, ale są niezbędne do wspierania reputacji Pythona w zakresie „uruchamialnego pseudokodu”. Przyzwoity programista w języku Python dokładnie zrozumie te metody i sposób ich wywoływania oraz zdefiniuje je tam, gdzie ma to sens.
Edycja : @deltab poprawił moje niechlujne użycie terminu „kolekcja”.
źródło
__len__
i__contains__
+=
działa inaczej w zależności od tego, czy bieżąca wartość jest zmienna czy niezmienna . To był główny powód, dla którego implementacja w Pythonie długo trwa, ponieważ deweloperzy Pythona bali się, że będzie to mylące.Jeśli
i
jest int, to nie można go zmienić, ponieważ ints są niezmienne, a zatem jeśli wartośći
zmian musi koniecznie wskazywać na inny obiekt:Jeśli jednak lewa strona jest zmienna , + = może ją zmienić; na przykład jeśli jest to lista:
W twojej pętli for
i
odnosi się do każdego elementu za
kolei. Jeśli są to liczby całkowite, zastosowanie ma pierwszy przypadek, a wynikiem tegoi += 1
musi być to, że odnosi się do innego obiektu liczb całkowitych.a
Oczywiście lista wciąż zawiera te same elementy, które zawsze miała.źródło
i = 1
ustawiai
się na niezmienny obiekt liczb całkowitych, toi = []
należy ustawići
na niezmienny obiekt listy. Innymi słowy, dlaczego obiekty całkowite są niezmienne, a obiekty listy można modyfikować? Nie widzę w tym żadnej logiki.list
implementuje metody zmieniające ich zawartość,int
nie robi tego.[]
jest zmiennym obiektem listy ii = []
pozwalai
odnosić się do tego obiektu.+=
operator / metodę, aby zachowywały się podobnie (zasada najmniejszego zaskoczenia) dla obu typów: albo zmień oryginalny obiekt, albo zwróć zmodyfikowaną kopię dla liczb całkowitych i list.+=
jest to zaskakujące w Pythonie, ale wydawało się, że inne wspomniane opcje byłyby również zaskakujące lub co najmniej mniej praktyczne (zmiana oryginalnego obiektu nie może być wykonana przy użyciu najczęstszego rodzaju wartości używasz + = z, ints. A kopiowanie całej listy jest znacznie droższe niż jej mutowanie, Python nie kopiuje takich rzeczy jak listy i słowniki, chyba że jest to wyraźnie wskazane). To była wtedy ogromna debata.Pętla tutaj jest trochę nieistotna. Podobnie jak parametry funkcji lub argumenty, konfiguracja takiej pętli for jest w zasadzie tylko fantazyjnym przypisaniem.
Liczby całkowite są niezmienne. Jedynym sposobem na ich zmodyfikowanie jest utworzenie nowej liczby całkowitej i przypisanie jej do tej samej nazwy co oryginał.
Semantyka Pythona dla mapowania przypisań bezpośrednio na C (co nie dziwi, biorąc pod uwagę wskaźniki PyObject * CPython), z jedynym zastrzeżeniem, że wszystko jest wskaźnikiem i nie możesz mieć podwójnych wskaźników. Rozważ następujący kod:
Co się dzieje? Drukuje
1
. Czemu? Jest to w przybliżeniu odpowiednik następującego kodu C:W kodzie C oczywiste jest, że wartość parametru nie
a
ulega zmianie.Jeśli chodzi o to, dlaczego listy wydają się działać, odpowiedź jest po prostu taka, że przypisujesz to samo nazwisko. Listy są zmienne. Tożsamość nazwanego obiektu
a[0]
ulegnie zmianie, alea[0]
nadal jest prawidłową nazwą. Możesz to sprawdzić za pomocą następującego kodu:Ale to nie jest specjalne dla list. Zamień
a[0]
w tym kodzie na,y
a otrzymasz dokładnie ten sam wynik.źródło