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 x
jest skutecznie powiązane z określonym domyślnym, gdy funkcja jest wywoływana, nieokreślona, co może mieć głęboką wadę: def
linia 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.
źródło
[5]
” Jestem początkującym Python i nie spodziewałbym się tego, bo oczywiściefoo([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]
.Odpowiedzi:
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.
źródło
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.Załóżmy, że masz następujący kod
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
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
foo
powyż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:
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 przyMap
uż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.
źródło
("blueberries", "mangos")
.some_random_function()
dołączy sięfruits
zamiast przypisywać do niego, zachowanieeat()
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.global
i przypisałeś krotkę - nie ma absolutnie nic dziwnego, jeślieat
potem działa inaczej.Odpowiednia część dokumentacji :
źródło
function.data = []
) lub jeszcze lepiej, utworzenie obiektu.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ę:
spodziewasz się uzyskać nową listę, do której odwołuje się
a
.Czemu to mają
a=[]
wutworzyć 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:
użytkownik, czy chcesz
a
domyślnie ustawić datę i godzinę odpowiadającą podczas definiowania lub wykonywaniax
? 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ć:Wiem, wiem: to jest zamknięcie. Alternatywnie Python może podać słowo kluczowe, aby wymusić powiązanie w czasie definicji:
źródło
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).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:
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.
źródło
property
s - 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 = value
jak powiedział Lennart).Dlaczego nie introspekujesz?
Jestem naprawdę zaskoczony, że nikt nie przeprowadził wnikliwej introspekcji oferowanej przez Python (
2
i3
zastosuj) w sprawie na żądanie.Biorąc pod uwagę prostą małą funkcję
func
zdefiniowaną jako:Gdy Python go napotka, pierwszą rzeczą, którą zrobi, jest skompilowanie go w celu utworzenia
code
obiektu 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ęa
można teraz uznać za członka funkcjifunc
.Więc zróbmy introspekcję, przed i po, aby sprawdzić, jak lista jest rozszerzana wewnątrz obiektu funkcji. Używam
Python 3.x
do tego, w przypadku Python 2 to samo dotyczy (użyj__defaults__
lubfunc_defaults
w Python 2; tak, dwie nazwy dla tej samej rzeczy).Funkcja przed wykonaniem:
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):Ok, więc pusta lista jako pojedynczy wpis
__defaults__
, zgodnie z oczekiwaniami.Funkcja po wykonaniu:
Wykonajmy teraz tę funkcję:
Zobaczmy je
__defaults__
jeszcze raz:Zdumiony? Wartość wewnątrz obiektu zmienia się! Kolejne wywołania funkcji będą teraz po prostu dołączane do tego osadzonego
list
obiektu: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
None
jako domyślnego, a następnie zainicjowanie w treści funkcji: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 funkcjifunc
, możesz po prostu zmienić swoją funkcję i zwrócićid
listęa
uż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:Wszystko z siłą introspekcji!
* Aby sprawdzić, czy Python ocenia domyślne argumenty podczas kompilacji funkcji, spróbuj wykonać następujące czynności:
jak zauważysz,
input()
jest wywoływany przed procesem budowania funkcji i wiązania jej z nazwąbar
.źródło
id(...)
potrzebna jest ostatnia weryfikacja, czy teżis
operator odpowie na to samo pytanie?is
miałoby się dobrze, po prostu użyłem,id(val)
ponieważ myślę, że może być bardziej intuicyjny.None
jako 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.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ść
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:
Zwraca listę funkcji, które zwracają odpowiednio 0,1,2,3 ... Jeśli zachowanie zostanie zmienione, zamiast tego powiążą
i
się z wartością czasu wywołania i, aby uzyskać listę funkcji, które wszystkie zwróciły9
.Jedynym sposobem na wdrożenie tego w innym przypadku byłoby utworzenie dalszego zamknięcia z i-związanym, tj .:
3. Introspekcja
Rozważ kod:
Możemy uzyskać informacje na temat argumentów i wartości domyślnych za pomocą
inspect
modułu, któryTa 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:
Jednak straciliśmy zdolność do introspekcji i zobacz, jakie argumenty domyślne są . 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.
źródło
5 punktów w obronie Pythona
Prostota : zachowanie jest proste w następującym znaczeniu: Większość ludzi wpada w tę pułapkę tylko raz, nie kilka razy.
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.
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.)
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.
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.
źródło
To zachowanie można łatwo wytłumaczyć:
Więc:
a
nie zmienia się - każde wywołanie przypisania tworzy nowy obiekt int - drukowany jest nowy obiektb
nie zmienia się - nowa tablica jest budowana z wartości domyślnej i drukowanac
zmiany - operacja wykonywana jest na tym samym obiekcie - i jest drukowanaźródło
__iadd__
, ale nie działa z int. Oczywiście. :-)Pytasz, dlaczego:
nie jest wewnętrznie równoważny z tym:
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.
źródło
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:
Rozwiązanie : a kopia
Absolutnie bezpieczny rozwiązaniem jest
copy
albodeepcopy
pierwsza, a następnie obiekt wejściowy zrobić cokolwiek z kopią.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[:]
Lublist(some_list)
. Każdy obiekt może być również kopiowanycopy.copy(any_object)
lub dokładniej przezcopy.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. biurowyPrzykład problemu dla podobnego pytania SO
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 .
źródło
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 zrobiszif a is None: a = [1, 2, 3]
, i tak skopiujesz listę.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.
Brak widocznych wartości domyślnych w tym kodzie, ale występuje dokładnie ten sam problem.
Problemem jest to, że
foo
jest 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ł
foo
z domyślnym argumentem nie powinien modyfikować,a
czy 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ę.
źródło
append
się zmianya
„na miejscu”). To, że domyślna zmienna nie jest ponownie tworzona przy każdym wywołaniu, jest „nieoczekiwanym” bitem… przynajmniej dla mnie. :)cache={}
. Podejrzewam jednak, że to „najmniejsze zdziwienie” pojawia się, gdy nie oczekujesz (lub nie chcesz) funkcji, którą wywołujesz, aby mutować argument.cache={}
.None
i przypisanie rzeczywistej wartości domyślnej, jeśli argNone
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.Już zajęty temat, ale z tego, co tutaj przeczytałem, następujące pomogły mi zrozumieć, jak działa wewnętrznie:
źródło
a = a + [1]
przeciążeniaa
... rozważ zmianę nab = a + [1] ; print id(b)
i dodanie liniia.append(2)
. To sprawi, że bardziej oczywiste będzie, że+
na dwóch listach zawsze tworzona jest nowa lista (przypisana dob
), podczas gdy zmodyfikowanaa
może mieć tę samąid(a)
.To optymalizacja wydajności. Które z tych dwóch wywołań funkcji, zdaniem tej funkcji, są szybsze?
Dam ci podpowiedź. Oto demontaż (patrz http://docs.python.org/library/dis.html ):
#
1#
2)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.
źródło
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:
jest prawie dokładnie równoważny z tym:
Demonstracja
Oto demonstracja - możesz za każdym razem sprawdzić, czy są one tym samym obiektem
example.py
i działając z
python example.py
: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:
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 :
źródło
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:
Mamy nadzieję, że wystarczy pokazać, że niewykonanie domyślnych wyrażeń argumentów w czasie wykonywania
def
instrukcji 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.
źródło
Proste obejście przy użyciu Brak
źródło
To zachowanie nie jest zaskakujące, jeśli weźmiesz pod uwagę następujące kwestie:
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:
Wróć do oryginalnego przykładu i rozważ powyższe punkty:
Oto
foo
obiekt ia
atrybutfoo
(dostępny wfoo.func_defs[0]
). Ponieważa
jest listą,a
można ją modyfikować i dlatego jest atrybutem odczytu i zapisufoo
. 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
foo
bez przesłaniania wartości domyślnej wykorzystuje wartość domyślną zfoo.func_defs
. W tym przypadkufoo.func_defs[0]
jest używanya
w zakresie kodu obiektu funkcji. Zmiany doa
zmianyfoo.func_defs[0]
, która jest częściąfoo
obiektu i utrzymuje się między wykonaniem kodu wfoo
.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:
Biorąc pod uwagę (1) i (2) , można zobaczyć, dlaczego osiąga to pożądane zachowanie:
foo
instancja obiektu funkcjifoo.func_defs[0]
jest ustawionaNone
, ustawiana jest na obiekt niezmienny.L
w wywołaniu funkcji),foo.func_defs[0]
(None
) jest dostępne w zasięgu lokalnym jakoL
.L = []
tym przypisanie nie może się powieśćfoo.func_defs[0]
, ponieważ ten atrybut jest tylko do odczytu.L
jest tworzony w zakresie lokalnym i wykorzystywane do pozostałej części wywołania funkcji.foo.func_defs[0]
dlatego pozostaje niezmieniony dla przyszłych inwokacjifoo
.źródło
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 ...) :
Możesz sprawdzić, czy są one jednym i tym samym obiektem, używając
id
:Per Bretta Slatkina „Skuteczny Python: 59 konkretnych sposobów na lepsze pisanie w Pythonie”, pozycja 20: Użycie
None
i ciągi znaków do określenia dynamicznych domyślnych argumentów (s. 48)Ta implementacja zapewnia, że każde wywołanie funkcji otrzyma listę domyślną lub listę przekazaną do funkcji.
Preferowana metoda :
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.
źródło
Oto rozwiązania:
None
jako wartości domyślnej (lub wartości jednorazowejobject
) i włącz ją, aby utworzyć wartości w czasie wykonywania; lublambda
jako 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
)źródło
Kiedy to robimy:
... przypisujemy argument
a
do 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
?W dowolnym momencie, jeśli dzwoniący nie powie nam, co to
a
jest, ponownie go wykorzystamypavlo
.Jeśli
pavlo
jest modyfikowalny (modyfikowalny) ifoo
ostatecznie go modyfikuje, efekt, który zauważamy następnym razem,foo
jest wywoływany bez określaniaa
.Oto, co widzisz (pamiętaj,
pavlo
jest zainicjowany na []):Teraz
pavlo
jest [5].foo()
Ponowne wywołanie zmienia siępavlo
ponownie:Określanie,
a
kiedy dzwonieniefoo()
zapewnia, żepavlo
nie zostanie zmienione.Tak
pavlo
jest nadal[5, 5]
.źródło
Czasami wykorzystuję to zachowanie jako alternatywę dla następującego wzorca:
Jeśli
singleton
jest używany tylko przezuse_singleton
, podoba mi się następujący wzór: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.
źródło
_make_singleton
w 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.Możesz obejść ten problem, zastępując obiekt (a zatem remis z lunetą):
Brzydkie, ale działa.
źródło
Może być prawdą, że:
trzymanie się obu powyższych funkcji jest całkowicie spójne, a mimo to jeszcze jedno:
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 .
źródło
x=[]
oznacza „utwórz pusty obiekt listy i powiąż z nim nazwę„ x ”.” Tak więc wdef 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?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:
cache={}
Nie będzie się wymagać, aby wywoływać funkcję z faktycznym argumentem.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.
źródło
cache={}
wzorzec jest naprawdę rozwiązaniem tylko do rozmowy kwalifikacyjnej, w prawdziwym kodzie prawdopodobnie chcesz@lru_cache
!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.
drukuje następujące
źródło
Po prostu zmień funkcję na:
źródło
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.a
jest 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.a
jest 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 ========
[]
jest przedmiotem, tak samo jestNone
(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 =======
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,
items
musi wziąć adres tego nowego[]
, powiedzmy 2222222, i zwrócić go po wprowadzeniu pewnych zmian. Teraz wykonywany jest foo (3). ponieważ tylkox
pod 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 spowodujeitems
[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:
Każdy przycisk może posiadać odrębną funkcję zwrotną, która wyświetla inną wartość
i
. Mogę podać przykład, aby to pokazać:Jeśli wykonamy
x[7]()
, otrzymamy 7 zgodnie z oczekiwaniami ix[9]()
otrzymamy 9, kolejną wartośći
.źródło
x[7]()
tak9
.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:def name
Część należy oceniać w zakresie definiującej - chcemyname
być dostępne tam, mimo wszystko. Ocena funkcji tylko w jej wnętrzu uczyniłaby ją niedostępną.Ponieważ
parameter
jest to stała nazwa, możemy ją „ocenić” w tym samym czasie codef name
. Ma to również tę zaletę, że tworzy funkcję o znanym podpisie jakoname(parameter=...):
zamiast gołyname(...):
.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
default
zostanie oszacowany w czasie definicji, nadal może wpływać na czas wykonywania. Jeślidefault
jest 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ą:źródło
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.default
to coś innego niż reszta linii: to wyrażenie. Ocena wyrażenia jest procesem zupełnie innym niż definiowanie funkcji.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ć.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ą.
Teraz ponownie zdefiniujmy naszą funkcję za pomocą tego dekoratora:
Jest to szczególnie przydatne w przypadku funkcji, które pobierają wiele argumentów. Porównać:
z
Ważne jest, aby pamiętać, że powyższe rozwiązanie psuje się, jeśli spróbujesz użyć argumentów słowa kluczowego:
Dekorator można dostosować, aby na to pozwolić, ale pozostawiamy to jako ćwiczenie dla czytelnika;)
źródło