Domyślny argument zmiennego języka Python: dlaczego?

20

Wiem, że domyślne argumenty są tworzone w czasie inicjalizacji funkcji, a nie za każdym razem, gdy funkcja jest wywoływana. Zobacz następujący kod:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Nie rozumiem, dlaczego zostało to zaimplementowane w ten sposób. Jakie korzyści oferuje to zachowanie w stosunku do inicjalizacji za każdym razem?

Sardathrion - Przywróć Monikę
źródło
Jest to już omówione w zadziwiający szczegół na przepełnienie stosu: stackoverflow.com/q/1132941/5419599
Wildcard

Odpowiedzi:

14

Myślę, że powodem jest prostota implementacji. Pozwól mi rozwinąć.

Domyślną wartością funkcji jest wyrażenie, które należy ocenić. W twoim przypadku jest to proste wyrażenie, które nie zależy od zamknięcia, ale może być czymś, co zawiera wolne zmienne - def ook(item, lst = something.defaultList()). Jeśli chcesz zaprojektować Python, będziesz miał wybór - czy oceniasz go raz, kiedy funkcja jest zdefiniowana, czy za każdym razem, gdy funkcja jest wywoływana. Python wybiera pierwszą (w przeciwieństwie do Ruby, która idzie z drugą opcją).

Są z tego pewne korzyści.

Po pierwsze, masz trochę przyspieszenia i zwiększenia pamięci. W większości przypadków będziesz mieć niezmienne domyślne argumenty, a Python może je zbudować tylko raz, zamiast przy każdym wywołaniu funkcji. To oszczędza (trochę) pamięć i czas. Oczywiście nie działa to dobrze ze zmiennymi wartościami, ale wiesz, jak się poruszać.

Kolejną zaletą jest prostota. Łatwo jest zrozumieć, jak ocenia się to wyrażenie - korzysta z zakresu leksykalnego, gdy funkcja jest zdefiniowana. Jeśli pójdą w drugą stronę, zakres leksykalny może się zmieniać między definicją a wywołaniem, co utrudni debugowanie. W takich przypadkach Python przechodzi bardzo długą drogę.

Stefan Kanev
źródło
3
Interesujący punkt - choć zwykle jest to zasada najmniejszego zaskoczenia Pythonem. Niektóre rzeczy są proste w sensie formalnej złożoności modelu, ale nie są oczywiste i zaskakujące, i myślę, że to się liczy.
Steve314,
1
Najmniej zaskoczeniem jest tutaj: jeśli masz semantykę oceny przy każdym wywołaniu, możesz otrzymać błąd, jeśli zamknięcie zmienia się między dwoma wywołaniami funkcji (co jest całkiem możliwe). Może to być bardziej zaskakujące niż uświadomienie, że zostanie to raz ocenione. Oczywiście można argumentować, że kiedy pochodzisz z innych języków, wyłączasz semantykę oceniania przy każdym wywołaniu i to jest niespodzianka, ale możesz zobaczyć, jak to działa w obie strony :)
Stefan Kanev
Dobra uwaga na temat zakresu
0xc0de,
Myślę, że zakres jest właściwie najważniejszy. Ponieważ domyślnie nie jesteś ograniczony do stałych, możesz potrzebować zmiennych, które nie są objęte zakresem w witrynie wywoływania.
Mark Ransom
5

Jednym ze sposobów jest to, że parametr lst.append(item) nie powoduje mutacji lst. lstwciąż odwołuje się do tej samej listy. Po prostu treść tej listy została zmutowana.

Zasadniczo Python nie ma (o ile pamiętam) żadnych zmiennych stałych ani niezmiennych - ale ma pewne stałe, niezmienne typy. Nie możesz zmodyfikować wartości całkowitej, możesz ją tylko zastąpić. Ale możesz modyfikować zawartość listy bez jej zastępowania.

Podobnie jak liczba całkowita, nie możesz modyfikować odwołania, możesz je tylko zastąpić. Możesz jednak zmodyfikować zawartość obiektu, do którego się odwołujesz.

Jeśli chodzi o jednorazowe utworzenie domyślnego obiektu, to wyobrażam sobie, że jest to głównie optymalizacja, aby zaoszczędzić na tworzeniu obiektów i narzucaniu śmieci.

Steve314
źródło
+1 Dokładnie. Ważne jest, aby zrozumieć warstwę pośrednią - że zmienna nie jest wartością; zamiast tego odwołuje się do wartości. Aby zmienić zmienną, wartość można zamienić lub zmutować (jeśli jest zmienna ).
Joonas Pulakka
W obliczu jakiejkolwiek trudnej kwestii związanej ze zmiennymi w pythonie, uważam, że pomocne jest uznanie „=” za „operator wiązania nazwy”; nazwa jest zawsze odbijana, bez względu na to, czy to, z czym ją wiążemy, jest nowa (instancja nowego obiektu lub niezmienny typ), czy nie.
StarWeaver
4

Jakie korzyści oferuje to zachowanie w stosunku do inicjalizacji za każdym razem?

