Jak myśleć jak Pythonauta

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&amp;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.