Argomento predefinito mutevole di Python: Perché?


20

So che gli argomenti predefiniti vengono creati al momento dell'inizializzazione della funzione e non ogni volta che viene chiamata la funzione. Vedi 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é questo è stato implementato in questo modo. Quali vantaggi offre questo comportamento rispetto a un'inizializzazione ad ogni chiamata?


Questo è già discusso in dettagli sorprendenti su Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Risposte:


14

Penso che il motivo sia la semplicità di implementazione. 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: lo valuti una volta quando viene definita la funzione 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, ottieni un po 'di velocità e aumenti di memoria. Nella maggior parte dei casi avrai argomenti di default immutabili e Python può costruirli solo una volta, invece che su ogni chiamata di funzione. Ciò consente di risparmiare (alcuni) memoria e tempo. Ovviamente, non funziona abbastanza bene con valori mutabili, ma sai come andare in giro.

Un altro vantaggio è la semplicità. È abbastanza facile capire come viene valutata l'espressione: usa l'ambito lessicale quando viene definita la funzione. Se andassero dall'altra parte, l'ambito lessicale potrebbe cambiare tra la definizione e l'invocazione e rendere il debug un po 'più difficile. Python fa molto per essere estremamente semplice in questi casi.


3
Punto interessante - anche se di solito è il principio della minima sorpresa con Python. Alcune cose sono semplici in un senso formale della complessità del modello, ma non ovvie e sorprendenti, e penso che questo conti.
Steve314,

1
La cosa meno sorprendente qui è questa: se si dispone della semantica di valutazione su ogni chiamata, è possibile che si verifichi un errore se la chiusura cambia tra due chiamate di funzione (il che è del tutto possibile). Questo può essere più sorprendente rispetto al sapere che viene valutato una volta. Certo, si può sostenere che quando si proviene da altre lingue, si esclude la semantica di valutazione su ogni chiamata e questa è la sorpresa, ma puoi vedere come va in entrambe le direzioni :)
Stefan Kanev

Un buon punto sull'ambito
0xc0de,

Penso che l'ambito sia in realtà il bit più importante. Poiché per impostazione predefinita non sei limitato alle costanti, potresti richiedere variabili che non rientrano nell'ambito del sito di chiamata.
Mark Ransom,

5

Un modo per dirlo è che il parametro lst.append(item) non muta lst. lstfa ancora riferimento allo stesso elenco. È solo che il contenuto di tale elenco è stato modificato.

Fondamentalmente, Python non ha (che ricordo) variabili costanti o immutabili, ma ha alcuni tipi costanti e immutabili. Non puoi modificare un valore intero, puoi solo sostituirlo. Ma puoi modificare il contenuto di un elenco senza sostituirlo.

Come un numero 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 come un'ottimizzazione, per risparmiare sulla creazione di oggetti e sui costi generali di garbage collection.


+1 esattamente. È importante comprendere il livello di riferimento indiretto - che una variabile non è un valore; invece fa riferimento a un valore. Per modificare una variabile, il valore può essere scambiato o mutato (se è mutabile).
Joonas Pulakka,

Di fronte a qualcosa di complicato che coinvolge le variabili in Python, trovo utile considerare "=" come "operatore di associazione dei nomi"; il nome è sempre rimbalzo, indipendentemente dal fatto che la cosa a cui lo stiamo legando sia nuova (oggetto nuovo o istanza di tipo immutabile) o meno.
StarWeaver,

4

Quali vantaggi offre questo comportamento rispetto a un'inizializzazione ad ogni chiamata?

Ti consente di selezionare il comportamento che desideri, come dimostrato nel tuo esempio. Quindi, se vuoi che l'argomento predefinito sia immutabile, usi un valore immutabile , come Noneo 1. Se si desidera rendere mutabile l'argomento predefinito, si utilizza qualcosa di mutabile, ad esempio []. È solo flessibilità, anche se è vero, può mordere se non lo conosci.


2

Penso che la vera risposta sia: Python è stato scritto come un 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 davvero solo cotte a metà. Per la prova di questo prova:

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

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

Ci sono molte cose che mi piacerebbe se Python avesse la capacità di avvolgere i parametri predefiniti nelle chiusure. Per esempio:

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

Sarebbe estremamente utile, ma "a" non è nell'ambito al momento della definizione della funzione, quindi non puoi farlo e invece devi fare il goffo:

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

Che è quasi la stessa cosa ... quasi.



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 confronto:

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

Misurazioni del tempo 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

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.

E se dicessi:

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

Qui immagino di voler creare elementi in vari elenchi e avere un unico catch-all globale quando non specifico un elenco.

Ma come potrebbe indovinarlo il compilatore? Allora perché provarci? Potremmo fare affidamento sul fatto che questo sia stato chiamato o meno, e potrebbe aiutare a volte, ma davvero sarebbe solo un'ipotesi. Allo stesso tempo, c'è una buona ragione per non provare: la coerenza.

Così com'è, Python esegue semplicemente il codice. Alla variabile list_of_all è già assegnato un oggetto, quindi quell'oggetto viene passato per riferimento nel codice che per impostazione predefinita x nello stesso modo in cui una chiamata a qualsiasi funzione otterrebbe un riferimento a un oggetto locale chiamato qui.

Se volessimo distinguere il caso senza nome dal caso indicato, ciò comporterebbe il codice durante la 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.


-5

Ciò accade perché le funzioni in Python sono oggetti di prima classe :

I valori dei parametri predefiniti vengono valutati quando viene eseguita la definizione della funzione. Ciò significa che l'espressione viene valutata una volta , quando viene definita la funzione, e che viene utilizzato lo stesso valore "pre-calcolato" per ogni chiamata .

Continua spiegando che la modifica del valore del parametro modifica il valore predefinito per le chiamate successive e che una semplice soluzione di utilizzare Nessuno come predefinito, con un test esplicito nel corpo della funzione, è tutto ciò che serve per garantire sorprese.

Ciò significa che def foo(l=[])diventa un'istanza di quella funzione quando viene chiamato e viene riutilizzato per ulteriori chiamate. Pensa ai parametri delle funzioni come a dividere gli attributi di un oggetto.

Pro potrebbe includere sfruttando questo per avere le classi hanno variabili statiche C-like. Quindi è meglio dichiarare i valori predefiniti Nessuno e inizializzarli secondo necessità:

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]


5
No. È possibile definire le funzioni (di prima classe o meno) in modo diverso per valutare nuovamente l'espressione dell'argomento predefinito per ogni chiamata. E tutto ciò che segue, cioè circa il 90% della risposta, è completamente fuori questione. -1

1
Quindi, condividi con noi questa conoscenza di come definire le funzioni per valutare l'argomento predefinito per ogni chiamata, mi piacerebbe conoscere un modo più semplice di quello raccomandato da Python Docs .
inverti il

2
A livello di progettazione linguistica intendo. La definizione del linguaggio Python attualmente afferma che gli argomenti predefiniti sono trattati come sono; potrebbe anche affermare che gli argomenti predefiniti sono trattati in qualche altro modo. IOW stai rispondendo "ecco come stanno le cose" alla domanda "perché le cose sono come sono".

Python avrebbe potuto implementare parametri predefiniti simili a quelli di Coffeescript. Inserirà bytecode per verificare la presenza di parametri mancanti e se mancavano valuterebbe l'espressione.
Winston Ewert,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.