„Najmniejsze zdziwienie” i zmienny argument domyślny


2593

Każdy, kto majstruje przy Pythonie wystarczająco długo, został ugryziony (lub rozdarty na kawałki) przez następujący problem:

def foo(a=[]):
    a.append(5)
    return a

Nowicjusze Python oczekiwałby to funkcja zawsze zwraca listę z tylko jednego elementu: [5]. Rezultat jest natomiast zupełnie inny i bardzo zadziwiający (dla nowicjusza):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Mój menedżer po raz pierwszy spotkał się z tą funkcją i nazwał ją „dramatyczną wadą projektową” języka. Odpowiedziałem, że zachowanie ma podstawowe wytłumaczenie i jest naprawdę bardzo zagadkowe i nieoczekiwane, jeśli nie rozumiesz elementów wewnętrznych. Nie byłem jednak w stanie odpowiedzieć (sobie) na następujące pytanie: jaki jest powód wiązania domyślnego argumentu przy definicji funkcji, a nie przy wykonywaniu funkcji? Wątpię, czy doświadczone zachowanie ma praktyczne zastosowanie (kto tak naprawdę używał zmiennych statycznych w C, bez powodowania błędów?)

Edytuj :

Baczek dał ciekawy przykład. Wraz z większością twoich komentarzy, w szczególności z Utaal, rozwinąłem dalej:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Wydaje mi się, że decyzja projektowa była związana z tym, gdzie umieścić zakres parametrów: wewnątrz funkcji czy „razem z nią”?

Wykonanie wiązania wewnątrz funkcji oznaczałoby, że xjest skutecznie powiązane z określonym domyślnym, gdy funkcja jest wywoływana, nieokreślona, ​​co może mieć głęboką wadę: deflinia byłaby „hybrydowa” w tym sensie, że część wiązania ( obiekt funkcji) miałby miejsce w momencie definicji, a część (przypisanie parametrów domyślnych) w czasie wywołania funkcji.

Rzeczywiste zachowanie jest bardziej spójne: wszystko tej linii jest oceniane podczas wykonywania tej linii, co oznacza przy definicji funkcji.



4
Nie wątpię, że argumenty, które można modyfikować, naruszają zasadę najmniejszego zdziwienia dla przeciętnego człowieka i widziałem, jak wkraczają tam początkujący, a następnie heroicznie zastępują listy mailingowe krotkami mailingowymi. Niemniej jednak zmienne argumenty są nadal zgodne z Python Zen (Pep 20) i mieszczą się w klauzuli „oczywiste dla holenderskiego” (rozumianej / wykorzystywanej przez programistów pythonowych). Zalecane obejście z ciągiem dokumentów jest najlepsze, ale odporność na ciągi dokumentów i wszelkie (napisane) dokumenty nie są obecnie tak rzadkie. Osobiście wolałbym dekorator (powiedz @ naprawione_default).
Serge

5
Mój argument, kiedy się z tym spotykam, brzmi: „Dlaczego musisz utworzyć funkcję, która zwraca zmienną, którą opcjonalnie może być zmienną, którą przekazujesz do funkcji? Albo zmienia ona zmienną, albo tworzy nową. Dlaczego potrzebujesz zrobić jedno i drugie z jedną funkcją? Dlaczego interpreter powinien zostać przepisany, abyś mógł to zrobić bez dodawania trzech wierszy do kodu? ” Ponieważ mówimy o przepisaniu sposobu, w jaki interpreter obsługuje tutaj definicje funkcji i wywołania. To bardzo dużo do zrobienia w przypadku prawie niezbędnego przypadku użycia.
Alan Leuthard

12
„Początkujący w Pythonie oczekują, że ta funkcja zawsze zwraca listę zawierającą tylko jeden element: [5]” Jestem początkującym Python i nie spodziewałbym się tego, bo oczywiście foo([1])wróci [1, 5], nie [5]. To, co chciałeś powiedzieć, to to, że nowicjusz oczekiwałby, że funkcja wywoływana bez parametru zawsze będzie zwracać [5].
symplektomorficzny

2
W tym pytaniu zadaje się pytanie „Dlaczego tak [niewłaściwy sposób] został zaimplementowany?” Nie pyta: „Jaka jest właściwa droga?” , który jest objęty [ Dlaczego użycie arg = None rozwiązuje domyślny problem argumentów Pythona z możliwością zmiany? ] * ( stackoverflow.com/questions/10676729/… ). Nowi użytkownicy są prawie zawsze mniej zainteresowani tym pierwszym i znacznie bardziej zainteresowani drugim, więc czasami jest to bardzo użyteczny link / dupe do cytowania.
smci,

Odpowiedzi:


1612

W rzeczywistości nie jest to wada projektowa i nie wynika to z wewnętrznych elementów lub wydajności.
Wynika to po prostu z faktu, że funkcje w Pythonie są obiektami najwyższej klasy, a nie tylko fragmentem kodu.

Gdy tylko pomyślisz w ten sposób, ma to całkowicie sens: funkcja jest obiektem ocenianym na podstawie jej definicji; parametry domyślne są rodzajem „danych członkowskich” i dlatego ich stan może się zmieniać z jednego wywołania na drugie - dokładnie tak, jak w każdym innym obiekcie.

W każdym razie Effbot ma bardzo ładne wyjaśnienie przyczyn takiego zachowania w Domyślnych wartościach parametrów w Pythonie .
Uznałem to za bardzo jasne i naprawdę sugeruję przeczytanie go, aby uzyskać lepszą wiedzę na temat działania obiektów funkcyjnych.


80
Wszystkim, którzy czytają powyższą odpowiedź, zdecydowanie zalecam poświęcony czas na przeczytanie połączonego artykułu Effbot. Oprócz wszystkich innych przydatnych informacji, bardzo przydatna jest część dotycząca tego, w jaki sposób można użyć tej funkcji języka do buforowania wyników / zapamiętywania!
Cam Jackson

85
Nawet jeśli jest to obiekt pierwszej klasy, nadal można sobie wyobrazić projekt, w którym kod każdej wartości domyślnej jest przechowywany wraz z obiektem i poddawany ponownej ocenie przy każdym wywołaniu funkcji. Nie twierdzę, że byłoby lepiej, po prostu to, że funkcje będące obiektami pierwszej klasy nie wykluczają tego w pełni.
gerrit

312
Przepraszamy, ale wszystko, co uważane jest za „największy WTF w Pythonie”, jest zdecydowanie wadą projektową . Jest to źródło błędów dla wszystkich w pewnym momencie, ponieważ nikt nie spodziewa się takiego zachowania na początku - co oznacza, że ​​nie powinno być tak zaprojektowane od samego początku. Nie obchodzi mnie, przez jakie obręcze musieli przejść, powinni zaprojektować Pythona, aby domyślne argumenty nie były statyczne.
BlueRaja - Danny Pflughoeft

192
Niezależnie od tego, czy jest to wada projektowa, twoja odpowiedź wydaje się sugerować, że takie zachowanie jest w jakiś sposób konieczne, naturalne i oczywiste, biorąc pod uwagę, że funkcje są obiektami pierwszej klasy, i po prostu tak nie jest. Python ma zamknięcia. Jeśli zastąpisz domyślny argument przypisaniem w pierwszym wierszu funkcji, ocenia on każde wyrażenie (potencjalnie używając nazw zadeklarowanych w zakresie obejmującym). Nie ma żadnego powodu, aby ocena domyślnych argumentów za każdym razem, gdy funkcja jest wywoływana w dokładnie taki sam sposób, byłaby niemożliwa lub uzasadniona.
Mark Amery

24
Projekt nie wynika bezpośrednio z functions are objects. W twoim paradygmacie propozycja polegałaby na wprowadzeniu domyślnych wartości funkcji jako właściwości, a nie atrybutów.
bukzor

273

Załóżmy, że masz następujący kod

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Kiedy widzę deklarację jedzenia, najmniej zadziwiającą rzeczą jest myślenie, że jeśli pierwszy parametr nie zostanie podany, będzie równy krotce ("apples", "bananas", "loganberries")

Jednak, jak przypuszczam później w kodzie, robię coś takiego

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

wtedy, jeśli domyślne parametry byłyby powiązane podczas wykonywania funkcji, a nie deklaracji funkcji, byłbym zaskoczony (w bardzo zły sposób), aby odkryć, że owoce zostały zmienione. Byłoby to bardziej zadziwiające IMO niż odkrycie, że foopowyższa funkcja mutuje listę.

