„Najmniejsze zdumienie” i argument zmienny domyślny

Jul 16 2009

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 xjest skutecznie powiązany z określonym domyślnym, gdy funkcja jest wywoływana, a nie zdefiniowana, coś, co przedstawiałoby głęboki błąd: deflinia 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

1658 rob Jul 18 2009 at 04:29

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.

278 EliCourtwright Jul 16 2009 at 01:11

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 foofunkcja 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 StringBufferklucza, 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 Mapuż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.

244 glglgl Jul 10 2012 at 21:50

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 Nonejako 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
121 Utaal Jul 16 2009 at 06:21

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 adomyś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):
83 LennartRegebro Jul 16 2009 at 01:54

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.

69 DimitrisFasarakisHilliard Dec 09 2015 at 14:13

Dlaczego nie przyjrzysz się introspekcji?

Jestem naprawdę zaskoczony, że nikt nie przeprowadził wnikliwej introspekcji oferowanej przez Python ( 2i 3stosuj) na wywoływanych.

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

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

Kiedy Python go napotka, pierwszą rzeczą, jaką zrobi, jest skompilowanie go w celu utworzenia codeobiektu 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ę amoż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.xdo tego, dla Pythona 2 to samo dotyczy (użyj __defaults__lub func_defaultsw 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 listobiektu:

>>> 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 Nonejako 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 idlistę auż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.

59 Brian Jul 16 2009 at 17:05

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ążą isię 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ą inspectmoduł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 . 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.

55 LutzPrechelt Mar 30 2015 at 18:18

5 punktów w obronie Pythona

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

  2. 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.

  3. 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.)

  4. 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ę.

  5. 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.

53 ymv Jul 16 2009 at 02:15

To zachowanie można łatwo wyjaśnić:

  1. deklaracja funkcji (klasa 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
35 GlennMaynard Jul 16 2009 at 03:18

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.

35 hynekcer Nov 23 2012 at 01:09

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 copyalbo deepcopypierwsza, a następnie obiekt wejściowy zrobić cokolwiek z kopią.

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

Wiele wbudowanych 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. _var1Jest 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 .

31 Ben May 23 2011 at 11:24

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 foojest modyfikowanie jest zmienny zmienną przekazaną z rozmówcą, gdy rozmówca nie spodziewa się tego. Taki kod byłby w porządku, gdyby funkcja została 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ć atego, 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ę.

27 Stéphane Mar 27 2015 at 06:14

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
25 JasonBaker Jul 16 2009 at 06:18

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.

25 AaronHall May 01 2016 at 23:20

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
24 Baczek Jul 16 2009 at 19:19

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 definstrukcji 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.

21 DmitryMinkovsky Apr 25 2012 at 02:43

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

  1. Zachowanie atrybutów klasy tylko do odczytu podczas prób przypisania i tak dalej
  2. 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 fooobiekt i ajest atrybutem foo(dostępne pod adresem foo.func_defs[0]). Ponieważ ajest listą, ajest 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 foobez 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 aramach zakresu kodu obiektu funkcji. Zmiany do azmiany foo.func_defs[0], która jest częścią fooobiektu i utrzymuje się między wykonaniem kodu w foo.

Teraz porównaj to z przykładem z dokumentacji 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 footworzona jest instancja obiektu funkcji, foo.func_defs[0]jest ustawiony na Noneniezmienny obiekt.
  • Gdy funkcja jest wykonywana z ustawieniami domyślnymi (bez parametru określonego dla Lw wywołaniu funkcji), foo.func_defs[0]( None) jest dostępna w zakresie lokalnym jako L.
  • 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 Ljest 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.
20 hugo24 Feb 28 2013 at 18:10

Proste obejście przy użyciu Brak

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
19 Alexander Sep 12 2015 at 13:00

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 Nonei 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 Nonei 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.

17 Marcin Mar 21 2012 at 00:22

Oto rozwiązania:

  1. Użyj Nonejako wartości domyślnej (lub wartości jednorazowej object) i włącz ją, aby tworzyć wartości w czasie wykonywania; lub
  2. Użyj a lambdajako parametru domyślnego i wywołaj go w bloku try, aby uzyskać wartość domyślną (do tego 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)

16 joedborg Jan 15 2013 at 18:02

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.

16 Saish Sep 12 2014 at 05:05

Kiedy to robimy:

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

... przypisujemy argument ado 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 ajest, używamy ponownie pavlo.

Jeśli pavlojest zmienny (modyfikowalny), a fookończy się jego modyfikacją, efekt, który zauważymy następnym razem, foojest wywoływany bez określenia a.

Oto, co widzisz (Pamiętaj, pavlojest zainicjowany do []):

 >>> foo()
 [5]

Teraz pavlojest [5].

foo()Ponowne wywołanie zmienia pavloponownie:

>>> foo()
[5, 5]

Określenie, akiedy wywołanie foo()zapewnia, że pavlonie zostanie dotknięty.

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

Więc pavlojest nadal [5, 5].

>>> foo()
[5, 5, 5]
16 bgreen-litl Feb 06 2015 at 04:44

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

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Jeśli singletonjest używany tylko przez use_singleton, podoba mi się następujący wzór 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.

13 ChristosHayward Jul 17 2009 at 02:17

Może być prawdą, że:

  1. Ktoś używa wszystkich funkcji języka / biblioteki i
  2. Zmiana zachowania tutaj byłaby nierozsądna, ale

jest całkowicie spójne, aby trzymać się obu powyższych funkcji i jeszcze jeden punkt:

  1. 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 .

10 MarkRansom Oct 18 2017 at 00:38

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:

  1. 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.
  2. Zamierzasz pozostawić argument bez zmian, ale przypadkowo go zmodyfikowałeś. To błąd, napraw go.
  3. Zamierzasz zmodyfikować argument do użycia wewnątrz funkcji, ale nie spodziewałeś się, że modyfikacja będzie widoczna poza funkcją. W takim przypadku musisz 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ą.

9 Norfeldt Jul 22 2013 at 14:35

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
8 ytpillai May 26 2015 at 06:04

Po prostu zmień funkcję na:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
7 user2384994 Aug 22 2013 at 12:58

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. ajest 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. ajest 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 itemspobrać adres tego nowego [], powiedzmy 2222222, i zwrócić go po dokonaniu jakiejś zmiany. Teraz wykonywany jest foo (3). ponieważ xpodano 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.

6 MisterMiyagi Dec 15 2018 at 19:09

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 nameCzęść należy oceniać w zakresie definiującej - chcemy namebyć dostępne tam, mimo wszystko. Ocenianie funkcji tylko wewnątrz niej uczyniłoby ją niedostępną.

Ponieważ parameterjest 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 defaultjest oceniane w czasie definiowania, nadal może mieć wpływ na czas wykonywania. Jeśli defaultjest 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
    ...
4 PrzemekD Jan 03 2019 at 14:38

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;)