Pytanie "Najlżejszy zdziwienie" i Mutable Default Argument


Każdy, kto wystarczająco długo majstruje w Pythonie, został ukąszony (lub rozdarty na kawałki) w następującym wydaniu:

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

Nowicjusze pythonowi oczekiwaliby, że ta funkcja zawsze zwróci listę zawierającą tylko jeden element: [5]. Rezultat jest zupełnie inny i bardzo zadziwiający (dla nowicjuszy):

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

Mój menedżer miał swoje pierwsze spotkanie z tą funkcją i nazwał ją "dramatyczną wadą projektu" tego języka. Odpowiedziałem, że zachowanie ma ukryte wytłumaczenie, i rzeczywiście jest bardzo zagadkowe i nieoczekiwane, jeśli nie rozumiesz wewnętrznych elementów. Jednak nie byłem 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ę, by doświadczone zachowanie miało praktyczne zastosowanie (kto naprawdę użył zmiennych statycznych w C, bez błędów hodowlanych?)

Edytować:

Baczek przedstawił interesujący przykład. Wraz z większością twoich komentarzy, a zwłaszcza z Utaalem, omówił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 odnosiła się do miejsca ustawienia zakresu parametrów: wewnątrz funkcji lub "razem" z nią?

Wykonanie wiązania wewnątrz funkcji oznaczałoby, że x jest efektywnie związany z określoną wartością domyślną, gdy funkcja jest wywoływana, nie jest zdefiniowana, co może przedstawiać głęboką wadę: def linia byłaby "hybrydowa" w tym sensie, że część wiązania (obiektu funkcji) miałaby miejsce w definicji, a część (przypisanie domyślnych parametrów) w czasie wywoływania funkcji.

Rzeczywiste zachowanie jest bardziej spójne: wszystko to linii zostanie ocenione, gdy ta linia zostanie wykonana, co oznacza definicję funkcji.


2057
2017-07-15 18:00


pochodzenie


Pytanie uzupełniające - Dobre zastosowania dla zmiennych domyślnych argumentów - Jonathan
Nie wątpię, że zmienne argumenty naruszają zasadę najmniejszego zdziwienia dla przeciętnego człowieka i widziałem, jak początkujący wkraczają na miejsce, a potem heroicznie zastępują listy mailingowe krotkami pocztowymi. Niemniej jednak zmienne argumenty są nadal zgodne z Python Zen (Pep 20) i wpadają w "oczywiste dla Holendra" (rozumiane / wykorzystywane przez programistów Pythona). Zalecane obejście tego dokumentu jest najlepsze, ale opór wobec dokumentów i dokumentów (pisanych) nie jest tak rzadki w dzisiejszych czasach. Osobiście wolałbym dekoratora (powiedzmy @fixed_defaults). - Serge
Mój argument, kiedy natknąłem się na to, brzmi: "Dlaczego potrzebujesz stworzyć funkcję, która zwraca zmienną, która może opcjonalnie być zmienną, którą możesz przekazać do funkcji? Albo zmienia zmienną lub tworzy nową. zrobić jedno za pomocą jednej funkcji i dlaczego interpreter powinien zostać przepisany, abyś mógł to zrobić bez dodawania trzech linii do kodu? " Ponieważ mówimy o przepisywaniu sposobu, w jaki interpreter obsługuje tutaj definicje funkcji i wywołania. To dużo do zrobienia w ledwo potrzebnym przypadku użycia. - Alan Leuthard
"Nowicjusze pythonowi oczekiwaliby, że ta funkcja zawsze zwróci listę z tylko jednym elementem: [5]"Jestem nowicjuszem Python i nie spodziewałbym się tego, ponieważ oczywiście foo([1]) wróci [1, 5], nie [5]. Chodziło o to, że nowicjusz oczekiwałby tej funkcji wywołany bez parametru zawsze wróci [5]. - symplectomorphic
Dla przykład w samouczku w Pythonie, dlaczego jest if L is None: potrzebne? Usunąłem ten test i nie zrobiło to żadnej różnicy - sdaffa23fdsf


