Jak myśleć jak Pythonauta
To jest fragment wątku z archiwum grupy comp.lang.python. Postanowiłem go przetłumaczyć, ponieważ dotyka problemu, który bardzo często pojawia się w pytaniach początkujących Pythonautów.
Oryginał można znaleźć tutaj google groups.
Pytanie
W niedalekiej przeszłości (to było pisane w kwietniu 2002, tak dla porządku) pewien poszukiwacz odpowiedzi na swoje wątpliwości doznawszy oświecenia wysłał następujące pytanie na comp.lang.python:
Witam, jedną z rzeczy, którą bardzo lubię w Pythonie jest to, że instrukcje działają, tak jak się tego oczekuje. Weźmy na przykład użycie dict.values() dla słowników. Jeśli zachowa się wartość zwróconą przez dict.values(), a następnie zmodyfikuje słownik, to wcześniej zachowana wartość pozostanie nietknięta. >>> dict = {'a':1,'b':2} >>> list = dict.values() >>> list [1, 2] >>> dict['a']=3 >>> list [1, 2] >>> dict {'a': 3, 'b': 2} Jednakże, jeśli słownik zawiera listy jako wartości, zauważyłem zachowanie nieintuicyjne (które ostatnio popsuło mój kod). Jeśli zmodyfikuje się słownik, to lista utworzona wcześniej przy pomocy dict.values() zostanie automagicznie zaktualizowana. Fajna własność, ale to ostatnia rzecz, której bym się spodziewał! >>> dict = {'a':[1],'b':[2]} >>> list = dict.values() >>> list [[1], [2]] >>> dict['a'].append(3) >>> dict {'a': [1, 3], 'b': [2]} >>> list [[1, 3], [2]] To wygląda tak, jakby w pierwszym przypadku zwracana była kopia, natomiast w drugim referencje do listy. Ok, ale wg filozofii Pythona nie pownienem się przejmować tym, czy pracuję z listami w słownikach, czy czymkolwiek innym. Jeśli zachowanie ma zależeć od wiedzy na temat typu wartości, które znajdują się w słowniku, uważam to za w jakimś stopniu sprzeczne z intuicją. Kto się tutaj myli: moja intuicja czy Python? Jeśli moja intuicja, to jak powinienem poprawić swoje myślenie o modelu obiektowym Pythona, aby moja intuicja stała się lepsza. ;-)
Rzecz jasna, wina leżała po stronie intuicji pytającego, ale zdecydowanie nie jest (był) on osamotniony w trwaniu w tym błędnym przekonaniu.
Na szczęście dla niego, dwóch nieco bardziej pythonicznie doświadczonych grupowiczów - Michael Hudson i Alex Martelli - było w szczególnie pedaogicznym nastroju tego dnia i napisało przydługie artykuły wyjaśniające w raczej odmienny sposób gdzie zbłądził.
Odpowiedź Michaela Hudsona
Michael wygłosił tyradę na temat myślenia w kategoriach "nazw, obiektów i wiązań (bindings)" i narysował kilka diagramów wyjaśniającyh co się tak naprawdę dzieje we wnętrzu interaktywnej sesji, co do której OP miał wątpliwości:
> jedną z rzeczy, którą bardzo lubię w Pythonie jest to, że instrukcje > działają, tak jak się tego oczekuje. No cóż, Python działa dokładnie tak, jak tego oczekuję, ale nie jestem pewien, czy to mówi więcej o mnie czy o Pythonie <wink>. Na końcu Twojego postu mówisz: > Kto się tutaj myli: moja intuicja czy Python? Jeśli moja intuicja, > to jak powinienem poprawić swoje myślenie o modelu obiektowym > Pythona, aby moja intuicja stała się lepsza. ;-) To Ty się mylisz. :) Jako, że nie mogę na razie odczytać moich maili [1], to nie mam nic lepszego do roboty, niż narysowanie Ci kilku diagramów w ascii. Na początek trochę terminologii. A właściwie pierwsza rzecz to trochę anty-terminologii. Słowo "zmienna" uważam za szczególnie mało pomocne w kontekście Pythona. Preferuję raczej terminy "nazwy", "wiązania" i "obiekty". "Nazwy" wyglądają tak: ,-----. | foo | `-----' "Nazwy" żyją w przestrzeniach nazw, ale to nie jest dla tych rozważań istotne, jako że jedyną przestrzenią nazw jaka odgrywa jakąś rolę w naszym przypadku, jest ta związana z pętlą read-eval-print iterpretera. Właściwie to "nazwy" odgrywają pomniejszą rolę w naszym dramacie; "wiązania" i "obiekty" są prawdziwymi gwiazdami. "Wiązania" wyglądają tak: ------------> Lewe zakończenia "wiązań" mogą być połączone z "nazwami" lub innymi miejscami jak: atrybuty obiektów, pozycje list lub słowników. Ich prawe zakończenia zawsze łączą się z obiektami [2]. Obiekty wyglądają tak: +-------+ | "bar" | +-------+ To oznacza obiekt typu string "bar". Inne typy obiektów będą rysowane inaczej, ale mam nadzieję, że się połapiesz o co chodzi. > Weźmy na przykład użycie dict.values() dla słowników. > Jeśli zachowa się wartość zwróconą przez dict.values(), > a następnie zmodyfikuje słownik, to wcześniej zachowana wartość > pozostanie nietknięta. > >>> dict = {'a':1,'b':2} Po tej instrukcji adekwatny będzie taki rysunek: ,------. +-------+ | dict |------>|+-----+| +---+ `------' || "a" |+---->| 1 | |+-----+| +---+ |+-----+| +---+ || "b" |+---->| 2 | |+-----+| +---+ +-------+ > >>> list = dict.values() Teraz taki: ,------. +-------+ | dict |------>|+-----+| +---+ `------' || "a" |+------------>| 1 | |+-----+| +---+ |+-----+| /\ || "b" |+-----. ,---' |+-----+| | | +-------+ `----+----. | | ,------. +-----+ | \/ | list |------>| [0]-+------------' +---+ `------' | [1]-+--------------->| 2 | +-----+ +---+ > >>> list > [1, 2] Co oczywiście nie jest żadną niespodzianką. > >>> dict['a']=3 A teraz taki: ,------. +-------+ | dict |------>|+-----+| +---+ `------' || "a" |+-. | 1 | |+-----+| | +---+ |+-----+| | /\ || "b" |+-+---. ,---' |+-----+| | | | +-------+ | `----+----. | | | ,------. +-----+ | | \/ | list |------>| [0]-+---+--------' +---+ `------' | [1]-+---+----------->| 2 | +-----+ | +---+ | +---+ `----------->| 3 | +---+ > >>> list > [1, 2] > >>> dict > {'a': 3, 'b': 2} To także nie powinno być niespodzianką; poprostu prześledź strzałki (wiązania) powyżej. > Jednakże jeśli słownik zawiera listy jako wartości, zauważyłem > zachowanie nieintuicyjne (które ostatnio popsuło mój kod). > Jeśli zmodyfikuje się słownik, to lista utworzona wcześniej > przy pomocy dict.values() zostanie automagicznie zaktualizowana. > Fajna własność, ale to ostatnia rzecz, której bym się spodziewał! To dlatego, że nie myślisz w kategoriach "nazw", "obiektów" i "wiązań". > >>> dict = {'a':[1],'b':[2]} ,------. +-------+ | dict |------>|+-----+| +-----+ +---+ `------' || "a" |+---->| [0]-+-->| 1 | |+-----+| +-----+ +---+ |+-----+| +-----+ +---+ || "b" |+---->| [0]-+-->| 2 | |+-----+| +-----+ +---+ +-------+ > >>> list = dict.values() ,------. +-------+ | dict |------>|+-----+| +-----+ +---+ `------' || "a" |+------------>| [0]-+-->| 1 | |+-----+| +-----+ +---+ |+-----+| /\ || "b" |+-----. ,----' |+-----+| | | +-------+ `----+-----. | | ,------. +-----+ | \/ | list |------>| [0]-+------------' +-----+ +---+ `------' | [1]-+--------------->| [0]-+-->| 2 | +-----+ +-----+ +---+ > >>> list > [[1], [2]] I znowu, żadna niespodzianka. > >>> dict['a'].append(3) +---+ ,------. +-------+ ,->| 1 | | dict |------>|+-----+| +-----+ | +---+ `------' || "a" |+------------>| [0]-+-' |+-----+| | [1]-+-. |+-----+| +-----+ | +---+ || "b" |+-----. /\ `->| 3 | |+-----+| | ,----' +---+ +-------+ | | `----+-----. ,------. +-----+ | \/ | list |------>| [0]-+------------' +-----+ +---+ `------' | [1]-+--------------->| [0]-+-->| 2 | +-----+ +-----+ +---+ > >>> dict > {'a': [1, 3], 'b': [2]} > >>> list > [[1, 3], [2]] I to także nie powinno budzić zdziwienia. > To wygląda tak, jakby w pierwszym przypadku zwracana była kopia, > natomiast w drugim referencje do listy. Ok, ale wg filozofii > Pythona nie pownienem się przejmować tym, czy pracuję z listami > w słownikach, czy czymkolwiek innym. Jeśli zachowanie ma zależeć > od wiedzy na temat typu wartości, które znajdują się w słowniku, > uważam to za w jakimś stopniu sprzeczne z intuicją. Jeśli jeszcze nie odkryłeś na podstawie powyższych rysunków skąd wzięło się Twoje mylne wyobrażenie, to nie jestem pewien czy jakiś dalszy opis może tu pomóc. Pozdrowienia M. [1] Czy ktoś wie co się dzieje ze starship? [2] Każdy kto tutaj wspomni o UnboundLocalError zostanie rozstrzelany. -- A.D. 1517: Martin Luther nails his 95 Theses to the church door and is promptly moderated down to (-1, Flamebait). -- http://slashdot.org/comments.pl?sid=01/02/09/1815221&cid=52 (although I've seen it before)
Odpowiedź Alexa Martelli
Alex przyjął inną, bogatszą w słowa strategię, wyjaśniającą iż Python nie kopiuje, gdy nie musi, opowiadając fajną anegdotę o statui w Bolonii i sugerując iż OP powinien poczytać coś Borgesa, Calvino, Wittgensteina lub Korzibsky'iego:
> Witam, > > jedną z rzeczy, którą bardzo lubię w Pythonie jest to, > że instrukcje działają, tak jak się tego oczekuje. > Weźmy na przykład użycie dict.values() dla słowników. > Jeśli zachowa się wartość zwróconą przez dict.values(), > a następnie zmodyfikuje słownik, to wcześniej zachowana wartość > pozostanie nietknięta. Metoda .values() słownika została zdefiniowana tak, aby zwracać nową listę wartości. To jest i tak mniej lub bardziej nieuniknione, gdyż słownik normalnie nie _posiada_ listy swoich wartości, a więc musi ją utworzyć w locie, jeśli ktoś o to poprosi. To nie jest kopia -- to jest nowy obiekt listy. Jednakże Python NIE kopiuje za wyjątkiem sytuacji, w których kopiowanie jest specjalnie zdefiniowane i zamierzone. Metoda .values() będąc w pewnym sensie taką sytuacją, jak już wspomniałem, zwraca raczej nowy obiekt, niż kopiuje istniejący. Generalnie, jeśli tylko jest to możliwe, Python raczej zwraca referencje do tych obiektów, które ma już pod ręką, niż kopiuje. Jeśli CHCESZ kopii musisz o to poprosić -- zobacz moduł copy jeśli chcesz to zrobić w sposób generalny. Oczywiście tworzenie nowych obiektów, to jest inny przypadek. Jeśli to jest mało intuicyjne, to niech tak będzie -- tu tak naprawdę dla ogólnego przypadku nie ma żadnej alternatywy, która nie prowadziłaby do olbrzymich kosztów tworzenia kopii wszystkiego "tak na wszelki wypadek". O WIELE lepsze jest tworzenie kopii jedynie na wyraźne, jawne żądanie (i tworzenie obiektów wtedy, gdy nie ma już istniejących obiektów, które możnaby skopiować lub do których możnaby się odwołać). Oczywiście są też przypadki pośrednie - jak wycinki (slices). Standardowe sekwencje zwracają nowe obiekty, jeśli poprosi się o ich wycinek. To ma znaczenie jedynie dla list (dla obiektów niemodyfikowalnych (immutable) nie istotne jest czy otrzymaliśmy kopię, czy nie). Lista nie potrafi "współdzielić części samej siebie", a więc poproszona o wycinek zwraca kopię, nową listę (oczywiście dla zachowania ogólności, czyni to także poproszona o wycinek-wszystkiego, lista[:] -- w tym przypadku nowy obiekt może być rozumiany jako kopia istniejącego obiektu) Z drugiej jednak strony bardzo popularny pakiet Numeric definiuje typ array, który jest zdolny do współdzielenia części lub wszystkich danych wśród wielu obiektów array -- a więc wycinek obiektu array z Numeric współdzieli dane z obiektem array, z którego został wycięty, zwróć uwagę: >>> import Numeric >>> a=Numeric.array(range(6)) >>> b=a[:] >>> id(a) 136052568 >>> id(b) 136052728 >>> ale dwa oddzielne obiekty a i b współdzielą dane: >>> a array([0, 1, 2, 3, 4, 5]) >>> b array([0, 1, 2, 3, 4, 5]) >>> a[3]=23 >>> b array([ 0, 1, 2, 23, 4, 5]) >>> Za każdym z tych zachowań kryją się doskonałe walory praktyczne -- listy są o wiele prostsze, gdyż nie trzeba się martwić o współdzielenie danych, natomiast obiekty array mają inne przypadki użycia -- jednak trudno nie być zaskoczonym, gdy tak w pewnym stopniu podobne obiekty różnią się w takich szczegółach. Jednak wszystkie kopiowania, które następują, np. w przypadku wycinków lub czegokolwiek innego (poza JEDNYM wyjątkiem o którym za chwilę) są zawsze kopiami PŁYTKIMI. Python NIGDY nie podejmuje się OLBRZYMIEGO zadania _głębokiego_ kopiowania o ile specjalnie o to nie poprosisz - specjalnie czyli przy pomocy funkcji deepcopy z modułu copy. GŁĘBOKIE kopiowanie jest poważną sprawą - funkcja deepcopy musi uważać na cykle, odtworzyć każdą jednostkę referencyjną, potencjalnie prześledzić referencje do dowolnej głębokości, rekurencyjnie -- musi wiernie odtworzyć bezgranicznie złożony graf referencji między obiektami. To działa, ale oczywiście nigdy nie będzie to tak szybkie jak zwykłe, przyziemne płytkie kopiowanie (które z kolei nigdy nie jest tak szybkie, jak udostępnienie poprostu jednej referencji więcej do obiektu, jeśli tylko jest to wykonalne). A więc wracając do problemu, który najwyraźniej Cię tu sprowadził: > Jednakże jeśli słownik zawiera listy jako wartości, zauważyłem > zachowanie nieintuicyjne (które ostatnio popsuło mój kod). > Jeśli zmodyfikuje się słownik, to lista utworzona wcześniej > przy pomocy dict.values() zostanie automagicznie zaktualizowana. > Fajna własność, ale to ostatnia rzecz, której bym się spodziewał! Niezupełnie -- jeśli zmienisz obiekt do którego referencję zawiera słownik (a nie sam słownik jako taki), to inne referencje do tego-samego-obiektu pozostają referencjami do właśnie tego obiektu -- jeśli ten obiekt zostanie zmodyfikowany, to zobaczysz właśnie ten zmodyfikowany obiekt niezależnie od tego jakiej referencji do niego użyjesz. >>>> dict = {'a':[1],'b':[2]} >>>> list = dict.values() >>>> list > [[1], [2]] Nie używaj nazw typów wbudowanych (built-in types) jako zmiennych: SPARZYSZ się kiedyś na tym. dict, list, str, tuple, file, int, long, float, unicode... NIE używaj tych identyfikatorów do swoich celów, choć nie wiem jak kuszące by to było. Jeśli nie wyrobisz sobie nawyku unikania ich, to pewnego dnia będziesz próbował utorzyć listę przy pomocy x=list('ciao') i dostaniesz zagadkowe błędy... ponieważ sprawiłeś, że identyfikator 'list' odnosi się już do pewnego obiektu listy, a nie do samego typu list. Używaj alist, somedict, myfile, cokolwiek... to nie ma nic wspólnego z Twoim problemem tutaj, poprostu drobna rada!-) >>>> dict['a'].append(3) To nie "zmienia słownika" -- obiekt słownika nadal zawiera te same referencje, do obiektów z tymi samymi id (dwa obiekty string - klucze i dwa obiekty listy - wartości). Modyfikujesz jeden z tych obiektów, ale to zupełnie inna sprawa. Tak czy siak mógłbyś zmodyfikować obiekt listy poprzez jakąkolwiek inną referencję do niego, np.: >>> alist=list('ciao') >>> adict={'a':alist} >>> adict {'a': ['c', 'i', 'a', 'o']} >>> alist.pop() 'o' >>> adict {'a': ['c', 'i', 'a']} >>> Jeśli chciałbyś, aby słownik adict odnosił się do KOPII ("snapshota" jeśli wolisz) zawartości alist, mógłbyś zrobić tak: >>> import copy >>> alist=list('ciao') >>> adict={'a':copy.copy(alist)} >>> adict {'a': ['c', 'i', 'a', 'o']} >>> alist.pop() 'o' >>> adict {'a': ['c', 'i', 'a', 'o']} >>> i wtedy stringowa reprezentacja obiektu słownika byłaby odizolowana od wszelkich zmian listy, do której referencję zawiera alist. Ta stringowa reprezentacja oddelegowuje część swojego zadania do obiektów, do których referencje słownik zawiera, a więc jeśli chcesz to odizolować, potrzebujesz kopii - może nawet głębokich kopii, chociaż właściwie (<drżę na samą myśl>... nie, właściwie nie, chociaż...:-). >>>> dict > {'a': [1, 3], 'b': [2]} >>>> list > [[1, 3], [2]] > > To wygląda tak, jakby w pierwszym przypadku zwracana była kopia, > natomiast w drugim referencje do listy. Ok, ale wg filozofii Nie. ZAWSZE referencje. .values() nie zwraca ani referencji do istniejącego obiektu ANI kopii istniejącego obiektu, ponieważ w tym przypadku nie ma "istniejącego obiektu" -- a więc zwraca ona zawsze NOWY obiekt odpowiednio skonstruowany wg specyfikacji. > Pythona nie pownienem się przejmować tym, czy pracuję z listami > w słownikach, czy czymkolwiek innym. Jeśli zachowanie ma zależeć > od wiedzy na temat typu wartości, które znajdują się w słowniku, > uważam to za w jakimś stopniu sprzeczne z intuicją. Nie ma tu takiej zależności. Jest za to olbrzymia różnica pomiędzy zmianą obiektu, a zmianą INNEGO obiektu, na który ten pierwszy wskazuje. W Bolonii ponad 100 lat temu statuę lokalnego bohatera przedstawionego z wyciągniętym przed siebie palcem -- przypuszczalnie w przyszłość, ale wziąwszy pod uwagę miejsce, w którym była ulokowana, tubylcy szybko zaczęli ją identyfikować jako "statua, która wskazuje na Hotel Belfiore". Pewnego dnia jakiś przedsiębiorczy inwestor kupił ten hotel i zrestrukturyzował go -- a dokładniej, tam gdzie do tej pory był hotel teraz była restauracja, Da Carlo. A więc, "statua, która wskazuje na Hotel Belfiore" stała się nagle "statuą, która wskazuje na restaurację Da Carlo...!" Zdumiewające, nieprawdaż? Zważywszy, że marmur nie jest zbyt płynny (łatwo odkształcalny) i statua nie została przesunięta ani naruszona w żaden sposób...? Tak przy okazji to jest prawdziwa anegdota (poza tym, że nie jestem pewien nazw hotelu i restauracji -- co do nich mogę się mylić), i myślę że może tu być pomocna. Słownik lub statua nie zmieniły się ani trochę, nawet jeśli obiekty, do kórych referencje zawierają (na które wskazują) zostały zmodyfikowane nie do poznania, a nazwy pod którymi były znane (np. stringowa reprezentacja słownika) uległy zmianie. Nazwa lub reprezentacja stanowiła i stanowi luźno związaną, nietrwałą charakterystykę pewnego stanu statui lub słownika. > Kto się tutaj myli: moja intuicja czy Python? Jeśli moja intuicja, > to jak powinienem poprawić swoje myślenie o modelu obiektowym > Pythona, aby moja intuicja stała się lepsza. ;-) Twoja intuicja, która zawiodła Cię tu na manowce (Python robi to co do niego należy), może być poprawiona na kilka sposobów. Dzieła J.L. Borgesa i I. Calvino, jeśli lubisz fikcję, która jest dosyć skomplikowana, ale nadal całkiem przyjemna, byłyby dobrym wyborem. Jeśli wolisz coś niefikcyjnego napisanego przez inżynierów ciężko walczących, aby rozwiać niektóre błędy filozofów, świetni są Wittgenstein i Korzibsky. Nie żartuję, ale zdaję sobie sprawę, że wielu Pythonautów nie specjalnie obchodzą powyższe tematy. W tym przypadku, ta grupa i jej archiwa, eseje GvR i /F oraz zródła Pythona mogą być ciekawym materiałem do przeczytania. Alex
Esejem autorstwa /F, o którym Alex wspomniał, jest prawdopodobnie ten (a jeśli nawet nie był, powinieneś go przeczytać). Opowiada o tym samym zagadnieniu w bardziej zwięzłej formie.
Zadowolony pytający
A teraz aby udowodnić, że był jakiś sens w tym wszystkim i że OP odszedł usatysfakcjonowany:
Drogi Michaelu, drogi Aleksie, jesteście doskonałymi nauczycielami !!! Michael, bardzo mi pomogłeś pojąć sens problemu swoimi rysunkami. Wielkie dzięki za Twoje dzieło sztuki. Alex, ta anegdota o statui wskazującej na Hotel Belfiore sprawiła, że błąd w mojej intuicji wydał mi się teraz taki oczywisty! Podoba mi się to i nigdy, przenigdy już tego nie zapomnę! Dziękuję za Twoją odpowiedź! Myślę, że dzisiaj nauczyłem się bardzo dużo na mojej drodze do zostania prawdziwym Pythonautą!
Mam nadzieję, że uznacie te odpowiedzi także za użyteczne.