Dobre zastosowania wartości domyślnych argumentów funkcji mutowalnych?

84

Częstym błędem w Pythonie jest ustawianie modyfikowalnego obiektu jako domyślnej wartości argumentu w funkcji. Oto przykład zaczerpnięty z tego wspaniałego artykułu Davida Goodgera :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

Wyjaśnienie, dlaczego tak się dzieje, znajduje się tutaj .

A teraz moje pytanie: czy istnieje dobry przypadek użycia dla tej składni?

Chodzi mi o to, że jeśli każdy, kto go napotka, popełnia ten sam błąd, debuguje go, rozumie problem i stara się go uniknąć, po co taka składnia?

Jonathan
źródło
1
Najlepsze wyjaśnienie, jakie znam, znajduje się w powiązanym pytaniu: funkcje są obiektami pierwszej klasy, podobnie jak klasy. Klasy mają zmienne dane atrybutów; funkcje mają zmienne wartości domyślne.
Katriel
10
To zachowanie nie jest „wyborem projektowym” - wynika ze sposobu działania języka - zaczynając od prostych zasad działania, z jak najmniejszymi wyjątkami. W pewnym momencie, kiedy zacząłem "myśleć w Pythonie", to zachowanie stało się naturalne - i byłbym zdziwiony, gdyby tak się nie stało
jsbueno
2
Też się nad tym zastanawiałem. Ten przykład jest w całej sieci, ale po prostu nie ma sensu - albo chcesz zmutować podaną listę i posiadanie wartości domyślnej nie ma sensu, albo chcesz zwrócić nową listę i powinieneś natychmiast wykonać kopię po wejściu do funkcji. Nie mogę sobie wyobrazić przypadku, w którym warto byłoby zrobić jedno i drugie.
Mark Ransom
2
Właśnie trafiłem na bardziej realistyczny przykład, który nie ma problemu, na który narzekam powyżej. Wartością domyślną jest argument __init__funkcji dla klasy, który jest ustawiany w zmiennej instancji; jest to całkowicie słuszna rzecz, którą chcieć zrobić, i wszystko idzie strasznie źle w przypadku zmiennego domyślnego. stackoverflow.com/questions/43768055/…
Mark Ransom

Odpowiedzi:

61

Możesz go użyć do buforowania wartości między wywołaniami funkcji:

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

ale zwykle tego rodzaju rzeczy robi się lepiej z klasą, ponieważ możesz wtedy mieć dodatkowe atrybuty, aby wyczyścić pamięć podręczną itp.

Duncan
źródło
12
... lub zapamiętujący dekorator.
Daniel Roseman,
29
@functools.lru_cache(maxsize=None)
Katriel
3
@katrielalex lru_cache jest nowością w Pythonie 3.2, więc nie każdy może z niego korzystać.
Duncan
2
Do Twojej wiadomości jest teraz backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
Panda
1
lru_cachejest niedostępne, jeśli masz wartości, których nie można mieszać.
Synedraacus,
14

Kanoniczną odpowiedzią jest ta strona: http://effbot.org/zone/default-values.htm

Wymienia również 3 „dobre” przypadki użycia dla mutowalnego argumentu domyślnego:

  • powiązanie zmiennej lokalnej z bieżącą wartością zmiennej zewnętrznej w wywołaniu zwrotnym
  • pamięć podręczna / zapamiętywanie
  • lokalne ponowne wiązanie nazw globalnych (dla wysoce zoptymalizowanego kodu)
Peter M. - oznacza Monikę
źródło
12

Może nie zmieniasz mutowalnego argumentu, ale spodziewasz się mutowalnego argumentu:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(Tak, wiem, że możesz użyć config=()w tym konkretnym przypadku, ale uważam, że jest to mniej jasne i mniej ogólne).

Przywróć Monikę
źródło
3
Upewnij się również, że nie dokonujesz mutacji i nie zwracasz tej wartości domyślnej bezpośrednio z funkcji, w przeciwnym razie jakiś kod spoza funkcji może ją zmodyfikować i wpłynie to na wszystkie wywołania funkcji.
Andrey Semakin
11
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

Używa randommodułu, efektywnie modyfikowalnego singletona, jako domyślnego generatora liczb losowych.

Fred Foo
źródło
7
Ale to też nie jest strasznie ważny przypadek użycia.
Evgeni Sergeev
3
Myślę, że nie ma różnicy w zachowaniu, między Pythonem „uzyskaj referencję raz” a nie-Pythonem „wyszukaj randomraz na wywołanie funkcji”. Oba kończą się użyciem tego samego przedmiotu.
nyanpasu64
4

EDYCJA (wyjaśnienie): Problem z modyfikowalnym domyślnym argumentem jest objawem głębszego wyboru projektu, a mianowicie tego, że domyślne wartości argumentów są przechowywane jako atrybuty obiektu funkcji. Możesz zapytać, dlaczego dokonano takiego wyboru; jak zawsze na takie pytania trudno odpowiedzieć właściwie. Ale z pewnością ma dobre zastosowania:

Optymalizacja pod kątem wydajności:

def foo(sin=math.sin): ...

Przechwytywanie wartości obiektów w zamknięciu zamiast zmiennej.

callbacks = []
for i in range(10):
    def callback(i=i): ...
    callbacks.append(callback)