Odpowiedzi:


W rzeczywistości nie jest to wada projektowa i nie wynika z wewnętrznych ani wydajności.
Wynika to po prostu z faktu, że funkcje w Pythonie są pierwszorzędnymi obiektami, a nie tylko fragmentem kodu.

Jak tylko zaczniesz myśleć w ten sposób, to całkowicie ma to sens: funkcja to obiekt oceniany na podstawie jego definicji; domyślne parametry są rodzajem "danych członków", a zatem ich stan może się zmieniać z jednego połączenia na drugie - dokładnie tak, jak w każdym innym obiekcie.

W każdym razie Effbot ma bardzo dobre wyjaśnienie powodów tego zachowania Domyślne wartości parametrów w Pythonie.
Stwierdziłem, że jest to bardzo jasne i naprawdę sugeruję, aby go przeczytać, aby lepiej poznać funkcjonowanie obiektów funkcjonalnych.


1353
2017-07-17 21:29



Wszystkim, którzy czytają powyższą odpowiedź, gorąco polecam poświęcić czas na przeczytanie połączonego artykułu Effbot. Oprócz wszystkich innych użytecznych informacji, bardzo przydatna może być część dotycząca tego, w jaki sposób można wykorzystać tę funkcję językową do buforowania / zapamiętywania wyników! - Cam Jackson
Nawet jeśli jest to obiekt pierwszej klasy, wciąż można wyobrazić sobie projekt, w którym kod dla każdej wartości domyślnej jest przechowywany wraz z obiektem i ponownej oceny za każdym razem, gdy funkcja jest wywoływana. Nie twierdzę, że byłoby lepiej, tylko że funkcje będące pierwszorzędnymi obiektami nie wykluczają tego całkowicie. - gerrit
Przykro mi, ale wszystko, co uważane jest za "największy WTF w Pythonie" zdecydowanie wadą projektu. To jest źródło błędów dla każdy w pewnym momencie, ponieważ nikt nie oczekuje tego zachowania na początku - co oznacza, że ​​nie powinien był zostać zaprojektowany w ten sposób na samym początku. Nie obchodzi mnie, jakie obręcze musieli przeskoczyć, oni powinien zaprojektowałem Pythona, aby domyślne argumenty były niestatyczne. - BlueRaja - Danny Pflughoeft
Niezależnie od tego, czy jest to wada projektowa, twoja odpowiedź zdaje się sugerować, że to zachowanie jest w jakiś sposób konieczne, naturalne i oczywiste, biorąc pod uwagę, że funkcje są obiektami najwyższej klasy, a tak po prostu nie jest. Python ma zamknięcia. Jeśli zastąpisz domyślny argument przydziałem w pierwszym wierszu funkcji, oceni on wyrażenie każdego wywołania (potencjalnie używając nazw zadeklarowanych w zasięgu obejmującym). Nie ma żadnego powodu, dla którego nie byłoby możliwe lub uzasadnione, aby domyślne argumenty były oceniane za każdym razem, gdy funkcja jest wywoływana w dokładnie taki sam sposób. - Mark Amery
Projekt nie wynika bezpośrednio z functions are objects. W twoim paradygmacie propozycja polega na zaimplementowaniu wartości domyślnych funkcji jako właściwości, a nie atrybutów. - bukzor


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śleć, że jeśli pierwszy parametr nie zostanie podany, to będzie równa krotce ("apples", "bananas", "loganberries")

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

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

wtedy jeśli domyślne parametry byłyby związane przy realizacji funkcji, a nie deklaracji funkcji, byłbym zaskoczony (w bardzo złym sensie), aby odkryć, że owoce zostały zmienione. Byłoby to bardziej zadziwiające IMO niż odkrycie tego foofunkcja powyżej mutowała listę.

