Historia pytania:
Przekazywanie parametrów w Pythonie realizuje zasadę "call by object reference" (czasami nazywaną call by sharing). Oznacza to, że zmienna wewnątrz funkcji zaczyna wskazywać na ten sam obiekt w pamięci, co przekazany z zewnątrz argument.
Problem:
Jeśli funkcja zmienia przekazany zmienny obiekt (na przykład listę lub słownik), zmiany są widoczne także na zewnątrz funkcji. Może to prowadzić do trudnych do wykrycia błędów, zwłaszcza jeśli oczekuje się, że funkcja nie zmieni danych wejściowych.
Rozwiązanie:
Aby uniknąć efektów ubocznych, warto wykonać kopiowanie obiektu wewnątrz funkcji lub używać niemutowalnych struktur danych. Do kopiowania stosuje się standardowe metody (na przykład list.copy() dla list, dict.copy() dla słowników lub copy.deepcopy()).
Przykład kodu:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # Jak uniknąć zmian? Zrobić kopię: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Kluczowe cechy:
Czy można być pewnym, że kopia listy utworzona za pomocą .copy() jest całkowicie niezależna od oryginalnej listy?
Nie — .copy() tworzy płytką kopię. Jeśli wewnątrz znajdują się zagnieżdżone obiekty zmienne, zmiany w nich będą widoczne także w oryginale.
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
Czy zwrócenie nowego obiektu na podstawie wejścia gwarantuje brak zmian w oryginale?
Nie zawsze. Jeśli w nowym obiekcie wykorzystywane są części oryginalnego (na przykład odniesienie do wewnętrznej zagnieżdżonej listy), oryginalny obiekt może zostać zmieniony.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
Czy domyślne wartości argumentów dla obiektów zmiennych mogą prowadzić do problemów przy wielokrotnym wywołaniu funkcji?
Tak — wartość domyślna obliczana jest tylko raz podczas definiowania funkcji.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Negatywny przypadek
W bibliotece do przetwarzania konfiguracji użyto listy jako wartości domyślnej, co doprowadziło do gromadzenia się elementów między różnymi wywołaniami funkcji. Zachowanie było nieprzewidywalne i długo się ujawniało.
Plusy:
Mniej kodu dla powtarzających się wywołań, widoczna oszczędność pamięci.
Minusy:
Niejawne zachowanie, trudności w debugowaniu, długotrwałe błędy.
Pozytywny przypadek
Zastosowanie None jako wartości domyślnej i jawne tworzenie nowego obiektu przy każdym wywołaniu.
Plusy:
Przewidywalność, brak niespodziewanych efektów ubocznych, niezawodność.
Minusy:
Wymaga świadomego podejścia i nieco więcej kodu.