Prawdziwy problem leży w zmiennych zmiennych, a wszystkie języki mają do pewnego stopnia ten problem. Oto pytanie: załóżmy, że w Javie mam następujący kod:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Teraz, czy moja mapa używa wartości StringBuffer klucza, kiedy został umieszczony na mapie, czy też przechowuje klucz przez odniesienie? Tak czy inaczej, ktoś jest zaskoczony; albo osoba, która próbowała pozbyć się obiektu przy Mapużyciu wartości identycznej z tą, w której go umieściła, lub osoba, która nie mogła wydobyć swojego obiektu, nawet jeśli klucz, którego używa, jest dosłownie tym samym obiektem użyto go do umieszczenia go na mapie (właśnie dlatego Python nie zezwala na stosowanie wbudowanych typów danych jako zmiennych słownikowych).

Twój przykład jest dobrym przykładem przypadku, w którym nowi użytkownicy Pythona będą zaskoczeni i ugryzieni. Ale twierdzę, że jeśli to „naprawimy”, stworzy to tylko inną sytuację, w której zamiast tego zostaną ugryzieni, a ta będzie jeszcze mniej intuicyjna. Co więcej, zawsze tak jest w przypadku zmiennych zmiennych; zawsze zdarza się, że ktoś intuicyjnie może oczekiwać jednego lub przeciwnego zachowania w zależności od tego, jaki kod pisze.

Osobiście podoba mi się obecne podejście Pythona: domyślne argumenty funkcji są oceniane, gdy funkcja jest zdefiniowana i ten obiekt jest zawsze domyślny. Przypuszczam, że mogliby w specjalnym przypadku użyć pustej listy, ale tego rodzaju specjalna obudowa spowodowałaby jeszcze większe zdziwienie, nie mówiąc już o niezgodności wstecznej.


30
Myślę, że to kwestia debaty. Działasz na zmiennej globalnej. Każda ocena wykonana w dowolnym miejscu w kodzie, obejmująca zmienną globalną, będzie teraz (poprawnie) oznaczać („jagody”, „mango”). parametr domyślny może być jak każdy inny przypadek.
Stefano Borini

47
Właściwie nie sądzę, że zgadzam się z twoim pierwszym przykładem. Nie jestem pewien, czy podoba mi się pomysł modyfikacji takiego inicjalizatora, ale jeśli tak, spodziewam się, że będzie on zachowywał się dokładnie tak, jak opisano - zmieniając wartość domyślną na ("blueberries", "mangos").
Ben Blank

12
Domyślny parametr jest jak każdy inny przypadek. Nieoczekiwane jest to, że parametr jest zmienną globalną, a nie lokalną. To z kolei wynika z tego, że kod jest wykonywany przy definicji funkcji, a nie wywołaniu. Kiedy już to zdobędziesz i to samo dotyczy zajęć, wszystko będzie zupełnie jasne.
Lennart Regebro

17
Uważam ten przykład za mylący, a nie błyskotliwy. Jeśli some_random_function()dołączy się fruitszamiast przypisywać do niego, zachowanie eat() się zmieni. Tyle o obecnym cudownym projekcie. Jeśli użyjesz domyślnego argumentu, do którego odwołuje się gdzie indziej, a następnie zmodyfikujesz odwołanie spoza funkcji, poprosisz o kłopoty. Prawdziwy WTF ma miejsce, gdy ludzie definiują nowy domyślny argument (literał listy lub wywołanie konstruktora) i wciąż otrzymują bit.
Alexis

13
Właśnie jawnie zadeklarowałeś globali przypisałeś krotkę - nie ma absolutnie nic dziwnego, jeśli eatpotem działa inaczej.
user3467349,

241

Odpowiednia część dokumentacji :

Domyślne wartości parametrów są oceniane od lewej do prawej 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ść. Jest to szczególnie ważne, aby zrozumieć, kiedy domyślnym parametrem jest obiekt podlegający zmianom, taki jak lista lub słownik: jeśli funkcja modyfikuje obiekt (np. Przez dodanie elementu do listy), wartość domyślna jest w rzeczywistości modyfikowana. Zasadniczo nie jest to zamierzone. Rozwiązaniem tego Noneproblemu jest użycie go jako domyślnego i jawne przetestowanie go w treści funkcji, np .:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
Zwroty „to nie jest ogólnie to, co było zamierzone” i „sposób na obejście tego” pachnie, jakby dokumentowały wadę projektową.
bukzor

4
@Matthew: Jestem tego świadomy, ale nie warto wpadać w pułapkę. Z tego powodu generalnie zobaczysz przewodniki stylu i linters bezwarunkowo oznaczające zmienne wartości domyślne jako błędne. Bezpośrednim sposobem na zrobienie tego samego jest umieszczenie atrybutu w funkcji ( function.data = []) lub jeszcze lepiej, utworzenie obiektu.
bukzor

6
@bukzor: Pułapki należy odnotować i udokumentować, dlatego to pytanie jest dobre i otrzymało tak wiele pozytywnych opinii. Jednocześnie pułapki nie muszą być koniecznie usuwane. Ilu początkujących Python przekazało listę do funkcji, która ją zmodyfikowała, i byli zszokowani widokiem zmian w oryginalnej zmiennej? Jednak zmienne typy obiektów są wspaniałe, jeśli rozumiesz, jak ich używać. Myślę, że sprowadza się to tylko do opinii na temat tego konkretnego pułapki.
Matthew

33
Wyrażenie „to nie jest to, co było zamierzone”, oznacza „nie to, co naprawdę chciał programista”, nie „nie to, co powinien zrobić Python”.
holdenweb,

4
@holdenweb Wow, jestem spóźniony na imprezę. Biorąc pod uwagę kontekst, bukzor ma całkowitą rację: dokumentują zachowanie / konsekwencje, które nie były „zamierzone”, kiedy zdecydowali, że język powinien wykonać definicję funkcji. Ponieważ jest to niezamierzona konsekwencja ich wyboru projektu, jest to wada projektowa. Gdyby nie była to wada projektowa, nie byłoby nawet potrzeby oferowania „rozwiązania tego”.
code_dredd

118

Nic nie wiem o wewnętrznych działaniach interpretera Pythona (nie jestem też ekspertem od kompilatorów i tłumaczy), więc nie obwiniaj mnie, jeśli zaproponuję coś bezsensownego lub niemożliwego.

Pod warunkiem, że obiekty Pythona są zmienne , myślę, że należy to wziąć pod uwagę przy projektowaniu domyślnych argumentów. Kiedy tworzysz listę:

a = []

spodziewasz się uzyskać nową listę, do której odwołuje się a.

Czemu to mają a=[]w

def x(a=[]):

utworzyć nową listę definicji funkcji, a nie wywołania? To tak, jakbyś pytał: „jeśli użytkownik nie dostarczy argumentu, utwórz nową listę i użyj jej tak, jakby została utworzona przez dzwoniącego”. Myślę, że zamiast tego jest to niejednoznaczne:

def x(a=datetime.datetime.now()):

użytkownik, czy chcesz a domyślnie ustawić datę i godzinę odpowiadającą podczas definiowania lub wykonywania x? W tym przypadku, podobnie jak w poprzednim, zachowam to samo zachowanie, jakby domyślny argument „przypisanie” był pierwszą instrukcją funkcji ( datetime.now()wywoływaną przy wywołaniu funkcji). Z drugiej strony, jeśli użytkownik chciałby odwzorować czas definicji, mógłby napisać:

b = datetime.datetime.now()
def x(a=b):

Wiem, wiem: to jest zamknięcie. Alternatywnie Python może podać słowo kluczowe, aby wymusić powiązanie w czasie definicji:

def x(static a=b):

11
Możesz zrobić: def x (a = Brak): A jeśli a to Brak, ustaw a = datetime.datetime.now ()
Anon

20
Dziękuję Ci za to. Naprawdę nie mogłem po prostu wskazać, dlaczego denerwuje mnie to bez końca. Zrobiłeś to pięknie przy minimalnym zamieszaniu i zamieszaniu. Jako ktoś, kto przychodzi z programowania systemów w C ++ i czasami naiwnie „tłumaczy” funkcje językowe, ten fałszywy przyjaciel wbił mi głowę do głowy, podobnie jak atrybuty klas. Rozumiem, dlaczego tak jest, ale nie mogę nie polubić tego, bez względu na to, jakie pozytywne skutki mogą z tego wyniknąć. Przynajmniej jest to tak sprzeczne z moim doświadczeniem, że prawdopodobnie (mam nadzieję) nigdy tego nie zapomnę ...
AndreasT