Prawdziwy problem leży w zmiennych zmiennych, a wszystkie języki mają w pewnym stopniu 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 moja mapa używa wartości StringBuffer klucz, kiedy został umieszczony na mapie, lub czy zapisuje klucz przez odniesienie? Tak czy inaczej, ktoś jest zaskoczony; albo osoba, która próbowała wydobyć przedmiot z Map przy użyciu wartości identycznej z wartością, z którą ją wprowadzili, lub z osobą, która nie może odzyskać swojego obiektu, mimo że klucz, którego używają, jest dosłownie tym samym obiektem, który został użyty do umieszczenia go na mapie (jest to właściwie dlaczego Python nie zezwala na używanie zmiennego wbudowanego typu danych jako kluczy słownika).

Twój przykład to dobry przypadek, w którym nowi użytkownicy Pythona będą zaskoczeni i ugryzieni. Ale twierdzę, że gdybyśmy to "naprawili", stworzyliby tylko inną sytuację, w której zamiast tego zostaliby ukąszeni, a ta byłaby jeszcze mniej intuicyjna. Co więcej, ma to zawsze miejsce w przypadku zmiennych zmiennych; zawsze napotykasz przypadki, w których ktoś intuicyjnie może oczekiwać jednego lub przeciwnego zachowania w zależności od tego, jaki kod pisze.

Osobiście lubię aktualne podejście Pythona: argumenty funkcji domyślnej są oceniane, gdy funkcja jest zdefiniowana, a obiekt jest zawsze domyślny. Przypuszczam, że mogliby skorzystać ze specjalnej skrzynki z pustą listą, ale ten rodzaj specjalnej obudowy spowodowałby jeszcze większe zdziwienie, nie wspominając już o tym, że nie można go cofnąć.


231
2017-07-15 18:11



Myślę, że to kwestia debaty. Działasz na zmiennej globalnej. Każda ocena przeprowadzona w dowolnym miejscu twojego kodu, obejmująca zmienną globalną, będzie teraz (prawidłowo) odnosić się do ("jagody", "mangos"). domyślny parametr może być po prostu jak każdy inny przypadek. - Stefano Borini
Właściwie to nie sądzę, że zgadzam się z twoim pierwszym przykładem. Nie jestem pewien, czy podoba mi się pomysł zmodyfikowania inicjalizatora w ten sposób, ale jeśli tak, oczekiwałbym, że zachowa się dokładnie tak, jak opisujesz - zmieniając wartość domyślną na ("blueberries", "mangos"). - Ben Blank
Domyślny parametr jest jak każdy inny przypadek. Nieoczekiwane jest to, że parametr jest zmienną globalną, a nie lokalną. To z kolei dlatego, że kod jest wykonywany przy definicji funkcji, a nie wywoływaniu. Kiedy już to zrozumiesz i to samo dotyczy klas, jest to całkowicie jasne. - Lennart Regebro
Uważam przykład za raczej wprowadzający w błąd niż genialny. Gdyby some_random_function() dołącza do fruits zamiast przypisywania jej, zachowanie eat()  będzie zmiana. Tyle dla obecnego wspaniałego projektu. Jeśli użyjesz domyślnego argumentu przywoływanego gdzie indziej, a następnie zmodyfikujesz referencję spoza tej funkcji, prosisz o kłopoty. Prawdziwy WTF występuje wtedy, gdy ludzie definiują nowy domyślny argument (literał listy lub wywołanie konstruktora), oraz nadal weź kawałek. - alexis
Właśnie jawnie oświadczyłeś global i ponownie przydzielono krotkę - nie ma absolutnie nic zaskakującego, jeśli eat po tym działa inaczej. - user3467349


AFAICS jeszcze nikt nie opublikował odpowiedniej części dokumentacja:

Domyślne wartości parametrów są obliczane podczas wykonywania definicji funkcji. Oznacza to, że wyrażenie jest oceniane jednorazowo, gdy funkcja jest zdefiniowana i że ta sama wartość "wstępnie obliczona" jest używana dla każdego połączenia. Jest to szczególnie ważne, aby zrozumieć, kiedy parametr domyślny jest zmiennym obiektem, takim jak lista lub słownik: jeśli funkcja modyfikuje obiekt (np. Poprzez dodanie elementu do listy), zmieniana jest domyślna wartość. Zasadniczo nie jest to zamierzone. Aby obejść ten problem, należy użyć wartości Brak jako wartości domyślnej i jawnie przetestować ją w treści funkcji [...]