Pozwala wybrać pożądane zachowanie, jak pokazano w przykładzie. Jeśli więc chcesz, aby domyślny argument był niezmienny, użyj niezmiennej wartości , takiej jak Nonelub 1. Jeśli chcesz zmienić domyślny argument na zmienny, użyj czegoś zmiennego, takiego jak []. To tylko elastyczność, choć trzeba przyznać, że może gryźć, jeśli jej nie znasz.

Joonas Pulakka
źródło
2

Myślę, że prawdziwa odpowiedź brzmi: Python został napisany jako język proceduralny i dopiero później przyjął aspekty funkcjonalne. To, czego szukasz, to domyślne ustawienie parametru jako zamknięcie, a zamknięcia w Pythonie są tak naprawdę tylko częściowo wypalone. Na dowód tej próby:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

co daje miejsce, w [2, 2, 2]którym można oczekiwać prawdziwego zamknięcia [0, 1, 2].

Jest całkiem sporo rzeczy, które chciałbym, gdyby Python miał możliwość zawijania domyślnych parametrów w zamknięciach. Na przykład:

def foo(a, b=a.b):
    ...

Byłoby to bardzo przydatne, ale „a” nie jest objęte zakresem definicji funkcji, więc nie możesz tego zrobić, a zamiast tego musisz robić niezręczne:

def foo(a, b=None):
    if b is None:
        b = a.b

Co jest prawie takie samo ... prawie.

ajscogo
źródło
1

Ogromną zaletą jest zapamiętywanie. To jest standardowy przykład:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

i dla porównania:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Pomiary czasu w ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s
steffen
źródło
0

Dzieje się tak, ponieważ kompilacja w Pythonie jest wykonywana przez wykonanie kodu opisowego.

Jeśli ktoś tak powiedział

def f(x = {}):
    ....

byłoby całkiem jasne, że za każdym razem chciałeś nowej tablicy.

Ale co jeśli powiem:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Tutaj zgaduję, że chcę tworzyć rzeczy na różnych listach i mieć jeden globalny catch-all, gdy nie podam listy.

Ale jak zgadłby to kompilator? Po co więc próbować? Moglibyśmy polegać na tym, czy to się nazywa, czy nie, i czasem może to pomóc, ale tak naprawdę to tylko zgadywanie. Jednocześnie istnieje dobry powód, aby nie próbować - spójności.

W tej chwili Python po prostu wykonuje kod. Zmienna list_of_all jest już przypisana do obiektu, więc obiekt jest przekazywany przez odwołanie do kodu, który domyślnie x, w taki sam sposób, jak wywołanie dowolnej funkcji otrzyma odwołanie do obiektu lokalnego o nazwie tutaj.

Gdybyśmy chcieli odróżnić nienazwany od nazwanego przypadku, wymagałoby to kodu przy kompilacji wykonującego przypisanie w znacznie inny sposób niż jest wykonywany w czasie wykonywania. Dlatego nie robimy specjalnego przypadku.

Jon Jay Obermark
źródło
-5

Dzieje się tak, ponieważ funkcje w Pythonie są obiektami pierwszej klasy :

Domyślne wartości parametrów są oceniane podczas wykonywania definicji funkcji. Oznacza to, że wyrażenie jest oceniane raz , gdy funkcja jest zdefiniowana, i że dla każdego wywołania używana jest ta sama „wstępnie obliczona” wartość .

Wyjaśnia dalej, że edycja wartości parametru modyfikuje wartość domyślną dla kolejnych wywołań i że proste rozwiązanie polegające na użyciu None jako wartości domyślnej, z jawnym testem w treści funkcji, jest wszystkim, co jest potrzebne, aby zapewnić brak niespodzianek.

Co oznacza, że ​​po def foo(l=[])wywołaniu staje się instancją tej funkcji i jest ponownie wykorzystywana do dalszych wywołań. Pomyśl o parametrach funkcji jako o oddzielaniu atrybutów obiektu.

Pro może obejmować wykorzystanie tego, aby klasy miały zmienne statyczne podobne do C. Dlatego najlepiej zadeklarować wartości domyślne Brak i zainicjować je w razie potrzeby:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

daje:

[5] [5] [5] [5]

zamiast nieoczekiwanego:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]

odwracać
źródło
5
Nie. Możesz zdefiniować funkcje (pierwszej klasy lub nie) inaczej, aby ponownie ocenić domyślne wyrażenie argumentu dla każdego wywołania. A potem wszystko, czyli około 90% odpowiedzi, jest całkowicie poza pytaniem. -1
1
Więc podziel się z nami tą wiedzą na temat definiowania funkcji do oceny domyślnego argumentu dla każdego wywołania, chciałbym wiedzieć o prostszym sposobie niż zaleca Python Docs .
odwrócenie
2
Mam na myśli poziom projektowania języka. Definicja języka Python mówi obecnie, że domyślne argumenty są traktowane tak, jak są; równie dobrze mógłby stwierdzić, że domyślne argumenty są traktowane w inny sposób. WIADOMO odpowiadasz „tak właśnie jest” na pytanie „dlaczego rzeczy są takie, jakie są”.
Python mógł zaimplementować domyślne parametry podobne do tego, jak robi to Coffeescript. Wstawiłby kod bajtowy, aby sprawdzić brakujące parametry, a jeśli ich brakowało, oceniłoby wyrażenie.
Winston Ewert