5
@Andreas, gdy używasz Pythona wystarczająco długo, zaczynasz dostrzegać, jak logicznie interpretuje on Python jako atrybuty klas w taki sposób - dzieje się tak tylko z powodu szczególnych dziwactw i ograniczeń języków takich jak C ++ (i Java oraz C # ...), że ma sens class {}interpretowanie zawartości bloku jako należącej do instancji :) Ale kiedy klasy są obiektami pierwszej klasy, naturalną rzeczą jest, aby ich zawartość (w pamięci) odzwierciedlała ich zawartość (W kodzie).
Karl Knechtel

6
Struktura normatywna nie jest dziwactwem ani ograniczeniem w mojej książce. Wiem, że może być niezdarny i brzydki, ale można to nazwać „definicją” czegoś. Dynamiczne języki wydają mi się trochę anarchistyczne: na pewno wszyscy są wolni, ale potrzebujesz struktury, aby ktoś opróżnił śmieci i utorował drogę. Chyba jestem stary ... :)
AndreasT

4
Definicja funkcji jest wykonywana w czasie ładowania modułu. Funkcja ciało jest wykonywany w momencie wywołania funkcji. Domyślny argument jest częścią definicji funkcji, a nie treści funkcji. (To staje się bardziej skomplikowane dla funkcji zagnieżdżonych.)
Lutz Prechelt

84

Powodem jest po prostu to, że powiązania są wykonywane, gdy kod jest wykonywany, a definicja funkcji jest wykonywana, cóż ... kiedy funkcje są zdefiniowane.

Porównaj to:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Ten kod cierpi z powodu dokładnie tego samego nieoczekiwanego zdarzenia. banany to atrybut klasy, a zatem, kiedy dodajesz do niego różne rzeczy, jest on dodawany do wszystkich instancji tej klasy. Powód jest dokładnie taki sam.

Jest to po prostu „How It Works”, a sprawienie, by działało inaczej w przypadku funkcji, byłoby prawdopodobnie skomplikowane, aw przypadku klasy prawdopodobnie niemożliwe, lub przynajmniej spowolnienie tworzenia instancji obiektu, ponieważ musiałbyś zachować kod klasy w pobliżu i wykonaj go, gdy obiekty zostaną utworzone.

Tak, to nieoczekiwane. Ale gdy grosz spadnie, idealnie pasuje do ogólnego działania Pythona. W rzeczywistości jest to dobra pomoc dydaktyczna, a gdy zrozumiesz, dlaczego tak się dzieje, znacznie lepiej zrozumiesz Python.

To powiedziawszy, powinno to być wyraźnie widoczne w każdym dobrym tutorialu Python. Ponieważ, jak wspomniałeś, wszyscy prędzej czy później napotykają ten problem.


Jak zdefiniować atrybut klasy, który jest inny dla każdej instancji klasy?
Kieveli

19
Jeśli jest inny dla każdej instancji, nie jest to atrybut klasy. Atrybuty klas są atrybutami klasy. Stąd nazwa. Dlatego są takie same dla wszystkich instancji.
Lennart Regebro

1
Jak zdefiniować atrybut w klasie, który jest inny dla każdej instancji klasy? (Ponownie zdefiniowano dla tych, którzy nie mogli ustalić, że osoba nie obeznana z konwencjami nazewnictwa Pythona może pytać o normalne zmienne składowe klasy).
Kieveli

@Kievieli: Mówisz o normalnych zmiennych członków klasy. :-) Atrybuty instancji definiuje się przez powiedzenie self.attribute = wartość w dowolnej metodzie. Na przykład __init __ ().
Lennart Regebro

@Kieveli: Dwie odpowiedzi: nie możesz, ponieważ wszystko, co zdefiniujesz na poziomie klasy, będzie atrybutem klasy, a każda instancja, która uzyska dostęp do tego atrybutu, uzyska dostęp do tego samego atrybutu klasy; możesz / sortować /, używając propertys - które są w rzeczywistości funkcjami na poziomie klasy, które działają jak normalne atrybuty, ale zachowują atrybut w instancji zamiast w klasie (używając, self.attribute = valuejak powiedział Lennart).
Ethan Furman,

66

Dlaczego nie introspekujesz?

Jestem naprawdę zaskoczony, że nikt nie przeprowadził wnikliwej introspekcji oferowanej przez Python ( 2i3 zastosuj) w sprawie na żądanie.

Biorąc pod uwagę prostą małą funkcję funczdefiniowaną jako:

>>> def func(a = []):
...    a.append(5)

Gdy Python go napotka, pierwszą rzeczą, którą zrobi, jest skompilowanie go w celu utworzenia codeobiektu dla tej funkcji. Po zakończeniu tego kroku kompilacji Python ocenia *, a następnie przechowuje domyślne argumenty ( []tutaj pusta lista ) w samym obiekcie funkcji . Jak wspomniano w górnej odpowiedzi: listę amożna teraz uznać za członka funkcji func.

Więc zróbmy introspekcję, przed i po, aby sprawdzić, jak lista jest rozszerzana wewnątrz obiektu funkcji. Używam Python 3.xdo tego, w przypadku Python 2 to samo dotyczy (użyj __defaults__lub func_defaultsw Python 2; tak, dwie nazwy dla tej samej rzeczy).

Funkcja przed wykonaniem:

>>> def func(a = []):
...     a.append(5)
...     

Gdy Python wykona tę definicję, weźmie określone parametry domyślne ( a = []tutaj) i wrzuci je do __defaults__atrybutu obiektu funkcji (odpowiednia sekcja: Callables):

>>> func.__defaults__
([],)

Ok, więc pusta lista jako pojedynczy wpis __defaults__, zgodnie z oczekiwaniami.

Funkcja po wykonaniu:

Wykonajmy teraz tę funkcję:

>>> func()

Zobaczmy je __defaults__jeszcze raz:

>>> func.__defaults__
([5],)

Zdumiony? Wartość wewnątrz obiektu zmienia się! Kolejne wywołania funkcji będą teraz po prostu dołączane do tego osadzonego listobiektu:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Tak więc, oto dlaczego, ta „wada” ma miejsce, ponieważ domyślne argumenty są częścią obiektu funkcji. Nie dzieje się tu nic dziwnego, wszystko to jest trochę zaskakujące.

Typowym rozwiązaniem tego problemu jest użycie go Nonejako domyślnego, a następnie zainicjowanie w treści funkcji:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Ponieważ treść funkcji jest wykonywana od nowa za każdym razem, zawsze otrzymujesz nową, pustą listę, jeśli nie podano żadnego argumentu a.


Aby dodatkowo sprawdzić, czy lista na liście __defaults__jest taka sama jak ta używana w funkcji func, możesz po prostu zmienić swoją funkcję i zwrócić idlistę aużywaną w treści funkcji. Następnie porównaj go z listą w __defaults__(pozycja [0]w __defaults__), a zobaczysz, w jaki sposób odnoszą się one do tego samego wystąpienia listy:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Wszystko z siłą introspekcji!


* Aby sprawdzić, czy Python ocenia domyślne argumenty podczas kompilacji funkcji, spróbuj wykonać następujące czynności:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

jak zauważysz, input()jest wywoływany przed procesem budowania funkcji i wiązania jej z nazwą bar.


1
Czy id(...)potrzebna jest ostatnia weryfikacja, czy też isoperator odpowie na to samo pytanie?
das-g

1
@ das-g ismiałoby się dobrze, po prostu użyłem, id(val)ponieważ myślę, że może być bardziej intuicyjny.
Dimitris Fasarakis Hilliard

Używanie Nonejako domyślnego poważnie ogranicza użyteczność __defaults__introspekcji, więc nie sądzę, aby __defaults__działało to dobrze jako obrona posiadania pracy w taki sposób. Leniwa ocena zrobiłaby więcej, aby zachować domyślne wartości funkcji przydatne z obu stron.
Brilliand

58

Kiedyś myślałem, że lepszym rozwiązaniem byłoby tworzenie obiektów w czasie wykonywania. Jestem teraz mniej pewny, ponieważ tracisz kilka przydatnych funkcji, chociaż może być tego warte, aby po prostu zapobiec nieporozumieniom dla początkujących. Wady takiego działania to:

1. Wydajność

def foo(arg=something_expensive_to_compute())):
    ...

Jeśli używana jest ocena czasu wywołania, wówczas kosztowna funkcja jest wywoływana za każdym razem, gdy używana jest funkcja bez argumentu. Za każdą rozmowę zapłacisz wysoką cenę lub będziesz musiał ręcznie buforować wartość zewnętrznie, zanieczyszczając przestrzeń nazw i dodając gadatliwość.

2. Wymuszanie związanych parametrów

Przydatną sztuczką jest powiązanie parametrów lambda z bieżącym wiązaniem zmiennej podczas tworzenia lambda. Na przykład:

funcs = [ lambda i=i: i for i in range(10)]

Zwraca listę funkcji, które zwracają odpowiednio 0,1,2,3 ... Jeśli zachowanie zostanie zmienione, zamiast tego powiążą isię z wartością czasu wywołania i, aby uzyskać listę funkcji, które wszystkie zwróciły 9.