195
2017-07-10 14:50



Wyrażenia "to nie jest ogólnie to, co było zamierzone" i "sposób obejścia tego" to zapach, który dokumentuje wadę projektu. - bukzor
@Matthew: Dobrze wiem, ale to nie jest warte pułapki. Ogólnie rzecz biorąc, przewodniki stylu i linters bezwarunkowo zmieniają zmienne wartości domyślne jako nieprawidłowe z tego powodu. Wyraźnym sposobem na zrobienie tego samego jest wypchanie atrybutu na funkcję (function.data = []) lub jeszcze lepiej, zrobić obiekt. - bukzor
@bukzor: Pułapki muszą być odnotowane i udokumentowane, dlatego to pytanie jest dobre i otrzymało tak dużo awansów. W tym samym czasie pułapki niekoniecznie muszą zostać usunięte. Ilu początkujących Pythona przekazało listę funkcji, która je zmodyfikowała, i byli zszokowani, gdy zobaczyli zmiany widoczne w oryginalnej zmiennej? Jednak zmienne typy obiektów są wspaniałe, kiedy zrozumiesz, jak z nich korzystać. Chyba sprowadza się to do opinii na temat tej konkretnej pułapki. - Matthew
Wyrażenie "to nie jest ogólnie zamierzone" oznacza "nie to, co programista faktycznie chciał zrealizować", a nie "nie to, co Python ma robić". - holdenweb
@oriadam Może wtedy zechcesz napisać o tym pytanie. Może robisz coś innego, niż zamierzałeś ... - glglgl


Nic nie wiem o wewnętrznych działaniach interpretera Pythona (i nie jestem też ekspertem od kompilatorów i interpretatorów), więc nie obwiniaj mnie, jeśli proponuję cokolwiek niedorzecznego lub niemożliwego.

Pod warunkiem, że obiekty Pythona są zmienne Myślę, że powinno to być brane pod uwagę przy projektowaniu domyślnych argumentów. Kiedy tworzysz listę:

a = []

spodziewasz się dostać Nowy lista wymieniona przez za.

Dlaczego a = [] w

def x(a=[]):

utworzyć nową listę definicji funkcji, a nie wywoływać? To tak, jakbyś pytał "jeśli użytkownik nie poda argumentu utworzyć instancję nową listę i używać jej tak, jakby była produkowana przez dzwoniącego ". Myślę, że jest to niejednoznaczne:

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

użytkownik, czy chcesz za domyślnie do datetime odpowiadającego podczas definiowania lub wykonywania x? W tym przypadku, podobnie jak w poprzednim, zachowam to samo zachowanie, jak gdyby domyślny argument "assignment" był pierwszą instrukcją funkcji (datetime.now () wywołaną przy wywołaniu funkcji). Z drugiej strony, jeśli użytkownik chciałby mapowania w czasie definiowania czasu, mógłby napisać:

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

Wiem, wiem: to jest zamknięcie. Alternatywnie Python może dostarczyć słowo kluczowe, które wymusiłoby wiązanie czasu definicji:

def x(static a=b):

97
2017-07-15 23:21



