programowanieProgramista Python / Inżynier Danych

Co się dzieje, gdy przekazujesz obiekt zmienny (na przykład listę lub słownik) do funkcji Pythona? Jak uniknąć niespodziewanych zmian wewnątrz i na zewnątrz funkcji?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

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:

  • Przekazywanie zmiennego obiektu pozwala na zmianę jego stanu zarówno wewnątrz, jak i na zewnątrz funkcji.
  • Aby tego uniknąć, stosuje się kopiowanie danych (płytkie/głębokie kopiowanie).
  • Obiekty niemutowalne są chronione przed takimi zmianami.

Pytania z podtekstami.

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]

Typowe błędy i antywzorce

  • Zmiana przekazanego zmiennego obiektu wewnątrz funkcji bez powiadamiania użytkownika.
  • Stosowanie płytkiego kopiowania dla zagnieżdżonych struktur danych (błąd z mutującymi zagnieżdżonymi obiektami).
  • Używanie zmiennych obiektów jako wartości domyślnych w argumentach funkcji.

Przykład z życia

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.