Jedynym sposobem na wdrożenie tego w innym przypadku byłoby utworzenie dalszego zamknięcia z i-związanym, tj .:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspekcja

Rozważ kod:

def foo(a='test', b=100, c=[]):
   print a,b,c

Możemy uzyskać informacje na temat argumentów i wartości domyślnych za pomocą inspectmodułu, który

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Ta informacja jest bardzo przydatna w przypadku generowania dokumentów, metaprogramowania, dekoratorów itp.

Załóżmy teraz, że zachowanie wartości domyślnych można zmienić, tak aby odpowiadało to:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

Jednak straciliśmy zdolność do introspekcji i zobacz, jakie argumenty domyślne . Ponieważ obiekty nie zostały zbudowane, nigdy nie możemy ich zdobyć bez faktycznego wywołania funkcji. Najlepsze, co moglibyśmy zrobić, to zapisać kod źródłowy i zwrócić go jako ciąg znaków.


1
można osiągnąć introspekcję także wtedy, gdy dla każdej z nich istnieje funkcja tworzenia domyślnego argumentu zamiast wartości. moduł kontrolny po prostu wywoła tę funkcję.
yairchu

@SilentGhost: Mówię o tym, czy zachowanie zostało zmienione, aby je odtworzyć - utworzenie go raz jest bieżącym zachowaniem i dlaczego istnieje domyślny problem z możliwością zmiany.
Brian

1
@yairchu: Zakłada się, że konstrukcja jest bezpieczna (tzn. nie ma skutków ubocznych). Przebadanie argumentów nie powinno nic robić , ale ocena dowolnego kodu mogłaby przynieść efekt.
Brian

1
Inny projekt języka często oznacza po prostu pisanie różnych rzeczy. Twój pierwszy przykład można łatwo napisać jako: _expensive = drogi (); def foo (arg = _prawne), jeśli nie chcesz, aby było to ponownie oceniane.
Glenn Maynard

@Glenn - do tego miałem na myśli, mówiąc „buforuj zmienną zewnętrznie” - jest to trochę bardziej szczegółowe i kończysz z dodatkowymi zmiennymi w twojej przestrzeni nazw.
Brian

55

5 punktów w obronie Pythona

  1. Prostota : zachowanie jest proste w następującym znaczeniu: Większość ludzi wpada w tę pułapkę tylko raz, nie kilka razy.

  2. Spójność : Python zawsze przekazuje obiekty, a nie nazwy. Domyślny parametr jest oczywiście częścią nagłówka funkcji (a nie treści funkcji). Dlatego powinien być oceniany w czasie ładowania modułu (i tylko w czasie ładowania modułu, chyba że jest zagnieżdżony), a nie w czasie wywołania funkcji.

  3. Przydatność : Jak zauważa Frederik Lundh w swoim wyjaśnieniu „Domyślne wartości parametrów w Pythonie” , obecne zachowanie może być bardzo przydatne w zaawansowanym programowaniu. (Używaj oszczędnie.)

  4. Wystarczająca dokumentacja : w najbardziej podstawowej dokumentacji Pythona, samouczku, problem został głośno ogłoszony jako „Ważne ostrzeżenie” w pierwszej podsekcji sekcji „Więcej o definiowaniu funkcji” . Ostrzeżenie używa nawet pogrubionej czcionki, która rzadko jest stosowana poza nagłówkami. RTFM: Przeczytaj dokładny podręcznik.

  5. Meta-learning : Wpadnięcie w pułapkę jest w rzeczywistości bardzo pomocnym momentem (przynajmniej jeśli jesteś uczniem refleksyjnym), ponieważ później lepiej zrozumiesz powyższy punkt „Spójność”, który nauczy Cię wiele o Pythonie.


18
Zajęło mi rok odkrycie, że takie zachowanie psuje mój kod na produkcji, w końcu usunąłem kompletną funkcję, aż przypadkiem wpadłem na tę wadę projektową. Używam Django. Ponieważ środowisko pomostowe nie miało wielu żądań, ten błąd nigdy nie miał żadnego wpływu na kontrolę jakości. Kiedy uruchomiliśmy i otrzymaliśmy wiele równoczesnych żądań - niektóre funkcje narzędzia zaczęły się wzajemnie nadpisywać! Robić dziury bezpieczeństwa, błędy i co nie.
oriadam

7
@oriadam, bez obrazy, ale zastanawiam się, jak nauczyłeś się Pythona bez wcześniejszego napotkania tego. Właśnie uczę się teraz języka Python i ta możliwa pułapka jest wspomniana w oficjalnym samouczku języka Python obok pierwszej wzmianki o domyślnych argumentach. (Jak wspomniano w punkcie 4 tej odpowiedzi.) Przypuszczam, że morałem jest - raczej niesympatycznie - czytanie oficjalnych dokumentów języka, którego używasz do tworzenia oprogramowania produkcyjnego.
Wildcard,

Byłoby również zaskakujące (dla mnie), gdyby wywołano funkcję o nieznanej złożoności oprócz wywołania funkcji, które wykonuję.
Vatine

52

To zachowanie można łatwo wytłumaczyć:

  1. deklaracja funkcji (klasy itp.) jest wykonywana tylko raz, tworząc wszystkie obiekty wartości domyślnych
  2. wszystko jest przekazywane przez odniesienie

Więc:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a nie zmienia się - każde wywołanie przypisania tworzy nowy obiekt int - drukowany jest nowy obiekt
  2. b nie zmienia się - nowa tablica jest budowana z wartości domyślnej i drukowana
  3. c zmiany - operacja wykonywana jest na tym samym obiekcie - i jest drukowana

(Właściwie add jest złym przykładem, ale moim głównym punktem jest niezmienność liczb całkowitych.)
Anon

Uświadomiłem sobie to po moim rozczarowaniu po sprawdzeniu, czy przy b ustawionym na [], b .__ add __ ([1]) zwraca [1], ale także pozostawia b nadal [], mimo że listy są zmienne. Mój błąd.
Anon

@ANon: istnieje __iadd__, ale nie działa z int. Oczywiście. :-)
Veky

35

Pytasz, dlaczego:

def func(a=[], b = 2):
    pass

nie jest wewnętrznie równoważny z tym:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

z wyjątkiem przypadku jawnego wywołania func (Brak, Brak), które zignorujemy.

Innymi słowy, zamiast oceniać parametry domyślne, dlaczego nie zapisać każdego z nich i ocenić je po wywołaniu funkcji?

Prawdopodobnie jest jedna odpowiedź - skutecznie zamieniłby każdą funkcję z domyślnymi parametrami w zamknięcie. Nawet jeśli wszystko jest ukryte w tłumaczu, a nie pełne zamknięcie, dane muszą być gdzieś przechowywane. Byłoby wolniej i zajmowałoby więcej pamięci.


6
Nie musiałoby to być zamknięcie - lepszym sposobem, aby o tym pomyśleć, byłoby po prostu ustawienie kodu bajtowego jako domyślnego na pierwszy wiersz kodu - mimo wszystko kompilacja w tym momencie i tak - nie ma żadnej różnicy między kodem w argumentach i kodzie w treści.
Brian

10
To prawda, ale nadal spowalniałoby Pythona i faktycznie byłoby dość zaskakujące, chyba że zrobiłbyś to samo dla definicji klas, co spowodowałoby głupią powolność, ponieważ musiałbyś ponownie uruchamiać całą definicję klasy za każdym razem, gdy tworzysz klasa. Jak wspomniano, poprawka byłaby bardziej zaskakująca niż problem.
Lennart Regebro

Uzgodniony z Lennartem. Jak mówi Guido, w każdej funkcji językowej lub standardowej bibliotece jest ktoś , kto z niej korzysta.
Jason Baker

6
Zmiana teraz byłaby obłędem - po prostu badamy, dlaczego tak jest. Gdyby na początku dokonał późnej oceny domyślnej, niekoniecznie byłoby to zaskakujące. To z pewnością prawda, że ​​taka podstawowa różnica w analizie składni miałaby szerokie, i prawdopodobnie wiele niejasnych, wpływ na język jako całość.
Glenn Maynard

35

1) Tak zwany problem „Zmiennego argumentu domyślnego” jest ogólnie szczególnym przykładem pokazującym, że:
„Wszystkie funkcje z tym problemem cierpią również z powodu podobnego problemu skutków ubocznych rzeczywistego parametru ”,
co jest sprzeczne z zasadami programowania funkcjonalnego, zwykle nieistotne i należy je naprawić razem.