Możesz zrobić: def x (a = Brak): A następnie, jeśli a jest Brak, ustaw a = datetime.datetime.now () - Anon
Wiem, to był tylko przykład do wyjaśnienia, dlaczego wolałbym wiązać czas wykonania. - Utaal
Dziękuję Ci za to. Naprawdę nie mogłem powiedzieć, dlaczego to mnie niepokoi. Zrobiłeś to pięknie z minimalnym rozmyciem i zamieszaniem. Jako ktoś pochodzący z programowania systemowego w C ++ i czasami naiwnie "tłumaczący" cechy językowe, ten fałszywy przyjaciel kopnął mnie w miękki łeb wielki czas, tak jak atrybuty klasowe. Rozumiem, dlaczego tak się dzieje, ale nie mogę powstrzymać się od niechęci, bez względu na to, co z tego wyniknie. Przynajmniej jest to tak sprzeczne z moim doświadczeniem, że prawdopodobnie (miejmy nadzieję) nigdy o tym nie zapomnę ... - AndreasT
@Andreas, gdy już użyjesz Pythona wystarczająco długo, zaczniesz widzieć, jak logiczne jest, że Python interpretuje rzeczy jako atrybuty klasy tak jak to robi - tylko ze względu na szczególne dziwactwa i ograniczenia języków takich jak C ++ (i Java oraz C # ...), że ma sens dla zawartości class {} blok należy interpretować jako należący do instancje :) Ale gdy klasy są obiektami pierwszej klasy, oczywiście naturalną rzeczą jest, aby ich zawartość (w pamięci) odzwierciedlała ich zawartość (w kodzie). - Karl Knechtel
Normatywna struktura nie jest dziwactwem ani ograniczeniem w mojej książce. Wiem, że to może być niezdarne i brzydkie, ale możesz nazwać to "definicją" czegoś. Języki dynamiczne wydają mi się trochę anarchistami: oczywiście każdy jest wolny, ale potrzebujesz struktury, aby kogoś opróżnić i utorować drogę. Chyba jestem stary ... :) - AndreasT


Powodem jest po prostu, że powiązania są wykonywane po wykonaniu kodu, a definicja funkcji jest wykonywana, dobrze ... kiedy funkcje są zdefiniowane.

Porównaj to:

class BananaBunch:
    bananas = []

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

Ten kod cierpi na dokładnie taki sam nieoczekiwany przypadek. banany to atrybut klasy, a więc po dodaniu do niego elementów jest on dodawany do wszystkich wystąpień tej klasy. Powód jest dokładnie taki sam.

To po prostu "Jak to działa", a sprawienie, żeby działało inaczej w przypadku funkcji, byłoby prawdopodobnie skomplikowane, a w przypadku zajęć prawdopodobnie niemożliwe, lub przynajmniej spowolnienie instancji obiektu, ponieważ musiałbyś zachować kod klasy i wykonać go, gdy obiekty są tworzone.

Tak, to nieoczekiwane. Ale gdy spadnie pensa, pasuje idealnie do sposobu działania Pythona. W rzeczywistości jest to dobra pomoc dydaktyczna, a kiedy już zrozumiesz, dlaczego tak się dzieje, znacznie lepiej porozmawiasz z Pythonem.

To powiedziawszy, powinno to być widoczne w każdym dobrym samouczku w Pythonie. Bo jak już wspomnieliśmy, prędzej czy później wszyscy wpadną w ten problem.


72
2017-07-15 18:54



Jeśli jest inny dla każdej instancji, nie jest atrybutem klasy. Atrybuty klasy są atrybutami w KLASIE. Stąd nazwa. Dlatego są one takie same dla wszystkich przypadków. - Lennart Regebro
Nie pytał o opis zachowania Pythona, prosił o racjonalne uzasadnienie. Nic w Pythonie nie jest po prostu "Jak to działa"; to wszystko robi to, co robi z jakiegoś powodu. - Glenn Maynard
I podałem uzasadnienie. - Lennart Regebro
Nie powiedziałbym, że to "to dobra pomoc dydaktyczna", ponieważ tak nie jest. - Geo
@Geo: Tyle tylko, że tak. Pomaga zrozumieć wiele rzeczy w Pythonie. - Lennart Regebro


Kiedyś myślałem, że tworzenie obiektów w czasie wykonywania byłoby lepszym podejściem. Teraz jestem mniej pewny, ponieważ tracisz kilka użytecznych funkcji, choć może warto, bez względu na to, by uniknąć zamieszania u nowicjusza. Wady takiego postępowania to:

1. Wydajność

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

Jeśli używana jest ocena czasu oczekiwania, wówczas kosztowna funkcja jest wywoływana za każdym razem, gdy używana jest funkcja bez argumentów. Możesz zapłacić wysoką cenę za każde połączenie lub ręcznie zapisać wartość zewnętrzną, zanieczyszczając przestrzeń nazw i dodając gadatliwość.

2. Wymuszanie parametrów związanych

Przydatną sztuczką jest powiązanie parametrów lambda z obecny powiązanie zmiennej, gdy tworzona jest 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 zostaną powiązane i do czas połączenia wartość i, więc otrzymasz listę funkcji, które wszystkie powróciły 9.

Jedynym sposobem implementacji tego byłoby utworzenie dalszego zamknięcia za pomocą i bound, czyli:

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

3. Introspekcja

Zastanów się nad kodem:

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

Możemy uzyskać informacje o argumentach i wartościach domyślnych za pomocą inspect moduł, który

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

Ta informacja jest bardzo przydatna w przypadku takich rzeczy jak generowanie dokumentów, metaprogramowanie, dekoratory itp.

Załóżmy teraz, że zachowanie wartości domyślnych może zostać zmienione, aby było to odpowiednikiem:

_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 możliwość introspekcji i zobaczmy, jakie są domyślne argumenty . Ponieważ obiekty nie zostały skonstruowane, nigdy nie możemy ich zdobyć bez wywoływania funkcji. Najlepsze, co możemy zrobić, to przechowywać kod źródłowy i zwracać go jako ciąg znaków.


50
2017-07-16 10:05



można uzyskać introspekcję również wtedy, gdy dla każdej istnieje funkcja tworzenia domyślnego argumentu zamiast wartości. moduł inspekcyjny po prostu wywoła tę funkcję. - yairchu
@SilentGhost: Mówię o tym, czy zachowanie zostało zmienione w celu odtworzenia go - tworzenie go raz jest bieżącym zachowaniem i dlaczego istnieje zmienny domyślny problem. - Brian
@yairchu: Zakłada, że ​​konstrukcja jest bezpieczna (tzn. nie ma skutków ubocznych). Introspekcja argumentów nie powinna zrobić cokolwiek, ale ocena arbitralnego kodu może w końcu przynieść efekt. - Brian
Inny język często oznacza po prostu inaczej. Twój pierwszy przykład może być łatwo zapisany jako: _expensive = expensive (); def foo (arg = _expensive), jeśli specjalnie nie rób tego chcę, żeby została ponownie oceniona. - Glenn Maynard
@Glenn - o to mi chodziło z "pamięcią podręczną zmiennej zewnętrznej" - jest nieco bardziej gadatliwa i kończy się dodatkowymi zmiennymi w twojej przestrzeni nazw. - Brian


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, a nie kilka razy.

  2. Konsystencja: Python zawsze przekazuje przedmioty, a nie nazwiska. Domyślnym parametrem jest oczywiście część funkcji nagłówek (nie ciało funkcji). Dlatego należy go oceniać w czasie ładowania modułu (i tylko w czasie ładowania modułu, chyba że zagnieżdżone), nie w czasie wywoływania funkcji.

  3. Przydatność: Jak wyjaśnia Frederik Lundh w swoim wyjaśnieniu z "Domyślne wartości parametrów w języku Python", obecne zachowanie może być bardzo przydatne w zaawansowanym programowaniu. (Użyj oszczędnie.)

  4. Wystarczająca dokumentacja: W najbardziej podstawowej dokumentacji Pythona, samouczek, problem głośno jest ogłaszany jako na "Ważne ostrzeżenie" w pierwszy podsekcja działu "Więcej o definiowaniu funkcji". Ostrzeżenie nawet używa pogrubionej czcionki, który jest rzadko stosowany poza pozycjami. RTFM: przeczytaj dokładną instrukcję.

  5. Meta-learning: Wpadnięcie w pułapkę jest w rzeczywistości bardzo pomocna chwila (przynajmniej jeśli jesteś uczniem refleksyjnym), ponieważ później lepiej zrozumiesz, o co chodzi "Spójność" powyżej i to będzie nauczy Cię wiele o Pythonie.


