Problem domyślnych argumentów w Pythonie
Obiekty typów wbudowanych, takich jak int
, float
, bool
, str
, tuple
oraz Unicode
, są niemutowalne. Obiekty typów wbudowanych, takich jak list
, set
i dict
, są z kolei mutowalne — taki obiekt może zatem zmienić swój stan lub zawartość. To bardzo prosta definicja obiektów mutowalnych i niemutowalnych w Pythonie. Najbardziej interesujące są tutaj jednak mutowalne domyślne obiekty w definicjach funkcji.
Poniżej napisałem bardzo prostą funkcję:
def foobar(element, data=[]):
data.append(element)
return data
W tej funkcji data
jest argumentem domyślnym ustawionym na listę (a więc obiekt mutowalny). Wykonajmy ją:
>>> print(foobar(12))
[12]
Takiego wyniku się właśnie spodziewałem. Teraz wykonujemy ją kilka razy:
>>> print(foobar(22))
[12, 22]
>>> print(foobar(32))
[12, 22, 32]
Co tu się dzieje? Za pierwszym razem funkcja zwraca dokładnie to, czego się spodziewaliśmy, a za drugim i każdym kolejnym ta sama lista jest używana w każdym wywołaniu.
Dlaczego tak się dzieje?
Domyślne argumenty Pythona są ewaluowane tylko raz, podczas definiowania funkcji, a nie za każdym razem, gdy funkcja jest wywoływana. Kiedy Python ją spotka, to pierwszą rzeczą, jaką zrobi, będzie skompilowanie jej w celu stworzenia obiektu kodu dla tej funkcji. Gdy zakończymy kompilację, Python dokonuje ewaluacji, a następnie przechowuje domyślne argumenty w samym obiekcie funkcji.
Zróbmy więc introspekcję. Aby wyjaśnić wszelkie nieporozumienia, podkreśliłem kilka wierszy kodu.
Funkcja przed wykonaniem
def foo(l=[]):
l.append(10)
Gdy funkcja zostanie zdefiniowana, możesz sprawdzić jej domyślne argumenty, używając __defaults__
. __defaults__
to krotka zawierająca domyślne wartości argumentów, o ile takie występują (None, jeśli żaden argument nie ma wartości domyślnej).
>>> foo.__defaults__
([],)
W _defaults_
znajdziemy tylko i wyłącznie pustą listę
Funkcja po wykonaniu
Wykonajmy tę funkcję i sprawdźmy elementy domyślne:
>>> foo()
>>> foo.__defaults__
([10],)
Wartość wewnątrz obiektu się zmienia. Kolejne wywołania funkcji będą dołączać kolejne wartości do wewnętrznego obiektu listy. Wykonajmy tę funkcję kilka razy:
>>> foo()
>>> foo()
>>> foo()
>>> foo.__defaults__
([10, 10, 10, 10],)
Dzieje się tak, ponieważ domyślne wartości argumentów są przechowywane w obiekcie funkcji, a nie w obiekcie kodu (bo reprezentują one wartości obliczane w czasie wykonywania).
Rozwiązanie
Pytanie teraz brzmi, jak zakodować foobar
, aby uzyskać oczekiwane zachowanie. Rozwiązanie jest na szczęście proste. Zmienimy argument domyślny z mutowalnego obiektu na None
i przetestujemy go w ciele funkcji pod kątem tej wartości:
def foobar(element, data=None):
if data is None:
data = []
data.append(element)
return data
>>> foobar(12)
[12]
Tak więc zastąpienie mutowalnych argumentów domyślnych wartością None
rozwiązało nasz problem.
Jak dobrze używać mutowalnych domyślnych argumentów
Mutowalne domyślne argumenty też mają dobre przypadki użycia:
- Binding zmiennej lokalnej z bieżącą wartością zmiennej zewnętrznej w wywołaniu zwrotnym.
- Cache /memoizacja.
Mam nadzieję, że spodobało Ci się to wyjaśnienie mutowalnych domyślnych argumentów w Pythonie.
Źródło:
Oryginał artykułu w języku angielskim możesz przeczytać tutaj.