Przykład:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Rozwiązanie : a kopia
Absolutnie bezpieczny rozwiązaniem jest copyalbo deepcopypierwsza, a następnie obiekt wejściowy zrobić cokolwiek z kopią.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Wiele wbudowanych zmiennych typów ma metodę kopiowania, taką jak some_dict.copy()lub, some_set.copy()i można je łatwo skopiować, np . somelist[:]Lub list(some_list). Każdy obiekt może być również kopiowany copy.copy(any_object)lub dokładniej przez copy.deepcopy()(ten ostatni jest użyteczny, jeśli obiekt zmienny składa się z obiektów zmiennych). Niektóre obiekty są zasadniczo oparte na efektach ubocznych, takich jak obiekt „plik” i nie mogą być w znaczący sposób powielone przez kopiowanie. biurowy

Przykład problemu dla podobnego pytania SO

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Nie należy go zapisywać w żadnym publicznym atrybucie instancji zwróconym przez tę funkcję. (Zakładając, że prywatne atrybuty instancji nie powinny być modyfikowane spoza tej klasy lub podklas zgodnie z konwencją, tj_var1 Jest atrybutem prywatnym)

Wniosek:
obiektów parametrów wejściowych nie należy modyfikować (mutować) ani nie należy ich wiązać z obiektami zwracanymi przez funkcję. (Jeśli wolimy programować bez efektów ubocznych, co jest zdecydowanie zalecane. Zobacz Wiki o „skutkach ubocznych” (w tym kontekście pierwsze dwa akapity są odpowiednie).)

2)
Tylko wtedy, gdy wymagany jest efekt uboczny rzeczywistego parametru, ale niepożądany w przypadku parametru domyślnego, użytecznym rozwiązaniem jest def ...(var1=None): if var1 is None: var1 = [] Więcej ..

3) W niektórych przypadkach użyteczne jest zachowanie zmiennych parametrów domyślnych .


5
Mam nadzieję, że wiesz, że Python nie jest funkcjonalnym językiem programowania.
Veky

6
Tak, Python jest językiem opartym na wielu paragigmach z pewnymi funkcjami. („Nie sprawiaj, że każdy problem wygląda jak gwóźdź tylko dlatego, że masz młot.”) Wiele z nich jest w najlepszych praktykach Pythona. Python ma ciekawe programowanie funkcjonalne HOWTO Inne funkcje to zamykanie i curry, nie wymienione tutaj.
hynekcer

1
Dodam również, na tym późnym etapie, że semantyka przypisania Pythona została zaprojektowana wyraźnie, aby w razie potrzeby unikać kopiowania danych, więc tworzenie kopii (a zwłaszcza głębokich kopii) wpłynie negatywnie zarówno na czas działania, jak i użycie pamięci. Dlatego należy ich używać tylko wtedy, gdy jest to konieczne, ale nowicjusze często mają trudności z ich zrozumieniem.
holdenweb,

1
@holdenweb Zgadzam się. Tymczasowa kopia jest najczęstszym sposobem, a czasem jedynym możliwym sposobem ochrony oryginalnych zmiennych danych przed zewnętrzną funkcją, która potencjalnie je modyfikuje. Na szczęście funkcja, która w nieuzasadniony sposób modyfikuje dane, jest uważana za błąd i dlatego jest rzadka.
hynekcer

Zgadzam się z tą odpowiedzią. I nie rozumiem, dlaczego def f( a = None )konstrukcja jest zalecana, jeśli naprawdę masz na myśli coś innego. Kopiowanie jest w porządku, ponieważ nie powinieneś mutować argumentów. A kiedy to zrobisz if a is None: a = [1, 2, 3], i tak skopiujesz listę.
koddo

30

W rzeczywistości nie ma to nic wspólnego z wartościami domyślnymi, poza tym, że często pojawia się jako nieoczekiwane zachowanie podczas pisania funkcji ze zmiennymi wartościami domyślnymi.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

Brak widocznych wartości domyślnych w tym kodzie, ale występuje dokładnie ten sam problem.

Problemem jest to, że foojest modyfikowanie jest zmienny zmienną przekazaną z rozmówcą, gdy rozmówca nie spodziewa się tego. Taki kod byłby w porządku, gdyby funkcja została wywołana w podobny sposóbappend_5 ; wtedy osoba wywołująca wywołałaby funkcję w celu zmodyfikowania przekazywanej wartości i oczekiwane jest zachowanie. Ale taka funkcja nie byłaby w stanie przyjąć domyślnego argumentu i prawdopodobnie nie zwróciłaby listy (ponieważ wywołujący już ma odniesienie do tej listy; do tej, którą właśnie przekazał).

Oryginał fooz domyślnym argumentem nie powinien modyfikować, aczy został jawnie przekazany, czy otrzymał wartość domyślną. Twój kod powinien pozostawić zmienne argumenty w spokoju, chyba że z kontekstu / nazwy / dokumentacji jasno wynika, że ​​argumenty powinny zostać zmodyfikowane. Używanie zmiennych wartości przekazywanych jako argumenty jako tymczasowe zasoby lokalne jest bardzo złym pomysłem, niezależnie od tego, czy jesteśmy w Pythonie, czy nie, i czy w grę wchodzą domyślne argumenty.

Jeśli potrzebujesz w sposób destrukcyjny manipulować lokalnym tymczasowym podczas obliczania czegoś i musisz rozpocząć manipulację od wartości argumentu, musisz wykonać kopię.


7
Chociaż są powiązane, myślę, że jest to odrębne zachowanie (ponieważ spodziewamy appendsię zmiany a„na miejscu”). To, że domyślna zmienna nie jest ponownie tworzona przy każdym wywołaniu, jest „nieoczekiwanym” bitem… przynajmniej dla mnie. :)
Andy Hayden

2
@AndyHayden jeśli jest funkcja oczekuje modyfikować argumentu, dlaczego miałoby to sens mają domyślne?
Mark Ransom

@ MarkRansom jedynym przykładem, jaki mogę wymyślić, jest cache={}. Podejrzewam jednak, że to „najmniejsze zdziwienie” pojawia się, gdy nie oczekujesz (lub nie chcesz) funkcji, którą wywołujesz, aby mutować argument.
Andy Hayden

1
@AndyHayden Zostawiłem tutaj swoją własną odpowiedź z rozszerzeniem tego sentymentu. Powiedz mi co myślisz. Mogę dodać do tego twój przykład cache={}.
Mark Ransom

1
@AndyHayden Chodzi mi o to, że jeśli kiedykolwiek zdumiewa cię przypadkowa mutacja domyślnej wartości argumentu, to masz inny błąd, a mianowicie twój kod może przypadkowo mutować wartość dzwoniącego, gdy domyślna nie była używana. I zauważ, że użycie Nonei przypisanie rzeczywistej wartości domyślnej, jeśli arg None nie rozwiązuje tego problemu (z tego powodu uważam, że jest to anty-wzorzec). Jeśli naprawisz drugi błąd, unikając mutowania wartości argumentów, niezależnie od tego, czy mają one wartości domyślne, nigdy nie zauważysz tego „zadziwiającego” zachowania.
Ben

27

Już zajęty temat, ale z tego, co tutaj przeczytałem, następujące pomogły mi zrozumieć, jak działa wewnętrznie:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
w rzeczywistości może to być nieco mylące dla początkujących, ponieważ a = a + [1]przeciążenia a... rozważ zmianę na b = a + [1] ; print id(b)i dodanie linii a.append(2). To sprawi, że bardziej oczywiste będzie, że +na dwóch listach zawsze tworzona jest nowa lista (przypisana do b), podczas gdy zmodyfikowana amoże mieć tę samą id(a).
Jörn Hees,

25

To optymalizacja wydajności. Które z tych dwóch wywołań funkcji, zdaniem tej funkcji, są szybsze?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Dam ci podpowiedź. Oto demontaż (patrz http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2)

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Wątpię, czy doświadczone zachowanie ma praktyczne zastosowanie (kto tak naprawdę używał zmiennych statycznych w C, bez rozmnażania błędów?)

Jak widać, nie ma korzyści wydajność podczas korzystania niezmienne domyślne argumenty. Może to mieć znaczenie, jeśli jest to często wywoływana funkcja lub domyślny argument zajmuje dużo czasu. Pamiętaj też, że Python nie jest C. W C masz stałe, które są prawie bezpłatne. W Pythonie nie masz tej korzyści.


24

Python: Zmienny domyślny argument

Domyślne argumenty są obliczane podczas kompilacji funkcji w obiekt funkcji. Wielokrotnie używane przez funkcję, są i pozostają tym samym obiektem.

Gdy są mutowalne, gdy są mutowane (na przykład poprzez dodanie do niego elementu), pozostają mutowane podczas kolejnych wywołań.

Pozostają zmutowane, ponieważ za każdym razem są tym samym obiektem.

Kod ekwiwalentny:

Ponieważ lista jest powiązana z funkcją podczas kompilacji i tworzenia instancji obiektu funkcji, to:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

jest prawie dokładnie równoważny z tym:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Demonstracja

Oto demonstracja - możesz za każdym razem sprawdzić, czy są one tym samym obiektem

  • widząc, że lista jest tworzona przed zakończeniem kompilacji funkcji do obiektu funkcji,
  • zauważając, że identyfikator jest taki sam przy każdym odwołaniu do listy,
  • zauważając, że lista pozostaje zmieniona, gdy funkcja, która jej używa, jest wywoływana po raz drugi,
  • przestrzegając kolejności, w której wydruk jest drukowany ze źródła (którą dla ciebie wygodnie ponumerowałem):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

i działając z python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Czy to narusza zasadę „najmniejszego zdziwienia”?

Ta kolejność wykonywania jest często myląca dla nowych użytkowników Pythona. Jeśli rozumiesz model wykonania Pythona, staje się on całkiem oczekiwany.

Zwykła instrukcja dla nowych użytkowników Python:

Ale właśnie dlatego zwykłą instrukcją dla nowych użytkowników jest tworzenie ich domyślnych argumentów:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Wykorzystuje singleton None jako obiekt wartownika, aby powiedzieć funkcji, czy otrzymaliśmy argument inny niż domyślny. Jeśli nie otrzymamy żadnego argumentu, faktycznie chcemy użyć nowej pustej listy [], jako domyślnej.

Jak mówi samouczek dotyczący przepływu sterowania :

Jeśli nie chcesz, aby domyślne były współużytkowane między kolejnymi wywołaniami, możesz zamiast tego napisać taką funkcję:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

Najkrótszą odpowiedzią byłoby prawdopodobnie „definicja to wykonanie”, dlatego cały argument nie ma ścisłego sensu. Jako bardziej wymyślny przykład możesz przytoczyć to:

def a(): return []

def b(x=a()):
    print x

Mamy nadzieję, że wystarczy pokazać, że niewykonanie domyślnych wyrażeń argumentów w czasie wykonywania definstrukcji nie jest łatwe lub nie ma sensu, albo jedno i drugie.

Zgadzam się jednak, że to gotcha, gdy próbujesz użyć domyślnych konstruktorów.


20

Proste obejście przy użyciu Brak

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

To zachowanie nie jest zaskakujące, jeśli weźmiesz pod uwagę następujące kwestie:

  1. Zachowanie atrybutów klasy tylko do odczytu podczas prób przypisania i to
  2. Funkcje są obiektami (dobrze wyjaśnione w przyjętej odpowiedzi).

Rola (2) została szeroko omówiona w tym wątku. (1) jest prawdopodobnie czynnikiem wywołującym zdumienie, ponieważ zachowanie to nie jest „intuicyjne”, gdy pochodzi z innych języków.

(1) opisano w samouczku dotyczącym klas w języku Python . Próbując przypisać wartość do atrybutu klasy tylko do odczytu:

... wszystkie zmienne znalezione poza najbardziej wewnętrznym zakresem są tylko do odczytu ( próba zapisu do takiej zmiennej po prostu utworzy nową zmienną lokalną w najbardziej wewnętrznym zakresie, pozostawiając niezmienioną zmienną zewnętrzną o identycznej nazwie ).

Wróć do oryginalnego przykładu i rozważ powyższe punkty:

def foo(a=[]):
    a.append(5)
    return a

Oto fooobiekt i aatrybut foo(dostępny w foo.func_defs[0]). Ponieważ ajest listą, amożna ją modyfikować i dlatego jest atrybutem odczytu i zapisu foo. Jest inicjowany do pustej listy określonej przez sygnaturę podczas tworzenia instancji funkcji i jest dostępny do odczytu i zapisu, dopóki istnieje obiekt funkcji.

Wywołanie foobez przesłaniania wartości domyślnej wykorzystuje wartość domyślną z foo.func_defs. W tym przypadku foo.func_defs[0]jest używany aw zakresie kodu obiektu funkcji. Zmiany do azmiany foo.func_defs[0], która jest częścią fooobiektu i utrzymuje się między wykonaniem kodu w foo.

Teraz porównaj to z przykładem z dokumentacji dotyczącej emulacji domyślnego zachowania argumentów w innych językach , tak że domyślne ustawienia sygnatury funkcji są używane przy każdym uruchomieniu funkcji:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Biorąc pod uwagę (1) i (2) , można zobaczyć, dlaczego osiąga to pożądane zachowanie:

  • Gdy fooinstancja obiektu funkcji foo.func_defs[0]jest ustawiona None, ustawiana jest na obiekt niezmienny.
  • Gdy funkcja jest wykonywana z ustawieniami domyślnymi (bez parametru określonego Lw wywołaniu funkcji), foo.func_defs[0]( None) jest dostępne w zasięgu lokalnym jako L.
  • Po L = []tym przypisanie nie może się powieść foo.func_defs[0], ponieważ ten atrybut jest tylko do odczytu.
  • Za (1) , nowa zmienna lokalna także nazwany Ljest tworzony w zakresie lokalnym i wykorzystywane do pozostałej części wywołania funkcji. foo.func_defs[0]dlatego pozostaje niezmieniony dla przyszłych inwokacji foo.

19

Pokażę alternatywną strukturę do przekazywania domyślnej wartości listy do funkcji (działa równie dobrze z słownikami).

Jak inni szeroko komentowali, parametr listy jest powiązany z funkcją, gdy jest zdefiniowany, a nie podczas wykonywania. Ponieważ listy i słowniki można modyfikować, wszelkie zmiany tego parametru wpłyną na inne wywołania tej funkcji. W rezultacie kolejne wywołania funkcji otrzymają tę wspólną listę, która mogła zostać zmieniona przez inne wywołania funkcji. Co gorsza, dwa parametry wykorzystują parametr wspólny tej funkcji w tym samym czasie, nieświadomy zmian wprowadzonych przez drugą.