47
2018-03-30 11:18



Zajęło mi rok odkrycie, że to zachowanie psuje mój kod na produkcji, skończyło się usunięciem kompletnej funkcji, dopóki nie trafiłem na tę wadę projektu przez przypadek. Używam Django. Ponieważ środowisko testowe nie miało wielu żądań, ten błąd nigdy nie miał żadnego wpływu na kontrolę jakości. Kiedy połączyliśmy się i otrzymaliśmy wiele jednoczesnych żądań - niektóre funkcje narzędziowe zaczęły nadpisywać nawzajem swoje parametry! Wykonywanie luk w zabezpieczeniach, błędów i innych problemów. - oriadam
@oriadam, bez urazy, ale zastanawiam się, jak nauczyłeś się Python bez uprzedniego brania udziału w tym. Właśnie uczę się Pythona i ta możliwa pułapka jest wymienione w oficjalnym samouczku Python tuż obok pierwszej wzmianki o domyślnych argumentach. (Jak wspomniano w punkcie 4 tej odpowiedzi.) Przypuszczam, że morał jest raczej niesympatyczny - do czytania oficjalni doktorzy języka używanego do tworzenia oprogramowania produkcyjnego. - Wildcard
Byłoby również dla mnie zaskakujące, gdyby funkcja wywoływania nieznanej złożoności została wywołana dodatkowo do wywoływanego przeze mnie wywołania funkcji. - Vatine


Dlaczego nie introspekcja?

Jestem naprawdę Zaskoczony, nikt nie przeprowadził wnikliwej introspekcji oferowanej przez Python (2 i 3 zastosuj) na kalamburach.

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

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

Kiedy Python go napotka, pierwszą rzeczą, którą zrobi, będzie skompilowanie go w celu utworzenia code obiekt dla tej funkcji. Podczas gdy ten etap kompilacji jest zakończony, Pyton ocenia* i wtedy sklepy domyślne argumenty (pusta lista [] tutaj) w obiekcie funkcji. Jak wspomniano w pierwszej odpowiedzi: lista a można teraz uznać za członek funkcji func.

Zróbmy więc introspekcję, przed i po, aby zbadać, jak lista zostaje rozwinięta wewnątrz obiekt funkcji. używam Python 3.x w tym przypadku dla Pythona 2 obowiązuje to samo (use __defaults__ lub func_defaults w Pythonie 2; tak, dwie nazwy dla tej samej rzeczy).

Funkcja przed wykonaniem:

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