Katriel
źródło
7
liczby całkowite i funkcje wbudowane nie są modyfikowalne!
Przywróć Monikę
2
@Jonathan: W pozostałym przykładzie nadal nie ma zmiennego argumentu domyślnego, czy po prostu go nie widzę?
Przywróć Monikę
2
@Jonathan: nie chodzi mi o to, że są one zmienne. Chodzi o to, że system, którego Python używa do przechowywania domyślnych argumentów - w obiekcie funkcji, zdefiniowanym w czasie kompilacji - może być przydatny. To implikuje problem ze zmiennym domyślnym argumentem, ponieważ ponowne oszacowanie argumentu przy każdym wywołaniu funkcji sprawi, że sztuczka będzie bezużyteczna.
Katriel
2
@katriealex: OK, ale proszę, powiedz to w swojej odpowiedzi, że zakładasz, że argumenty będą musiały zostać ponownie ocenione i że pokazujesz, dlaczego byłoby to złe. Nit-pick: domyślne wartości argumentów nie są przechowywane w czasie kompilacji, ale podczas wykonywania instrukcji definicji funkcji.
Przywróć Monikę
@WolframH: true: P! Chociaż te dwa często się pokrywają.
Katriel
0

Wiem, że to jest stary, ale tak na marginesie chciałbym dodać przypadek użycia do tego wątku. Regularnie piszę niestandardowe funkcje i warstwy dla TensorFlow / Keras, przesyłam moje skrypty na serwer, trenuję tam modele (z niestandardowymi obiektami), a następnie zapisuję modele i pobieram je. Aby załadować te modele, muszę następnie dostarczyć słownik zawierający wszystkie te niestandardowe obiekty.

To, co możesz zrobić w sytuacjach takich jak moja, to dodać kod do modułu zawierającego te niestandardowe obiekty:

custom_objects = {}

def custom_object(obj, storage=custom_objects):
    storage[obj.__name__] = obj
    return obj

Następnie mogę po prostu ozdobić dowolną klasę / funkcję, która musi znajdować się w słowniku

@custom_object
def some_function(x):
    return 3*x*x + 2*x - 2

Co więcej, powiedzmy, że chcę przechowywać moje niestandardowe funkcje utraty danych w innym słowniku niż moje niestandardowe warstwy Keras. Korzystanie z functools.partial daje mi łatwy dostęp do nowego dekoratora

import functools
import tf

custom_losses = {}
custom_loss = functools.partial(custom_object, storage=custom_losses)

@custom_loss
def my_loss(y, y_pred):
    return tf.reduce_mean(tf.square(y - y_pred))
Szymon
źródło
-1

W odpowiedzi na pytanie o dobre zastosowania zmiennych domyślnych wartości argumentów, przedstawiam następujący przykład:

Zmienna wartość domyślna może być przydatna do programowania łatwych w użyciu, możliwych do zaimportowania poleceń własnego autorstwa. Zmienna domyślna metoda sprowadza się do posiadania prywatnych, statycznych zmiennych w funkcji, którą można zainicjować przy pierwszym wywołaniu (bardzo podobnie do klasy), ale bez konieczności uciekania się do zmiennych globalnych, bez konieczności używania opakowania i bez konieczności tworzenia instancji obiekt klasy, który został zaimportowany. Jest na swój sposób elegancki, na co mam nadzieję, że się zgodzisz.

Rozważ te dwa przykłady:

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

Jeśli uruchomisz ten kod, zobaczysz, że funkcja dittle () internalizuje się przy pierwszym wywołaniu, ale nie przy dodatkowych wywołaniach, używa prywatnej statycznej pamięci podręcznej (zmienna wartość domyślna) do wewnętrznej pamięci statycznej między wywołaniami, odrzuca próby przejęcia pamięć statyczna jest odporna na złośliwe dane wejściowe i może działać w oparciu o warunki dynamiczne (w tym przypadku na temat liczby wywołań funkcji).

Klucz do używania mutable defaults nie robi niczego, co spowoduje ponowne przypisanie zmiennej w pamięci, ale zawsze zmienia zmienną w miejscu.

Aby naprawdę zobaczyć potencjalną moc i użyteczność tej techniki, zapisz ten pierwszy program w bieżącym katalogu pod nazwą „DITTLE.py”, a następnie uruchom następny program. Importuje i wykorzystuje nasze nowe polecenie dittle () bez konieczności zapamiętywania jakichkolwiek kroków ani programowania przeskakiwania tamborków.

Oto nasz drugi przykład. Skompiluj i uruchom to jako nowy program.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Czy to nie jest tak gładkie i czyste, jak to tylko możliwe? Te zmienne wartości domyślne mogą naprawdę się przydać.

========================

Po chwili zastanowienia się nad moją odpowiedzią nie jestem pewien, czy zrobiłem różnicę między używaniem domyślnej metody mutowalnej a zwykłym sposobem osiągania tego samego.

Zwykłym sposobem jest użycie funkcji możliwej do zaimportowania, która otacza obiekt klasy (i używa funkcji globalnej). Dla porównania, tutaj metoda oparta na klasach, która próbuje zrobić to samo, co domyślna metoda mutowalna.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

Zapisz ten program oparty na klasie w swoim bieżącym katalogu jako DITTLE.py, a następnie uruchom następujący kod (taki sam jak wcześniej).

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Porównując te dwie metody, korzyści wynikające z używania zmiennego ustawienia domyślnego w funkcji powinny być wyraźniejsze. Zmienna domyślna metoda nie wymaga zmiennych globalnych, jej zmiennych wewnętrznych nie można ustawić bezpośrednio. I chociaż metoda mutowalna zaakceptowała przekazany przez wiedzę argument dla pojedynczego cyklu, a następnie zlekceważyła go, metoda Class została trwale zmieniona, ponieważ jej zmienne wewnętrzne są bezpośrednio widoczne na zewnątrz. Którą metodę łatwiej zaprogramować? Myślę, że zależy to od Twojego komfortu z metodami i złożonością celów.

user10637953
źródło