Zła metoda (prawdopodobnie ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Możesz sprawdzić, czy są one jednym i tym samym obiektem, używając id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Bretta Slatkina „Skuteczny Python: 59 konkretnych sposobów na lepsze pisanie w Pythonie”, pozycja 20: Użycie Nonei ciągi znaków do określenia dynamicznych domyślnych argumentów (s. 48)

Konwencja osiągania pożądanego wyniku w Pythonie polega na podaniu wartości domyślnej Nonei udokumentowaniu rzeczywistego zachowania w dokumentacji.

Ta implementacja zapewnia, że ​​każde wywołanie funkcji otrzyma listę domyślną lub listę przekazaną do funkcji.

Preferowana metoda :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Mogą istnieć uzasadnione przypadki użycia „niewłaściwej metody”, w których programista zamierzał udostępnić domyślny parametr listy, ale jest to bardziej wyjątek niż reguła.


17

Oto rozwiązania:

  1. Użyj Nonejako wartości domyślnej (lub wartości jednorazowej object) i włącz ją, aby utworzyć wartości w czasie wykonywania; lub
  2. Użyj a lambdajako parametru domyślnego i wywołaj go w bloku try, aby uzyskać wartość domyślną (do tego właśnie służy abstrakcja lambda).

Druga opcja jest przydatna, ponieważ użytkownicy funkcji mogą przejść na żądanie, które może już istnieć (np. A type)


16

Kiedy to robimy:

def foo(a=[]):
    ...

... przypisujemy argument ado listy nienazwanej , jeśli program wywołujący nie przejdzie wartości a.

Aby uprościć tę dyskusję, nadajmy tymczasowo nazwę nienazwanej liście. Jak o pavlo?

def foo(a=pavlo):
   ...

W dowolnym momencie, jeśli dzwoniący nie powie nam, co to ajest, ponownie go wykorzystamy pavlo.

Jeśli pavlojest modyfikowalny (modyfikowalny) i fooostatecznie go modyfikuje, efekt, który zauważamy następnym razem, foojest wywoływany bez określania a.

Oto, co widzisz (pamiętaj, pavlojest zainicjowany na []):

 >>> foo()
 [5]

Teraz pavlojest [5].

foo()Ponowne wywołanie zmienia się pavloponownie:

>>> foo()
[5, 5]

Określanie, akiedy dzwonienie foo()zapewnia, że pavlonie zostanie zmienione.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Tak pavlojest nadal [5, 5].

>>> foo()
[5, 5, 5]

16

Czasami wykorzystuję to zachowanie jako alternatywę dla następującego wzorca:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Jeśli singletonjest używany tylko przez use_singleton, podoba mi się następujący wzór:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Użyłem tego do tworzenia klas klientów, które uzyskują dostęp do zasobów zewnętrznych, a także do tworzenia nagrań lub list do zapamiętywania.

Ponieważ nie sądzę, aby ten wzór był dobrze znany, zamieszczam krótki komentarz, aby uchronić się przed przyszłymi nieporozumieniami.


2
Wolę dodać dekorator do zapamiętywania i umieścić pamięć podręczną zapamiętywania na samym obiekcie funkcji.
Stefano Borini,

Ten przykład nie zastępuje wyświetlanego bardziej złożonego wzorca, ponieważ wywołujesz _make_singletonw czasie def w domyślnym przykładzie argumentu, ale w czasie wywołania w przykładzie globalnym. Prawdziwe podstawienie użyłoby pewnego rodzaju zmiennego pola dla domyślnej wartości argumentu, ale dodanie argumentu umożliwia przekazanie alternatywnych wartości.
Yann Vernier,

15

Możesz obejść ten problem, zastępując obiekt (a zatem remis z lunetą):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Brzydkie, ale działa.


3
To dobre rozwiązanie w przypadkach, gdy używasz oprogramowania do automatycznego generowania dokumentacji do dokumentowania typów argumentów oczekiwanych przez funkcję. Ustawienie a = None, a następnie ustawienie a na [], jeśli a jest None, nie pomaga czytelnikowi w zrozumieniu, czego można się spodziewać.
Michael Scott Cuthbert

Fajny pomysł: ponowne powiązanie tej nazwy gwarantuje, że nigdy nie będzie można jej zmienić. Naprawdę to lubię.
holdenweb,

To jest dokładnie taki sposób, aby to zrobić. Python nie tworzy kopii parametru, więc to od Ciebie zależy, czy kopia będzie jawna. Gdy masz kopię, możesz ją modyfikować bez żadnych niespodziewanych efektów ubocznych.
Mark Ransom,

13

Może być prawdą, że:

  1. Ktoś korzysta z każdej funkcji języka / biblioteki i
  2. Zmiana zachowania tutaj nie byłaby wskazana, ale

trzymanie się obu powyższych funkcji jest całkowicie spójne, a mimo to jeszcze jedno:

  1. Jest to myląca funkcja i jest niefortunna w Pythonie.

Inne odpowiedzi, a przynajmniej niektóre z nich albo robią punkty 1 i 2, ale nie 3, lub robią punkt 3 i umniejszają punkty 1 i 2. Ale wszystkie trzy są prawdziwe.

Może być prawdą, że zamiana koni w środkowej części rzeki wymagałaby znacznego zerwania i że może być więcej problemów związanych ze zmianą Pythona, aby intuicyjnie obsługiwać fragment otwierający Stefano. I może być prawdą, że ktoś, kto dobrze znał wewnętrzne elementy Pythona, może wyjaśnić pole minowe konsekwencji.Jednak,

Istniejące zachowanie nie jest w języku Python, a Python odnosi sukcesy, ponieważ bardzo niewiele w języku narusza zasadę najmniejszego zdziwienia w dowolnym miejscu pobliżuto źle. To prawdziwy problem, niezależnie od tego, czy mądrze byłoby go wykorzenić. To wada projektowa. Jeśli rozumiesz język znacznie lepiej, próbując wyśledzić zachowanie, mogę powiedzieć, że C ++ robi to wszystko i wiele więcej; dużo się uczysz, nawigując na przykład subtelne błędy wskaźnika. Ale to nie jest Python: ludzie, którzy dbają o Pythona na tyle, by wytrwać w obliczu tego zachowania, to ludzie, których pociąga język, ponieważ Python ma znacznie mniej niespodzianek niż inny język. Dabblery i ciekawi stają się Pythonistami, gdy są zdumieni, jak mało czasu potrzeba, aby coś zadziałało - nie z powodu projektu fl - mam na myśli ukrytą łamigłówkę logiczną - która jest sprzeczna z intuicją programistów, których pociąga Python ponieważ to po prostu działa .


6
-1 Chociaż jest to perspektywa możliwa do obrony, nie jest to odpowiedź i nie zgadzam się z nią. Zbyt wiele wyjątków rodzi własne narożne skrzynki.
Marcin

3
Zatem „zadziwiająco ignoranckie” jest twierdzenie, że w Pythonie bardziej sensowny byłby domyślny argument [], aby pozostawał [] za każdym razem, gdy funkcja jest wywoływana?
Christos Hayward

3
I ignorowanie nie jest traktowane jako niefortunny idiom ustawiający domyślny argument na Brak, a następnie w treści ustawienia funkcji, jeśli argument == Brak: argument = []? Czy ignorowanie tego idiomu jest niefortunne, ponieważ ludzie często chcą tego, czego oczekiwałby naiwny nowicjusz, że jeśli przypiszesz f (argument = []), argument automatycznie przejdzie do wartości []?
Christos Hayward

3
Ale w Pythonie duchem języka jest to, że nie musisz brać zbyt wielu głębokich nurkowań; array.sort () działa i działa niezależnie od tego, jak mało rozumiesz sortowanie, big-O i stałe. Piękno Pythona w mechanizmie sortowania tablic, aby dać jeden z niezliczonych przykładów, polega na tym, że nie musisz głęboko zanurzać się w wewnętrzne elementy. I inaczej mówiąc, piękno Pythona polega na tym, że zwykle nie jest wymagane głębokie zanurzenie się we wdrażaniu, aby uzyskać coś, co po prostu działa. I jest obejście (... jeśli argument == Brak: argument = []), NIEPRAWIDŁOWY.
Christos Hayward

3
Jako samodzielna instrukcja x=[]oznacza „utwórz pusty obiekt listy i powiąż z nim nazwę„ x ”.” Tak więc w def f(x=[])tworzona jest również pusta lista. Nie zawsze jest związany z x, więc zamiast tego zostaje przypisany do domyślnego surogatu. Później, gdy wywoływana jest funkcja f (), wartość domyślna jest wyciągana i przypisywana do x. Ponieważ sama pusta lista została wyparta, ta sama lista jest jedyną dostępną opcją dla x, niezależnie od tego, czy coś w niej utknęło, czy nie. Jak mogłoby być inaczej?
Jerry B

10

To nie jest wada projektowa . Każdy, kto się o to potknie, robi coś złego.

Widzę 3 przypadki, w których możesz napotkać ten problem:

  1. Zamierzasz zmodyfikować argument jako efekt uboczny funkcji. W takim przypadku domyślny argument nigdy nie ma sensu . Jedynym wyjątkiem jest nadużywanie listy argumentów do posiadania atrybutów funkcji, npcache={} Nie będzie się wymagać, aby wywoływać funkcję z faktycznym argumentem.
  2. Zamierzasz pozostawić argument niezmodyfikowany, ale przypadkowo tak zrobiłeś zmodyfikowałeś. To błąd, napraw go.
  3. Zamierzasz zmodyfikować argument do użycia wewnątrz funkcji, ale nie spodziewałeś się, że modyfikacja będzie widoczna poza funkcją. W takim przypadku musisz wykonać kopię argumentu, niezależnie od tego, czy był on domyślny, czy nie! Python nie jest językiem typu call-by-value, więc nie tworzy kopii dla ciebie, musisz o tym wyraźnie powiedzieć.

Przykład w pytaniu może należeć do kategorii 1 lub 3. Dziwne, że zarówno modyfikuje on przekazaną listę, jak i zwraca ją; powinieneś wybrać jedno lub drugie.


„Robienie czegoś złego” to diagnoza. To powiedziawszy, myślę, że były czasy = żaden wzorzec nie jest użyteczny, ale generalnie nie chcesz modyfikować, jeśli przeszedłeś w tym przypadku zmienną (2). Ten cache={}wzorzec jest naprawdę rozwiązaniem tylko do rozmowy kwalifikacyjnej, w prawdziwym kodzie prawdopodobnie chcesz @lru_cache!
Andy Hayden

9

Ten „błąd” dał mi wiele godzin nadgodzin! Ale zaczynam dostrzegać potencjalne zastosowanie tego (ale wolałbym, żeby to było w czasie wykonywania)

Dam ci to, co uważam za użyteczny przykład.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

drukuje następujące

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Po prostu zmień funkcję na:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Myślę, że odpowiedź na to pytanie polega na tym, w jaki sposób Python przekazuje dane do parametru (przekazanie przez wartość lub przez referencję), a nie na zmienności lub w jaki sposób Python obsługuje instrukcję „def”.

Krótkie wprowadzenie. Po pierwsze, w pythonie istnieją dwa typy danych, jeden jest prostym typem danych, takim jak liczby, a drugim typem danych są obiekty. Po drugie, przekazując dane do parametrów, python przekazuje elementarny typ danych według wartości, tj. Tworzy lokalną kopię wartości do zmiennej lokalnej, ale przekazuje obiekt przez odniesienie, tj. Wskaźniki do obiektu.

Przyjmując powyższe dwa punkty, wyjaśnijmy, co się stało z kodem Pythona. Dzieje się tak tylko z powodu przekazywania przez referencje do obiektów, ale nie ma to nic wspólnego ze zmiennymi / niezmiennymi, lub prawdopodobnie z faktu, że instrukcja „def” jest wykonywana tylko raz, gdy jest zdefiniowana.

[] jest obiektem, więc python przekazuje odwołanie do [] a, tzn. ajest tylko wskaźnikiem do [], który leży w pamięci jako obiekt. Jest tylko jedna kopia [] z wieloma odniesieniami do niej. Dla pierwszego foo () lista [] jest zmieniana na 1 za pomocą metody append. Należy jednak pamiętać, że istnieje tylko jedna kopia obiektu listy i ten obiekt staje się teraz 1 . Podczas uruchamiania drugiego foo (), to co mówi strona internetowa effbot (elementy nie są już oceniane) jest błędne. ajest oceniany jako obiekt listy, chociaż teraz jego zawartość wynosi 1 . Jest to efekt przejścia przez odniesienie! Wynik foo (3) można łatwo uzyskać w ten sam sposób.

Aby dodatkowo zweryfikować moją odpowiedź, spójrzmy na dwa dodatkowe kody.

====== nr 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]jest przedmiotem, tak samo jest None(pierwsze jest zmienne, a drugie niezmienne. Ale zmienność nie ma nic wspólnego z pytaniem). Żadnego nie ma gdzieś w kosmosie, ale wiemy, że tam jest i jest tylko jedna kopia Nic. Tak więc za każdym razem, gdy wywoływane jest foo, elementy są oceniane (w przeciwieństwie do niektórych odpowiedzi, które są oceniane tylko raz) jako None, aby być jasnym, odniesienie (lub adres) None. Następnie w foo pozycja jest zmieniana na [], tzn. Wskazuje inny obiekt, który ma inny adres.