Po tym, jak Python wykona tę definicję, przyjmie wszystkie określone parametry domyślne (a = [] tutaj i wkurzyć ich w __defaults__ atrybut dla obiektu funkcji (odpowiednia sekcja: Callables):

>>> func.__defaults__
([],)

O.k, więc pusta lista jako pojedynczy wpis w __defaults__, zgodnie z oczekiwaniami.

Funkcja po wykonaniu:

Wykonajmy teraz tę funkcję:

>>> func()

Teraz, zobaczmy to __defaults__ jeszcze raz:

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

Zaskoczony? Wartość wewnątrz obiektu zmienia się! Kolejne wywołania funkcji będą po prostu dołączane do tej wbudowanej list obiekt:

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

Tak, masz to, powód, dla którego to 'wada' dzieje się tak, ponieważ domyślne argumenty są częścią obiektu funkcji. Nie ma tu nic dziwnego, to wszystko jest trochę zaskakujące.

Powszechnym rozwiązaniem tego problemu jest zwykle None jako domyślny, a następnie zainicjować 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ą, świeżą pustą listę, jeśli nie przekazano żadnego argumentu a.


Aby dodatkowo zweryfikować listę w __defaults__ jest taki sam jak ten używany w funkcji func możesz po prostu zmienić swoją funkcję, aby zwrócić id listy a używane wewnątrz ciała funkcji. Następnie porównaj go z listą w __defaults__ (pozycja [0] w __defaults__), a zobaczysz, jak faktycznie odnoszą się one do tej samej instancji listy:

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

Wszystko dzięki sile 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ływana przed procesem budowania funkcji i powiązania jej z nazwą bar jest zrobione.


43
2017-12-09 07:13



Jest id(...) potrzebne do tej ostatniej weryfikacji, lub is operator odpowiada na to samo pytanie? - das-g
@ das-g is zrobiłbym dobrze, po prostu użyłem id(val) ponieważ uważam, że może być bardziej intuicyjny. - Jim Fasarakis Hilliard
@JimFasarakisHilliard Chciałbym dodać monit do input. lubić input('Did you just see me without calling me?'). Dzięki temu jest jaśniejszy. - Ev. Kounis
@ Ev.Kounis Podoba mi się! Dziękuję za wskazanie tego. - Jim Fasarakis Hilliard


To zachowanie jest łatwe do wyjaśnienia przez:

  1. deklaracja funkcji (klasy itp.) jest wykonywana tylko raz, tworząc wszystkie domyślne obiekty wartości
  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 jest wykonywana na tym samym obiekcie - i jest drukowana

40
2017-07-15 19:15



Twoje # 4 może być mylące dla ludzi, ponieważ liczby całkowite są niezmienne, a więc "jeśli" nie jest prawdą. Na przykład, przy d ustawionym na 0, d .__ dodaj __ (1) zwróci 1, ale d nadal będzie 0. - Anon
(Tak właściwie, Dodaj jest złym przykładem, ale moje główne punkty to wciąż niezmienne liczby całkowite). - Anon
tak, to nie był dobry przykład - ymv
Zrobiłem to, ku rozczarowaniu po sprawdzeniu, aby to zobaczyć, przy b ustawionym na [], b .__ dodaj __ ([1]) zwraca [1], ale pozostawia b także [], mimo że listy są zmienne. Mój błąd. - Anon
@ANon: jest __iadd__, ale nie działa z int. Oczywiście. :-) - Veky


To, o co pytasz, to dlaczego:

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

nie jest wewnętrznie równoważne 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ływania func (None, None), które zignorujemy.

Innymi słowy, zamiast oceny parametrów domyślnych, dlaczego nie przechowywać każdego z nich i ocenić je, gdy funkcja jest wywoływana?

Prawdopodobnie istnieje jedna odpowiedź - skutecznie zamieniłaby 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łaby wolniejsza i zużywała więcej pamięci.


30
2017-07-15 20:18



Nie musi to być zamknięcie - lepszym sposobem myślenia byłoby po prostu uczynienie kodu bajtowego tworzącego defaults pierwszą linią kodu - mimo wszystko kompilowanie ciała w tym momencie i tak - nie ma prawdziwej różnicy między kodem w argumentach i kodzie w ciele. - Brian
To prawda, ale spowolniłoby to działanie Pythona i byłoby to dość zaskakujące, chyba że zrobisz to samo dla definicji klas, co sprawi, że będzie głupio powolny, ponieważ będziesz musiał ponownie uruchomić całą definicję klasy za każdym razem, gdy utworzysz instancję klasa. Jak wspomniano, poprawka byłaby bardziej zaskakująca niż problem. - Lennart Regebro
Uzgodniono z Lennartem. Jak mówi Guido, dla każdej funkcji językowej lub standardowej biblioteki istnieje ktoś tam, używając go. - Jason Baker
Zmiana teraz byłaby szaleństwem - dopiero odkrywamy, dlaczego tak jest. Gdyby na początku dokonano spóźnionej oceny domyślnej, niekoniecznie byłoby to zaskakujące. To na pewno prawda, że ​​taki rdzeń, różnica w parsowaniu, miałaby zamiatanie i prawdopodobnie wiele niejasnych efektów na język jako całość. - Glenn Maynard