Diversity w polskim IT
Prashant Sharma
Prashant SharmaSoftware Engineer

Problem domyślnych argumentów w Pythonie

Poznaj przypadek użycia mutowalnego domyślnego argumentu w Pythonie i zobacz, jak rozwiązać problemy, które mogą się wtedy pojawić.
21.07.20203 min
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:

  1. Binding zmiennej lokalnej z bieżącą wartością zmiennej zewnętrznej w wywołaniu zwrotnym.
  2. 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

<p>Loading...</p>