„Najmniejsze zdumienie” i argument zmienny domyślny
Każdy, kto wystarczająco długo majstrował przy Pythonie, został ugryziony (lub rozerwany 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 zdumiewający (dla nowicjusza):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Mój menadżer miał kiedyś swoje pierwsze spotkanie z tą funkcją i nazwał ją „dramatyczną wadą projektową” języka. Odpowiedziałem, że to zachowanie ma ukryte wyjaśnienie i jest to rzeczywiście bardzo zagadkowe i nieoczekiwane, jeśli nie rozumiesz wewnętrznych elementów. 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 jej wykonaniu? Wątpię, aby doświadczone zachowanie miało praktyczne zastosowanie (kto naprawdę używał zmiennych statycznych w C, bez rozmnażania błędów?)
Edycja :
Baczek podał ciekawy przykład. Wraz z większością twoich komentarzy, a zwłaszcza Utaala, opracował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 dotyczyła tego, gdzie umieścić zakres parametrów: wewnątrz funkcji czy „razem” z nią?
Wykonywanie wiązania wewnątrz funkcji oznaczałoby, że x
jest skutecznie powiązany z określonym domyślnym, gdy funkcja jest wywoływana, a nie zdefiniowana, coś, co przedstawiałoby głęboki błąd: def
linia byłaby „hybrydowa” w tym sensie, że część wiązania ( obiekt funkcji) miałoby miejsce w momencie definicji, a część (przypisanie parametrów domyślnych) w czasie wywołania funkcji.
Rzeczywiste zachowanie jest bardziej spójne: wszystko w tej linii jest oceniane, gdy ta linia jest wykonywana, czyli w definicji funkcji.
Odpowiedzi
W rzeczywistości nie jest to wada konstrukcyjna i nie wynika to z elementów wewnętrznych ani wydajności.
Wynika to po prostu z faktu, że funkcje w Pythonie są obiektami pierwszej klasy, a nie tylko fragmentem kodu.
Gdy tylko zaczniesz myśleć w ten sposób, ma to całkowicie sens: funkcja to obiekt oceniany na podstawie jego definicji; parametry domyślne są czymś w rodzaju „danych składowych” i dlatego ich stan może zmieniać się z jednego wywołania do drugiego - dokładnie tak, jak w każdym innym obiekcie.
W każdym razie Effbot ma bardzo ładne wyjaśnienie przyczyn tego zachowania w Domyślnych wartościach parametrów w Pythonie .
Wydało mi się to bardzo jasne i naprawdę sugeruję przeczytanie go, aby uzyskać lepszą wiedzę na temat działania obiektów funkcji.
Załóżmy, że masz następujący kod
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
Kiedy widzę deklarację jedzenia, najmniej zadziwiające jest myślenie, że jeśli nie podamy pierwszego parametru, to będzie on równy krotce ("apples", "bananas", "loganberries")
Jednak przypuszczalnie później w kodzie robię coś takiego
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
wtedy gdyby parametry domyślne były związane z wykonywaniem funkcji, a nie deklaracją funkcji, byłbym zdziwiony (w bardzo zły sposób), gdybym odkrył, że owoce zostały zmienione. Byłoby to bardziej zdumiewające dla IMO niż odkrycie, że twoja foo
funkcja powyżej modyfikuje listę.
Prawdziwy problem tkwi 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?
Czy moja mapa używa wartości StringBuffer
klucza, gdy została umieszczona na mapie, czy też przechowuje klucz przez odniesienie? Tak czy inaczej, ktoś jest zdumiony; albo osoba, która próbowała wyciągnąć obiekt z Map
użytkowania, używając wartości identycznej z tą, do której go włożyli, albo osoba, która nie może odzyskać obiektu, mimo że klucz, którego używają, jest dosłownie tym samym obiektem który został użyty do umieszczenia go na mapie (właśnie dlatego Python nie pozwala na używanie swoich modyfikowalnych wbudowanych typów danych jako kluczy słownika).
Twój przykład jest dobrym przykładem przypadku, w którym nowicjusze w Pythonie będą zaskoczeni i ugryzieni. Ale argumentowałbym, że gdybyśmy to „naprawili”, stworzyłoby to tylko inną sytuację, w której zamiast tego zostaliby ugryzieni, a ta byłaby jeszcze mniej intuicyjna. Co więcej, dzieje się tak zawsze w przypadku zmiennych mutowalnych; zawsze napotykasz przypadki, w których ktoś może intuicyjnie oczekiwać jednego lub odwrotnego 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, a ten obiekt jest zawsze domyślny. Przypuszczam, że mogliby w specjalnych przypadkach użyć pustej listy, ale ten rodzaj specjalnej wielkości wywoływałby jeszcze większe zdziwienie, nie wspominając o tym, że byłby niezgodny wstecz.
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, kiedy funkcja jest zdefiniowana, i że ta sama „wstępnie obliczona” wartość jest używana dla każdego wywołania. Jest to szczególnie ważne, aby zrozumieć, kiedy domyślnym parametrem jest zmienny obiekt, taki jak lista lub słownik: jeśli funkcja modyfikuje obiekt (np. Poprzez dołączanie pozycji do listy), w efekcie modyfikowana jest wartość domyślna. Na ogół nie jest to zamierzone. Sposobem na obejście tego jest użycie
None
jako domyślnego i jawne przetestowanie tego 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
Nie wiem nic o wewnętrznym działaniu interpretera Pythona (i nie jestem też ekspertem w kompilatorach i interpretatorach), więc nie obwiniaj mnie, jeśli proponuję coś nieuzasadnionego lub niemożliwego.
Zakładając, że obiekty w Pythonie są zmienne , myślę, że należy to wziąć pod uwagę podczas projektowania domyślnych argumentów. Podczas tworzenia wystąpienia listy:
a = []
spodziewasz się, że otrzymasz nową listę, do której odwołuje się a
.
Dlaczego plik a=[]
in
def x(a=[]):
utworzyć wystąpienie nowej listy w definicji funkcji, a nie przy wywołaniu? To tak, jakbyś pytał „jeśli użytkownik nie poda argumentu, utwórz nową listę i użyj jej tak, jakby została utworzona przez dzwoniącego”. Myślę, że jest to niejednoznaczne:
def x(a=datetime.datetime.now()):
użytkownik, czy chcesz a
domyślnie ustawić datę i godzinę odpowiadającą momentowi 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łaną przy wywołaniu funkcji). Z drugiej strony, jeśli użytkownik chciałby odwzorować czasowo definicję, mógłby napisać:
b = datetime.datetime.now()
def x(a=b):
Wiem, wiem: to zamknięcie. Alternatywnie Python może dostarczyć słowo kluczowe, aby wymusić powiązanie w czasie definicji:
def x(static a=b):
Cóż, powód jest po prostu taki, że wiązania są wykonywane, gdy kod jest wykonywany, a definicja funkcji jest wykonywana, no 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 jest atrybutem klasy, a zatem kiedy dodajesz do niego elementy, jest on dodawany do wszystkich instancji tej klasy. Powód jest dokładnie taki sam.
To po prostu „Jak to działa” i sprawienie, by działało inaczej w przypadku funkcji byłoby prawdopodobnie skomplikowane, aw przypadku klas prawdopodobnie niemożliwe, lub przynajmniej spowolniło tworzenie instancji obiektu, ponieważ musiałbyś trzymać kod klasy w pobliżu i wykonaj go, gdy obiekty zostaną utworzone.
Tak, to nieoczekiwane. Ale gdy tylko grosz spadnie, idealnie pasuje do tego, jak ogólnie działa Python. W rzeczywistości jest to dobra pomoc dydaktyczna, a kiedy zrozumiesz, dlaczego tak się dzieje, znacznie lepiej zrozumiesz Pythona.
To powiedziawszy, powinno być widoczne w każdym dobrym samouczku Pythona. Ponieważ, jak wspomniałeś, każdy wcześniej czy później napotyka ten problem.
Dlaczego nie przyjrzysz się introspekcji?
Jestem naprawdę zaskoczony, że nikt nie przeprowadził wnikliwej introspekcji oferowanej przez Python ( 2
i 3
stosuj) na wywoływanych.
Biorąc pod uwagę prostą małą funkcję func
zdefiniowaną jako:
>>> def func(a = []):
... a.append(5)
Kiedy Python go napotka, pierwszą rzeczą, jaką zrobi, jest skompilowanie go w celu utworzenia code
obiektu dla tej funkcji. Podczas gdy ten krok kompilacji jest zakończony, Python oblicza *, a następnie przechowuje domyślne argumenty ( []
tutaj pusta lista ) w samym obiekcie funkcji . Jak wspomniano w pierwszej odpowiedzi: listę a
można teraz uważać za członka funkcji func
.
Zróbmy więc introspekcję, przed i po, aby zbadać, jak lista jest rozszerzana wewnątrz obiektu funkcji. Używam Python 3.x
do tego, dla Pythona 2 to samo dotyczy (użyj __defaults__
lub func_defaults
w Pythonie 2; tak, dwie nazwy dla tego samego).
Funkcja przed wykonaniem:
>>> def func(a = []):
... a.append(5)
...
Gdy Python wykona tę definicję, weźmie dowolne parametry domyślne określone ( a = []
tutaj) i umieści je w __defaults__atrybucie obiektu funkcji (odpowiednia sekcja: Callables):
>>> func.__defaults__
([],)
Ok, więc pusta lista jako pojedynczy wpis __defaults__
, tak jak oczekiwano.
Funkcja po wykonaniu:
Wykonajmy teraz tę funkcję:
>>> func()
Zobaczmy teraz __defaults__
ponownie:
>>> func.__defaults__
([5],)
Zdziwiony? Wartość wewnątrz obiektu zmienia się! Kolejne wywołania funkcji będą teraz po prostu dołączane do tego osadzonego list
obiektu:
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
Więc masz to, powodem, dla którego pojawia się ta „wada” , jest to, że domyślne argumenty są częścią obiektu funkcji. Nie dzieje się tu nic dziwnego, to tylko trochę zaskakujące.
Typowym rozwiązaniem tego problemu jest użycie None
jako domyślne, 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ą, nową pustą listę, jeśli żaden argument nie został przekazany a
.
Aby dodatkowo sprawdzić, czy lista w __defaults__
jest taka sama, jak ta użyta w funkcji func
, możesz po prostu zmienić swoją funkcję, aby zwracała id
listę a
używaną w treści funkcji. Następnie porównaj ją z listą w __defaults__
(pozycja [0]
w __defaults__
), a zobaczysz, w jaki sposób odnoszą się one do tej samej instancji listy:
>>> def func(a = []):
... a.append(5)
... return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
Wszystko z mocą 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 budowaniem funkcji i wiązaniem jej z nazwą bar
.
Kiedyś myślałem, że lepszym podejściem będzie tworzenie obiektów w czasie wykonywania. Jestem teraz mniej pewny, ponieważ tracisz kilka przydatnych funkcji, chociaż może to być tego warte, aby 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 funkcja jest używana bez argumentu. Za każde połączenie zapłaciłbyś wysoką cenę lub musiałbyś ręcznie buforować wartość na zewnątrz, zanieczyszczając przestrzeń nazw i dodając szczegółowość.
2. Wymuszanie parametrów związanych
Przydatną sztuczką jest powiązanie parametrów lambda z bieżącym powią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ążą i
się one z wartością czasu wywołania i, więc otrzymasz listę funkcji, które wszystkie zwróciły 9
.
Jedynym sposobem implementacji tego w innym przypadku byłoby utworzenie kolejnego domknięcia z powiązaniem i, 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 o argumentach i wartościach domyślnych za pomocą inspect
modułu, który
>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
Informacje te są bardzo przydatne w przypadku generowania dokumentów, metaprogramowania, dekoratorów itp.
Teraz załóżmy, że zachowanie wartości domyślnych można zmienić, 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 zdolność do introspekcji i zobacz, jakie argumenty domyślne są . Ponieważ obiekty nie zostały skonstruowane, nigdy nie możemy ich zdobyć bez wywołania funkcji. Najlepsze, co możemy zrobić, to przechowywać kod źródłowy i zwracać go jako ciąg.
5 punktów w obronie Pythona
Prostota : zachowanie jest proste w następującym sensie: większość ludzi wpada w tę pułapkę tylko raz, a nie kilka razy.
Spójność : Python zawsze przekazuje obiekty, a nie nazwy. Domyślny parametr jest oczywiście częścią nagłówka funkcji (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 podkreśla Frederik Lundh w swoim wyjaśnieniu „Domyślne wartości parametrów w Pythonie” , obecne zachowanie może być całkiem przydatne w zaawansowanym programowaniu. (Używaj oszczędnie.)
Wystarczająca dokumentacja : W najbardziej podstawowej dokumentacji Pythona, samouczku, problem jest głośno ogłaszany jako „Ważne ostrzeżenie” w pierwszej podsekcji sekcji „Więcej o definiowaniu funkcji” . Ostrzeżenie używa nawet pogrubienia, które rzadko jest stosowane poza nagłówkami. RTFM: Przeczytaj dokładną instrukcję.
Meta-learning : wpadnięcie w pułapkę jest w rzeczywistości bardzo pomocnym momentem (przynajmniej jeśli jesteś refleksyjnym uczniem), ponieważ później lepiej zrozumiesz powyższy punkt „Spójność”, a to nauczy Cię wiele o Pythonie.
To zachowanie można łatwo wyjaśnić:
- deklaracja funkcji (klasa itp.) jest wykonywana tylko raz, tworząc wszystkie domyślne obiekty wartości
- 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
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 jest wykonywana na tym samym obiekcie - i jest drukowana
Pytasz, dlaczego tak:
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łania funkcji func (None, None), które zignorujemy.
Innymi słowy, zamiast oceniać parametry domyślne, dlaczego nie przechowywać każdego z nich i oceniać je po wywołaniu funkcji?
Prawdopodobnie jedna odpowiedź jest tutaj - skutecznie zamieniłaby każdą funkcję z domyślnymi parametrami w zamknięcie. Nawet jeśli wszystko jest ukryte w tłumaczu i nie jest to pełne zamknięcie, dane muszą być gdzieś przechowywane. Byłoby wolniejsze i wymagałoby więcej pamięci.
1) Tak zwany problem „Mutable Default Argument” jest generalnie specjalnym przykładem pokazującym, że:
„Wszystkie funkcje z tym problemem mają również podobny problem związany z efektami ubocznymi rzeczywistego parametru ”,
co jest sprzeczne z zasadami programowania funkcjonalnego zwykle niepotrzebne i powinny być połączone 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 copy
albo deepcopy
pierwsza, 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 typów zmiennych ma metodę kopiowania, taką jak some_dict.copy()
lub, some_set.copy()
lub można je łatwo skopiować, takie jak somelist[:]
lub list(some_list)
. Każdy obiekt może być również skopiowany przez copy.copy(any_object)
lub dokładniej przez copy.deepcopy()
(ta ostatnia przydatna, jeśli obiekt zmienny składa się z obiektów mutowalnych). Niektóre obiekty są zasadniczo oparte na efektach ubocznych, takich jak obiekt „plik”, i nie można ich w znaczący sposób odtworzyć za pomocą kopii. biurowy
Przykładowy problem dotyczący 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 również zapisywać w żadnym publicznym atrybucie instancji zwracanej 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:
Obiekty parametrów wejściowych nie powinny być modyfikowane w miejscu (mutowane) ani nie powinny być wiązane z obiektem zwracanym przez funkcję. (Jeśli wolimy programowanie bez efektów ubocznych, co jest zdecydowanie zalecane. Zobacz Wiki o „skutkach ubocznych” (pierwsze dwa akapity są istotne w tym kontekście).)
2)
Tylko jeśli efekt uboczny rzeczywistego parametru jest wymagany, ale niepożądany dla parametru domyślnego, użytecznym rozwiązaniem jest def ...(var1=None):
if var1 is None:
var1 = []
Więcej ..
3) W niektórych przypadkach przydatne jest zmienne zachowanie parametrów domyślnych .
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]
W tym kodzie nie widać żadnych wartości domyślnych, ale pojawia się 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 nazwana czymś w rodzaju append_5
; wówczas obiekt wywołujący wywoływałby funkcję w celu zmodyfikowania przekazywanej wartości, a zachowanie byłoby oczekiwane. Jednak taka funkcja byłaby bardzo mało prawdopodobna, aby pobierała domyślny argument i prawdopodobnie nie zwróciłaby listy (ponieważ wywołujący ma już odniesienie do tej listy; tę, którą właśnie przekazał).
Twój oryginał foo
, z domyślnym argumentem, nie powinien modyfikować a
tego, 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 mają zostać zmodyfikowane. Używanie zmiennych wartości przekazywanych jako argumenty jako lokalne zmienne tymczasowe jest skrajnie złym pomysłem, niezależnie od tego, czy pracujemy w Pythonie, czy nie i czy są w to zaangażowane argumenty domyślne, czy nie.
Jeśli potrzebujesz destrukcyjnie manipulować lokalną zmienną tymczasową w trakcie obliczania czegoś i musisz rozpocząć manipulację od wartości argumentu, musisz zrobić kopię.
Już zajęty temat, ale z tego, co przeczytałem tutaj, pomogły mi zrozumieć, jak to 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
To optymalizacja wydajności. W wyniku tej funkcjonalności, które z tych dwóch wywołań funkcji Twoim zdaniem jest szybsze?
def print_tuple(some_tuple=(1,2,3)):
print some_tuple
print_tuple() #1
print_tuple((1,2,3)) #2
Dam ci wskazówkę. Oto demontaż (patrzhttp://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ę, aby doświadczone zachowanie miało praktyczne zastosowanie (kto naprawdę używał zmiennych statycznych w C, bez rozmnażania błędów?)
Jak widać, użycie niezmiennych argumentów domyślnych ma wpływ na wydajność. Może to mieć znaczenie, jeśli jest to często wywoływana funkcja lub jeśli konstruowanie argumentu domyślnego zajmuje dużo czasu. Pamiętaj też, że Python to nie C. W C masz stałe, które są prawie wolne. W Pythonie nie masz tej korzyści.
Python: Mutable Default Argument
Domyślne argumenty są obliczane w momencie kompilacji funkcji do obiektu funkcji. Gdy są używane przez funkcję, wielokrotnie przez tę funkcję, są i pozostają tym samym obiektem.
Kiedy są mutowalne, po zmutowaniu (na przykład poprzez dodanie do niego elementu) pozostają mutowane w kolejnych wywołaniach.
Pozostają zmutowane, ponieważ za każdym razem są tym samym obiektem.
Równoważny kod:
Ponieważ lista jest powiązana z funkcją, gdy obiekt funkcji jest kompilowany i tworzony, to:
def foo(mutable_default_argument=[]): # make a list the default argument
"""function that uses a list"""
jest prawie dokładnie równoważne 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 sprawdzić, czy są to ten sam obiekt za każdym razem, gdy się do nich odwołuje
- widząc, że lista jest tworzona przed zakończeniem kompilacji funkcji do obiektu funkcji,
- obserwując, że identyfikator jest taki sam za każdym razem, gdy istnieje odniesienie do listy,
- obserwując, że lista pozostaje zmieniona, gdy funkcja, która jej używa, zostanie wywołana po raz drugi,
- obserwując kolejność, w jakiej wydruk jest drukowany ze źródła (które wygodnie dla ciebie 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 uruchamiając go 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 wykonywania Pythona, staje się to dość oczekiwane.
Zwykła instrukcja dla nowych użytkowników Pythona:
Ale właśnie dlatego zwykłą instrukcją dla nowych użytkowników jest utworzenie zamiast tego domyślnych argumentów:
def example_function_2(default_kwarg=None):
if default_kwarg is None:
default_kwarg = []
Używa to pojedynczego obiektu None jako obiektu wartownika, aby poinformować funkcję, czy otrzymaliśmy argument inny niż domyślny. Jeśli nie otrzymamy żadnego argumentu, to w rzeczywistości chcemy użyć nowej, pustej listy []
jako domyślnej.
Jak mówi sekcja samouczka na temat przepływu sterowania :
Jeśli nie chcesz, aby wartość domyślna była współdzielona między kolejnymi wywołaniami, możesz zamiast tego napisać funkcję w następujący sposób:
def f(a, L=None): if L is None: L = [] L.append(a) return L
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 zacytować to:
def a(): return []
def b(x=a()):
print x
Miejmy 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ę, że jest to problem, gdy próbujesz użyć domyślnych konstruktorów.
Takie zachowanie nie jest zaskakujące, jeśli weźmiesz pod uwagę następujące kwestie:
- Zachowanie atrybutów klasy tylko do odczytu podczas prób przypisania i tak dalej
- Funkcje to obiekty (dobrze wyjaśnione w przyjętej odpowiedzi).
Rola (2) została obszernie omówiona w tym wątku. (1) jest prawdopodobnie czynnikiem wywołującym zdziwienie, ponieważ takie zachowanie nie jest „intuicyjne”, gdy pochodzi z innych języków.
(1) jest opisane w samouczku Pythona dotyczącym klas . 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 spowoduje po prostu utworzenie nowej zmiennej lokalnej 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 foo
obiekt i a
jest atrybutem foo
(dostępne pod adresem foo.func_defs[0]
). Ponieważ a
jest listą, a
jest zmienna i dlatego jest atrybutem do odczytu i zapisu foo
. Jest inicjowany na pustej liście zgodnie z podpisem, gdy tworzona jest instancja funkcji i jest dostępny do odczytu i zapisu, dopóki istnieje obiekt funkcji.
Wywołanie foo
bez zastępowania wartości domyślnej używa wartości domyślnej z foo.func_defs
. W tym przypadku foo.func_defs[0]
jest używany w a
ramach zakresu kodu obiektu funkcji. Zmiany do a
zmiany foo.func_defs[0]
, która jest częścią foo
obiektu i utrzymuje się między wykonaniem kodu w foo
.
Teraz porównaj to z przykładem z dokumentacji na temat emulacji domyślnego zachowania argumentów innych języków , tak że wartości domyślne podpisu funkcji są używane za każdym razem, gdy funkcja jest wykonywana:
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 powoduje to pożądane zachowanie:
- Gdy
foo
tworzona jest instancja obiektu funkcji,foo.func_defs[0]
jest ustawiony naNone
niezmienny obiekt. - Gdy funkcja jest wykonywana z ustawieniami domyślnymi (bez parametru określonego dla
L
w wywołaniu funkcji),foo.func_defs[0]
(None
) jest dostępna w zakresie lokalnym jakoL
. - W dniu
L = []
przypisanie nie może się powieśćfoo.func_defs[0]
, ponieważ ten atrybut jest tylko do odczytu. - Per (1) , nowa zmienna lokalna również nazwana
L
jest tworzona w zakresie lokalnym i używana przez pozostałą część wywołania funkcji.foo.func_defs[0]
w związku z tym pozostaje niezmieniony dla przyszłych wywołańfoo
.
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]
Mam zamiar zademonstrować alternatywną strukturę przekazywania domyślnej wartości listy do funkcji (działa równie dobrze ze słownikami).
Jak szeroko komentowali inni, parametr listy jest powiązany z funkcją, gdy jest zdefiniowany, a nie gdy jest wykonywany. Ponieważ listy i słowniki są zmienne, każda zmiana tego parametru wpłynie na inne wywołania tej funkcji. W rezultacie kolejne wywołania funkcji otrzymają tę wspólną listę, która mogła zostać zmieniona przez jakiekolwiek inne wywołania funkcji. Co gorsza, dwa parametry używają parametru współdzielonego tej funkcji w tym samym czasie nieświadomi zmian wprowadzonych przez drugi.
Niewłaściwa 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ą to jeden i ten sam obiekt, używając id
:
>>> id(a)
5347866528
>>> id(b)
5347866528
Per Brett Slatkin, „Efektywny Python: 59 Specyficzne sposoby lepszego pisania w Pythonie”, punkt 20: Używanie None
i Docstrings do określania dynamicznych argumentów domyślnych (s. 48)
Konwencją osiągnięcia pożądanego wyniku w Pythonie jest podanie domyślnej wartości
None
i udokumentowanie rzeczywistego zachowania w łańcuchu dokumentów.
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ępniać domyślny parametr listy, ale jest to bardziej prawdopodobne, że jest to wyjątek niż reguła.
Oto rozwiązania:
- Użyj
None
jako wartości domyślnej (lub wartości jednorazowejobject
) i włącz ją, aby tworzyć wartości w czasie wykonywania; lub - Użyj a
lambda
jako parametru domyślnego i wywołaj go w bloku try, aby uzyskać wartość domyślną (do tego służy abstrakcja lambda).
Druga opcja jest fajna, ponieważ użytkownicy funkcji mogą przekazać wywoływalny element, który może już istnieć (taki jak a type
)
Możesz to obejść, zastępując obiekt (a tym samym krawat z lunetą):
def foo(a=[]):
a = list(a)
a.append(5)
return a
Brzydkie, ale działa.
Kiedy to robimy:
def foo(a=[]):
...
... przypisujemy argument a
do nienazwanej listy, jeśli wywołujący nie przekazuje wartości a.
Aby ułatwić tę dyskusję, nadajmy tymczasowo nazwę nienazwanej liście. A co powiesz pavlo
?
def foo(a=pavlo):
...
W dowolnym momencie, jeśli dzwoniący nie powie nam, co a
jest, używamy ponownie pavlo
.
Jeśli pavlo
jest zmienny (modyfikowalny), a foo
kończy się jego modyfikacją, efekt, który zauważymy następnym razem, foo
jest wywoływany bez określenia a
.
Oto, co widzisz (Pamiętaj, pavlo
jest zainicjowany do []):
>>> foo()
[5]
Teraz pavlo
jest [5].
foo()
Ponowne wywołanie zmienia pavlo
ponownie:
>>> foo()
[5, 5]
Określenie, a
kiedy wywołanie foo()
zapewnia, że pavlo
nie zostanie dotknięty.
>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
Więc pavlo
jest nadal [5, 5]
.
>>> foo()
[5, 5, 5]
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 singleton
jest używany tylko przez use_singleton
, podoba mi się następujący wzór jako zamiennik:
# _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 instancji klas klientów, które mają dostęp do zewnętrznych zasobów, a także do tworzenia dykt lub list do zapamiętywania.
Ponieważ nie sądzę, aby ten wzorzec był dobrze znany, zamieszczam krótki komentarz, aby ustrzec się przed przyszłymi nieporozumieniami.
Może być prawdą, że:
- Ktoś używa wszystkich funkcji języka / biblioteki i
- Zmiana zachowania tutaj byłaby nierozsądna, ale
jest całkowicie spójne, aby trzymać się obu powyższych funkcji i jeszcze jeden punkt:
- Jest to myląca funkcja i niefortunna w Pythonie.
Inne odpowiedzi, a przynajmniej niektóre z nich, albo podają punkt 1 i 2, ale nie 3, albo zwracają uwagę na punkt 3 i bagatelizują punkty 1 i 2. Ale wszystkie trzy są prawdziwe.
Może być prawdą, że zmiana koni w środku nurtu w tym miejscu wymagałaby znacznego złamania, a zmiana Pythona na intuicyjną obsługę otwierającego fragmentu Stefano może spowodować więcej problemów. I może być prawdą, że ktoś, kto dobrze znał Pythona, mógłby wyjaśnić pole minowe konsekwencji. Jednak,
Istniejące zachowanie nie jest w Pythonie, a Python odnosi sukcesy, ponieważ bardzo niewiele w tym języku narusza zasadę najmniejszego zdziwienia, gdziekolwiek w pobliżu . To prawdziwy problem, niezależnie od tego, czy mądrze byłoby go wykorzenić. To wada konstrukcyjna. Jeśli znacznie lepiej rozumiesz język, próbując prześledzić zachowanie, mogę powiedzieć, że C ++ robi to wszystko i więcej; można się wiele nauczyć, poruszając się na przykład po subtelnych błędach wskaźnika. Ale to nie jest Python: ludzie, którym zależy na Pythonie na tyle, by wytrwać w obliczu takiego zachowania, to ludzie, których pociąga język, ponieważ Python ma znacznie mniej niespodzianek niż inne języki. Amatorzy i ciekawscy stają się Pythonistami, gdy są zdumieni tym, jak mało czasu zajmuje uzyskanie czegoś, co działa - nie z powodu błędu projektowego - mam na myśli ukrytą łamigłówkę logiczną - która łamie intuicje programistów, których pociąga Python ponieważ to po prostu działa .
To nie jest wada projektowa . Każdy, kto się o to potyka, robi coś złego.
Widzę 3 przypadki, w których możesz napotkać ten problem:
- Zamierzasz zmodyfikować argument jako efekt uboczny funkcji. W takim przypadku używanie domyślnego argumentu nigdy nie ma sensu . Jedynym wyjątkiem jest sytuacja, gdy nadużywasz listy argumentów, aby mieć atrybuty funkcji, np.
cache={}
I nie oczekuje się, że w ogóle wywołasz funkcję z rzeczywistym argumentem. - Zamierzasz pozostawić argument bez zmian, ale przypadkowo go zmodyfikowałeś. To błąd, napraw go.
- 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 zrobić kopię argumentu, niezależnie od tego, czy był on domyślny, czy nie! Python nie jest językiem wywołania według wartości, więc nie tworzy kopii za 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 przekazaną listę, jak i zwraca ją; powinieneś wybrać jedną lub drugą.
Ten „błąd” dał mi dużo nadgodzin! Ale zaczynam dostrzegać potencjalne zastosowanie tego (ale wolałbym, żeby to było w czasie wykonywania)
Dam ci to, co uważam za przydatny 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
Po prostu zmień funkcję na:
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
Myślę, że odpowiedź na to pytanie leży w tym, jak Python przekazuje dane do parametru (przekazuje przez wartość lub przez odniesienie), a nie w zmienności lub w jaki sposób Python obsługuje instrukcję „def”.
Krótkie wprowadzenie. Po pierwsze, w Pythonie istnieją dwa typy danych, jeden to prosty elementarny typ danych, taki jak liczby, a drugi to 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.
Mając na uwadze powyższe dwa punkty, wyjaśnijmy, co się stało z kodem Pythona. Dzieje się tak tylko ze względu na przekazywanie obiektów przez referencję, ale nie ma to nic wspólnego z mutable / immutable lub zapewne z faktem, że instrukcja „def” jest wykonywana tylko raz, gdy jest zdefiniowana.
[] jest obiektem, więc Python przekazuje odniesienie do [] do a
, tj. a
jest tylko wskaźnikiem do [], który znajduje się w pamięci jako obiekt. Istnieje tylko jedna kopia [] z wieloma odniesieniami do niej. Dla pierwszej foo () lista [] jest zmieniana na 1 metodą append. Ale zwróć uwagę, że istnieje tylko jedna kopia obiektu listy i ten obiekt staje się teraz 1 . Podczas uruchamiania drugiej funkcji foo (), to, co mówi strona internetowa effbot (elementy nie są już oceniane), jest błędne. a
jest oceniany jako obiekt listy, chociaż teraz zawartość obiektu to 1 . To jest efekt przechodzenia przez odniesienie! Wynik foo (3) można łatwo wyprowadzić 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 jest None
(to pierwsze jest zmienne, podczas gdy drugie jest niezmienne. Ale zmienność nie ma nic wspólnego z pytaniem). Żadnego nie ma gdzieś w przestrzeni, ale wiemy, że tam jest i jest tam tylko jedna kopia Brak. 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ło jasne, odniesienie (lub adres) None. Następnie w foo item jest zmieniany na [], tj. Wskazuje na 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 pozycje wskazują obiekt listy [] z adresem, powiedzmy 11111111. zawartość listy jest zmieniana na 1 w funkcji foo w sequelu, ale adres nie ulega zmianie, nadal 11111111 Następnie nadchodzi foo (2, []). Chociaż [] w foo (2, []) ma taką samą zawartość jak domyślny parametr [] podczas wywoływania foo (1), ich adresy są różne! Ponieważ podajemy parametr jawnie, musimy items
pobrać adres tego nowego []
, powiedzmy 2222222, i zwrócić go po dokonaniu jakiejś zmiany. Teraz wykonywany jest foo (3). ponieważ x
podano tylko , 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. Zatem pozycje są oceniane jako adres 11111111 posiadający element 1. Lista znajdująca się pod adresem 2222222 również zawiera jeden element 2, ale nie jest wskazywany przez pozycje żadne jeszcze. W konsekwencji, Anend of 3 da items
[1,3].
Z powyższych wyjaśnień wynika, że strona internetowa effbot zalecana w zaakceptowanej odpowiedzi nie udzieliła właściwej odpowiedzi na to pytanie. Co więcej, uważam, że punkt na stronie Effbot jest błędny. Myślę, że kod dotyczący przycisku 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świetli 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 damy x[9]()
9, inną wartość i
.
TLDR: wartości domyślne zdefiniowanego czasu są spójne i bardziej wyraziste.
Definiowanie funkcji wpływa na dwa zakresy: zakres definiujący zawierający funkcję i zakres wykonywania zawarty w funkcji. Chociaż jest całkiem jasne, w jaki sposób bloki mapują się na zakresy, pytanie brzmi, gdzie def <name>(<args=defaults>):
należy:
... # defining scope
def name(parameter=default): # ???
... # execution scope
def name
Część należy oceniać w zakresie definiującej - chcemy name
być dostępne tam, mimo wszystko. Ocenianie funkcji tylko wewnątrz niej uczyniłoby ją niedostępną.
Ponieważ parameter
jest to nazwa stała, możemy ją „ocenić” w tym samym czasie co def name
. Ma to również tę zaletę, że tworzy funkcję ze znaną sygnaturą name(parameter=...):
zamiast gołego name(...):
.
Kiedy teraz oceniać default
?
Spójność mówi już „w definicji”: wszystko inne def <name>(<args=defaults>):
najlepiej ocenia się również w definicji. Opóźnianie części byłoby zadziwiającym wyborem.
Te dwie opcje również nie są równoważne: Jeśli default
jest oceniane w czasie definiowania, nadal może mieć wpływ na czas wykonywania. Jeśli default
jest oceniany w czasie wykonywania, nie może wpływać na czas definicji. Wybranie „w definicji” pozwala wyrazić oba przypadki, podczas gdy wybranie „w trakcie” może wyrazić tylko jeden:
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
...
Każda inna odpowiedź wyjaśnia, dlaczego jest to w rzeczywistości przyjemne i pożądane zachowanie lub dlaczego i tak nie powinieneś tego potrzebować. Mój jest dla tych upartych, którzy chcą skorzystać z prawa do naginania języka do swojej woli, a nie na odwrót.
„Naprawimy” to zachowanie za pomocą dekoratora, który skopiuje wartość domyślną zamiast ponownego używania tej samej instancji dla każdego argumentu pozycyjnego pozostawionego przy wartości domyślnej.
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 przedefiniujmy 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 przyjmują 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!
Należy zauważyć, że powyższe rozwiązanie nie działa, jeśli spróbujesz użyć argumentów słów kluczowych, na przykład:
foo(a=[4])
Dekorator mógłby być dostosowany tak, aby to umożliwić, ale zostawiamy to jako ćwiczenie dla czytelnika;)