====== nr 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Wywołanie foo (1) powoduje, że elementy wskazują na obiekt listy [] z adresem, powiedzmy 11111111. zawartość listy zmienia się na 1 w funkcji foo w kontynuacji, ale adres się nie zmienia, wciąż 11111111 , Potem nadchodzi foo (2, []). Chociaż [] w foo (2, []) ma taką samą treść jak domyślny parametr [] podczas wywoływania foo (1), ich adres jest inny! Ponieważ podajemy parametr jawnie, itemsmusi wziąć adres tego nowego[] , powiedzmy 2222222, i zwrócić go po wprowadzeniu pewnych zmian. Teraz wykonywany jest foo (3). ponieważ tylkoxpod warunkiem, elementy muszą ponownie przyjąć wartość domyślną. Jaka jest wartość domyślna? Jest on ustawiany podczas definiowania funkcji foo: obiekt listy znajdujący się w 11111111. Tak więc pozycje są oceniane jako adres 11111111 posiadający element 1. Lista znajdująca się pod 2222222 zawiera również jeden element 2, ale nie jest wskazywana przez żadne elementy więcej. W związku z tym dodatek 3 spowoduje items[1,3].

Z powyższych wyjaśnień wynika, że strona internetowa effbot zalecana w zaakceptowanej odpowiedzi nie dała odpowiedniej odpowiedzi na to pytanie. Co więcej, myślę, że punkt na stronie internetowej efektora jest zły. Myślę, że kod dotyczący interfejsu użytkownika jest poprawny:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Każdy przycisk może posiadać odrębną funkcję zwrotną, która wyświetla inną wartość i. Mogę podać przykład, aby to pokazać:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Jeśli wykonamy x[7](), otrzymamy 7 zgodnie z oczekiwaniami i x[9]()otrzymamy 9, kolejną wartość i.


5
Twój ostatni punkt jest błędny. Spróbuj, a zobaczysz, że x[7]()tak 9.
Duncan

2
„Python przekazuje elementarny typ danych według wartości, tzn. tworzy lokalną kopię wartości do zmiennej lokalnej” jest całkowicie niepoprawny. Dziwi mnie, że ktoś oczywiście zna bardzo dobrze Pythona, a mimo to ma tak okropne niezrozumienie podstaw. :-(
Veky

6

TLDR: Domyślne ustawienia czasu są spójne i bardziej wyraziste.


Zdefiniowanie funkcji wpływa na dwa zakresy: zakres definiujący zawierający funkcję oraz zakres wykonania zawarty w funkcji. Chociaż jest całkiem jasne, w jaki sposób bloki są mapowane na zakresy, pytanie brzmi, gdzie def <name>(<args=defaults>):należy:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

def nameCzęść należy oceniać w zakresie definiującej - chcemy namebyć dostępne tam, mimo wszystko. Ocena funkcji tylko w jej wnętrzu uczyniłaby ją niedostępną.

Ponieważ parameterjest to stała nazwa, możemy ją „ocenić” w tym samym czasie co def name. Ma to również tę zaletę, że tworzy funkcję o znanym podpisie jako name(parameter=...):zamiast goły name(...):.

Kiedy teraz oceniać default?

Spójność mówi już „przy definicji”: wszystko inne def <name>(<args=defaults>):najlepiej oceniać również przy definicji. Opóźnianie części byłoby zadziwiającym wyborem.

Te dwie opcje również nie są równoważne: Jeśli defaultzostanie oszacowany w czasie definicji, nadal może wpływać na czas wykonywania. Jeśli defaultjest oceniany w czasie wykonywania, nie może wpływać na czas definicji. Wybranie „przy definicji” pozwala wyrazić oba przypadki, a wybranie „przy wykonaniu” może wyrazić tylko jedną:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

„Spójność mówi już„ w definicji ”: wszystko inne def <name>(<args=defaults>):najlepiej oceniać również w definicji”. Nie sądzę, że wniosek wynika z założenia. To, że dwie rzeczy znajdują się w tej samej linii, nie oznacza, że ​​należy je oceniać w tym samym zakresie. defaultto coś innego niż reszta linii: to wyrażenie. Ocena wyrażenia jest procesem zupełnie innym niż definiowanie funkcji.
LarsH

@LarsH Definicje funkcji oceniane w Pythonie. Niezależnie od tego, czy jest to instrukcja ( def), czy wyrażenie ( lambda), nie zmienia to, że utworzenie funkcji oznacza ocenę - zwłaszcza jej sygnatury. Domyślne są częścią podpisu funkcji. Nie oznacza to, że wartości domyślne muszą zostać ocenione natychmiast - na przykład wskazówki typu mogą tego nie robić. Ale z pewnością sugeruje, że powinni, chyba że istnieje uzasadniony powód, aby tego nie robić.
MisterMiyagi,

OK, utworzenie funkcji oznacza ewaluację w pewnym sensie, ale oczywiście nie w tym sensie, że każde wyrażenie w niej jest oceniane w momencie definicji. Większość nie. Nie jest dla mnie jasne, w jakim sensie podpis jest szczególnie „oceniany” w czasie definicji bardziej niż ciało funkcji jest „oceniane” (przetwarzane w odpowiednią reprezentację); podczas gdy wyrażenia w ciele funkcji wyraźnie nie są oceniane w pełnym tego słowa znaczeniu. Z tego punktu widzenia spójność powiedziałaby, że wyrażenia w podpisie również nie powinny być „w pełni” ocenione.
LarsH

Nie mam na myśli, że się mylisz, tylko że twój wniosek nie wynika wyłącznie z konsekwencji.
LarsH

@ LarsH Wartości domyślne nie są częścią ciała, ani nie twierdzę, że spójność jest jedynym kryterium. Czy możesz zasugerować, jak wyjaśnić odpowiedź?
MisterMiyagi,

3

Każda inna odpowiedź wyjaśnia, dlaczego tak naprawdę jest to miłe i pożądane zachowanie lub dlaczego i tak nie powinno być potrzebne. Mój jest dla tych upartych, którzy chcą skorzystać ze swojego prawa, by zginać język według własnej woli, a nie na odwrót.

„Naprawimy” to zachowanie za pomocą dekoratora, który skopiuje wartość domyślną zamiast ponownego użycia tego samego wystąpienia dla każdego argumentu pozycyjnego pozostawionego na wartość domyślną.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Teraz ponownie zdefiniujmy naszą funkcję za pomocą tego dekoratora:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Jest to szczególnie przydatne w przypadku funkcji, które pobierają wiele argumentów. Porównać:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

z

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Ważne jest, aby pamiętać, że powyższe rozwiązanie psuje się, jeśli spróbujesz użyć argumentów słowa kluczowego:

foo(a=[4])

Dekorator można dostosować, aby na to pozwolić, ale pozostawiamy to jako ćwiczenie dla czytelnika;)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.