Argomento predefinito mutabile di Python: Perché?

18

So che gli argomenti predefiniti vengono creati al momento dell'inizializzazione della funzione e non ogni volta che viene chiamata la funzione. Vedere il seguente codice:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Quello che non capisco è perché è stato implementato in questo modo. Quali vantaggi offre questo comportamento rispetto all'inizializzazione ad ogni orario di chiamata?

    
posta Sardathrion 19.07.2012 - 10:39
fonte

7 risposte

14

Penso che la ragione sia l'implementazione della semplicità. Lasciami elaborare.

Il valore predefinito della funzione è un'espressione che è necessario valutare. Nel tuo caso è un'espressione semplice che non dipende dalla chiusura, ma può essere qualcosa che contiene variabili libere - def ook(item, lst = something.defaultList()) . Se devi progettare Python, avrai una scelta: la valuti una volta quando la funzione è definita o ogni volta che viene chiamata la funzione. Python sceglie il primo (a differenza di Ruby, che va con la seconda opzione).

Ci sono alcuni vantaggi per questo.

Innanzitutto, aumenta la velocità e la memoria. Nella maggior parte dei casi avrai degli argomenti predefiniti immutabili e Python li può costruire solo una volta, invece che su ogni chiamata di funzione. Ciò consente di risparmiare (alcuni) memoria e tempo. Certo, non funziona abbastanza bene con i valori mutabili, ma sai come puoi andare in giro.

Un altro vantaggio è la semplicità. È abbastanza facile capire come viene valutata l'espressione - utilizza l'ambito lessicale quando viene definita la funzione. Se sono andati nell'altro modo, lo scope lessicale potrebbe cambiare tra la definizione e l'invocazione e renderlo un po 'più difficile da eseguire il debug. In questi casi Python è molto più semplice per essere estremamente semplice.

    
risposta data 19.07.2012 - 12:49
fonte
5

Un modo per dirla è che lst.append(item) non muta il parametro lst . lst fa ancora riferimento alla stessa lista. È solo che il contenuto di quell'elenco è stato modificato.

Fondamentalmente, Python non ha affatto (che io ricordi) alcuna variabile costante o immutabile - ma ha alcuni tipi costanti e immutabili. Non è possibile modificare un valore intero, è possibile solo sostituirlo. Ma puoi modificare il contenuto di un elenco senza sostituirlo.

Come un intero, non puoi modificare un riferimento, puoi solo sostituirlo. Ma puoi modificare il contenuto dell'oggetto a cui fai riferimento.

Per quanto riguarda la creazione dell'oggetto predefinito una volta, immagino che sia principalmente un'ottimizzazione, da salvare sui costi di creazione di oggetti e di garbage collection.

    
risposta data 19.07.2012 - 11:22
fonte
3

What benefits does this behaviour offers over an initialisation at each call time?

Ti consente di selezionare il comportamento che desideri, come hai dimostrato nell'esempio. Quindi, se vuoi che l'argomento predefinito sia immutabile, usi un valore immutabile , come None o 1 . Se vuoi rendere mutabile l'argomento predefinito, usa qualcosa di mutabile, come [] . È solo la flessibilità, anche se è vero, può mordere se non lo sai.

    
risposta data 19.07.2012 - 11:12
fonte
2

Penso che la vera risposta sia: Python è stato scritto come linguaggio procedurale e ha adottato solo aspetti funzionali dopo il fatto. Quello che stai cercando è che il default dei parametri sia fatto come una chiusura, e le chiusure in Python sono in realtà solo mezzo cotto. Per prova di questo tentativo:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

che dà [2, 2, 2] dove ti aspetteresti che una chiusura vera produca [0, 1, 2] .

Ci sono un sacco di cose che mi piacerebbe se Python avesse la possibilità di avvolgere parametri predefiniti nelle chiusure. Ad esempio:

def foo(a, b=a.b):
    ...

Sarebbe estremamente utile, ma "a" non è nel campo di applicazione al momento della definizione della funzione, quindi non puoi farlo e invece devi fare il clunky:

def foo(a, b=None):
    if b is None:
        b = a.b

Che è quasi la stessa cosa ... quasi.

    
risposta data 08.04.2013 - 16:40
fonte
1

Un enorme vantaggio è la memoizzazione. Questo è un esempio standard:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

e per il confronto:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Misure temporali in ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s
    
risposta data 16.05.2018 - 08:10
fonte
0

Succede perché la compilazione in Python viene eseguita eseguendo il codice descrittivo.

Se uno ha detto

def f(x = {}):
    ....

sarebbe abbastanza chiaro che volevi un nuovo array ogni volta.

Ma cosa succede se dico:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Qui indovinerei che voglio creare cose in vari elenchi, e avere un unico catch-all globale quando non specificherò una lista.

Ma come lo indovinerebbe il compilatore? Allora perché provare? Potremmo fare affidamento sul fatto che questo sia stato nominato o meno, e potrebbe essere utile a volte, ma in realtà sarebbe solo una supposizione. Allo stesso tempo, c'è una buona ragione per non provare - coerenza.

Come è, Python esegue il codice. Alla variabile list_of_all viene assegnato già un oggetto, in modo tale che l'oggetto sia passato per riferimento nel codice che imposta automaticamente x allo stesso modo in cui una chiamata a qualsiasi funzione otterrebbe un riferimento a un oggetto locale chiamato qui.

Se volessimo distinguere il nome senza nome dal caso nominato, ciò implicherebbe il codice alla compilazione che esegue l'assegnazione in un modo significativamente diverso da quello che viene eseguito in fase di esecuzione. Quindi non facciamo il caso speciale.

    
risposta data 28.08.2014 - 00:39
fonte
-5

Questo succede perché le funzioni in Python sono oggetti di prima classe :

Default parameter values are evaluated when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call.

Si prosegue spiegando che la modifica del valore del parametro modifica il valore predefinito per le chiamate successive e che una soluzione semplice dell'utilizzo di None come predefinito, con un test esplicito nel corpo della funzione, è tutto ciò che è necessario per garantire sorprese.

Ciò significa che def foo(l=[]) diventa un'istanza di quella funzione quando viene chiamata e viene riutilizzata per ulteriori chiamate. Pensa ai parametri della funzione come se si separassero dagli attributi di un oggetto.

I pro potrebbero includere l'utilizzo di questo per far sì che le classi abbiano variabili statiche di tipo C. Quindi è meglio dichiarare i valori predefiniti Nessuno e inizializzarli come necessario:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

rendimenti:

[5] [5] [5] [5]

invece dell'imprevisto:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]

    
risposta data 19.07.2012 - 17:01
fonte

Leggi altre domande sui tag