Domanda "Almost Astonishment" e l'argomento Mutable Default


Chiunque armeggi con Python abbastanza a lungo è stato morso (o fatto a pezzi) dal seguente problema:

def foo(a=[]):
    a.append(5)
    return a

I principianti di Python si aspetterebbero che questa funzione restituisca sempre una lista con un solo elemento: [5]. Il risultato è invece molto diverso e molto sorprendente (per un novizio):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Un mio manager una volta ha avuto il suo primo incontro con questa funzione e l'ha definito "un difetto di design drammatico" della lingua. Ho risposto che il comportamento aveva una spiegazione sottostante, ed è davvero molto sconcertante e inaspettato se non capisci gli interni. Tuttavia, non ero in grado di rispondere (a me stesso) alla seguente domanda: qual è la ragione per legare l'argomento predefinito alla definizione della funzione e non all'esecuzione della funzione? Dubito che il comportamento esperto abbia un uso pratico (chi ha veramente usato le variabili statiche in C, senza generare bug?)

modificare:

Baczek ha fatto un esempio interessante. Insieme alla maggior parte dei tuoi commenti e di Utaal in particolare, ho approfondito ulteriormente:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Per me, sembra che la decisione di progettazione fosse relativa a dove collocare l'ambito dei parametri: all'interno della funzione o "insieme" con esso?

Fare il legame all'interno della funzione significherebbe quello x viene effettivamente associato al valore predefinito specificato quando la funzione viene chiamata, non definita, qualcosa che presenterebbe un difetto profondo: il def la linea sarebbe "ibrida" nel senso che parte del bind (dell'oggetto della funzione) avverrebbe alla definizione e parte (assegnazione dei parametri predefiniti) al momento dell'invocazione della funzione.

Il comportamento effettivo è più consistente: tutto di quella linea viene valutato quando viene eseguita quella riga, ovvero alla definizione della funzione.


2049
2017-07-15 18:00


origine


risposte:


In realtà, questo non è un difetto di progettazione, e non è dovuto a internals o performance.
Viene semplicemente dal fatto che le funzioni in Python sono oggetti di prima classe e non solo un pezzo di codice.

Non appena riesci a ragionare in questo modo, allora ha perfettamente senso: una funzione è un oggetto che viene valutato sulla sua definizione; i parametri di default sono una specie di "dati membro" e quindi il loro stato può cambiare da una chiamata all'altra - esattamente come in qualsiasi altro oggetto.

In ogni caso, Effbot ha una spiegazione molto buona delle ragioni di questo comportamento in Valori dei parametri predefiniti in Python.
L'ho trovato molto chiaro e suggerisco di leggerlo per una migliore conoscenza di come funzionano gli oggetti funzione.


1349
2017-07-17 21:29



Supponiamo di avere il seguente codice

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Quando vedo la dichiarazione di mangiare, la cosa meno stupefacente è pensare che se non viene dato il primo parametro, sarà uguale alla tupla ("apples", "bananas", "loganberries")

Tuttavia, supposto più avanti nel codice, faccio qualcosa di simile

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

quindi se i parametri di default fossero vincolati all'esecuzione della funzione piuttosto che alla dichiarazione di funzione, sarei stupito (in un modo pessimo) di scoprire che i frutti erano stati modificati. Sarebbe più IMO sorprendente che scoprire che il tuo foola funzione sopra stava mutando la lista.

Il vero problema è rappresentato dalle variabili mutabili e in tutte le lingue questo problema è in qualche misura presente. Ecco una domanda: supponiamo che in Java abbia il seguente codice:

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?

Ora, la mia mappa usa il valore di StringBuffer chiave quando è stata inserita nella mappa o memorizza la chiave per riferimento? Ad ogni modo, qualcuno è stupito; o la persona che ha cercato di estrarre l'oggetto dal Map usando un valore identico a quello con cui l'hanno messo, o la persona che non sembra recuperare il loro oggetto anche se la chiave che stanno usando è letteralmente lo stesso oggetto che è stato usato per metterlo nella mappa (questo è in realtà perché Python non consente l'utilizzo dei suoi tipi di dati incorporabili mutabili come chiavi del dizionario).

Il tuo esempio è buono in un caso in cui i nuovi arrivati ​​di Python saranno sorpresi e morsi. Ma direi che se "aggiustiamo" questo, allora creeremmo solo una situazione diversa in cui verrebbero invece morsi, e questo sarebbe ancora meno intuitivo. Inoltre, questo è sempre il caso quando si tratta di variabili mutabili; incontri sempre casi in cui qualcuno potrebbe intuitivamente aspettarsi uno o il comportamento contrario a seconda del codice che sta scrivendo.

A me personalmente piace l'approccio attuale di Python: gli argomenti delle funzioni predefinite vengono valutati quando la funzione è definita e quell'oggetto è sempre l'impostazione predefinita. Suppongo che potrebbero usare un elenco speciale con una lista vuota, ma quel tipo di involucro speciale causerebbe ancora più stupore, per non parlare dell'incompatibilità all'indietro.


231
2017-07-15 18:11



AFAICS nessuno ha ancora pubblicato la parte pertinente del documentazione:

I valori dei parametri predefiniti vengono valutati quando viene eseguita la definizione della funzione. Ciò significa che l'espressione viene valutata una volta, quando la funzione è definita, e che lo stesso valore "pre-calcolato" viene utilizzato per ogni chiamata. Ciò è particolarmente importante per capire quando un parametro predefinito è un oggetto mutabile, come un elenco o un dizionario: se la funzione modifica l'oggetto (ad esempio aggiungendo un elemento a un elenco), il valore predefinito viene modificato. Questo generalmente non è ciò che era inteso. Un modo per aggirare questo è usare None come predefinito, e testarlo esplicitamente nel corpo della funzione [...]


195
2017-07-10 14:50



Non so nulla del funzionamento interno dell'interprete Python (e non sono un esperto di compilatori e interpreti) quindi non incolpare me se propongo qualcosa di insensibile o impossibile.

A condizione che gli oggetti pitone sono mutabili Penso che questo dovrebbe essere tenuto in considerazione durante la progettazione degli argomenti predefiniti. Quando si crea un'istanza di un elenco:

a = []

ti aspetti di ottenere un nuovo lista di riferimento di un.

Perché dovrebbe l'a = [] in

def x(a=[]):

creare un'istanza di una nuova lista sulla definizione della funzione e non sull'invocazione? È proprio come stai chiedendo "se l'utente non fornisce l'argomento allora istanziare una nuova lista e usarla come se fosse prodotta dal chiamante ". Penso invece che sia ambiguo:

def x(a=datetime.datetime.now()):

utente, vuoi un per default al datetime corrispondente a quando si sta definendo o si sta eseguendo X? In questo caso, come nel precedente, manterrò lo stesso comportamento come se l'argomento predefinito "assignment" fosse la prima istruzione della funzione (datetime.now () chiamata su invocazione funzione). D'altra parte, se l'utente desiderava la mappatura del tempo di definizione, poteva scrivere:

b = datetime.datetime.now()
def x(a=b):

Lo so, lo so: questa è una chiusura. In alternativa, Python potrebbe fornire una parola chiave per forzare il binding in fase di definizione:

def x(static a=b):

97
2017-07-15 23:21



Bene, il motivo è semplicemente che i collegamenti vengono eseguiti quando il codice viene eseguito e la definizione della funzione viene eseguita, beh ... quando le funzioni vengono definite.

Confronta questo:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Questo codice soffre della stessa identica imprevidenza. bananas è un attributo di classe, e quindi, quando si aggiungono cose ad esso, viene aggiunto a tutte le istanze di quella classe. La ragione è esattamente la stessa.

E 'solo "How It Works", e farlo funzionare diversamente nella funzione case sarebbe probabilmente complicato, e nel caso in classe probabilmente impossibile, o almeno rallentare l'istanziazione degli oggetti molto, come si dovrebbe mantenere il codice della classe in giro ed eseguirlo quando gli oggetti vengono creati.

Sì, è inaspettato. Ma una volta che il penny scende, si adatta perfettamente a come Python funziona in generale. In effetti, è un buon aiuto per l'insegnamento e, una volta compreso il motivo per cui ciò accadrà, riuscirai a ingannare molto meglio Python.

Detto questo dovrebbe apparire in primo piano in ogni buon tutorial di Python. Perché come dici, prima o poi tutti si imbattono in questo problema.


72
2017-07-15 18:54



Ero solito pensare che creare gli oggetti in fase di esecuzione sarebbe stato l'approccio migliore. Ora sono meno sicuro, poiché perdi alcune funzioni utili, anche se potrebbe valerne la pena, a prescindere semplicemente dalla prevenzione della confusione dei newbie. Gli svantaggi di farlo sono:

1. Prestazioni

def foo(arg=something_expensive_to_compute())):
    ...

Se viene utilizzata la valutazione del tempo di chiamata, la funzione costosa viene chiamata ogni volta che la funzione viene utilizzata senza argomenti. Pagheresti un prezzo costoso per ogni chiamata o dovrai memorizzare manualmente il valore esternamente, inquinando il tuo spazio dei nomi e aggiungendo verbosità.

2. Forzare i parametri associati

Un trucco utile è quello di associare i parametri di un lambda al attuale associazione di una variabile quando viene creata la lambda. Per esempio:

funcs = [ lambda i=i: i for i in range(10)]

Questo restituisce un elenco di funzioni che restituiscono rispettivamente 0,1,2,3 .... Se il comportamento è cambiato, si legheranno invece i al Chiamami valore di i, in modo da ottenere un elenco di funzioni restituite tutte 9.

L'unico modo per implementarlo diversamente sarebbe creare un'ulteriore chiusura con il limite i, ovvero:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspezione

Considera il codice:

def foo(a='test', b=100, c=[]):
   print a,b,c

Possiamo ottenere informazioni sugli argomenti e le impostazioni predefinite usando il inspect modulo, che

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Questa informazione è molto utile per cose come la generazione di documenti, metaprogrammazione, decoratori ecc.

Ora, supponiamo che il comportamento dei valori predefiniti possa essere modificato in modo che questo sia l'equivalente di:

_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=[]

Tuttavia, abbiamo perso la capacità di introspezione e vediamo quali sono gli argomenti predefiniti siamo. Poiché gli oggetti non sono stati costruiti, non possiamo mai impossessarcene senza chiamare effettivamente la funzione. Il meglio che possiamo fare è archiviare il codice sorgente e restituirlo come una stringa.


50
2017-07-16 10:05



5 punti in difesa di Python

  1. Semplicità: Il comportamento è semplice nel senso seguente: La maggior parte delle persone cade in questa trappola solo una volta, non più volte.

  2. Consistenza: Python sempre passa oggetti, non nomi. Il parametro predefinito è, ovviamente, parte della funzione intestazione (non il corpo della funzione). Quindi dovrebbe essere valutato al tempo di caricamento del modulo (e solo al tempo di caricamento del modulo, a meno che non sia annidato), no al momento della chiamata della funzione.

  3. Utilità: Come Frederik Lundh sottolinea nella sua spiegazione di "Valori dei parametri predefiniti in Python", il il comportamento corrente può essere abbastanza utile per la programmazione avanzata. (Usare con parsimonia.)

  4. Documentazione sufficiente: Nella documentazione Python più basilare, il tutorial, il problema è annunciato a voce alta come un "Avviso importante" nel primo sottosezione di Sezione "Ulteriori informazioni sulla definizione delle funzioni". L'avvertenza usa anche il grassetto, che viene raramente applicato al di fuori delle intestazioni. RTFM: leggi il manuale di precisione.

  5. Meta-learning: Cadere nella trappola è in realtà molto momento utile (almeno se sei uno studente riflessivo), perché in seguito capirai meglio il punto "Consistenza" qui sopra e così sarà ti insegnano molto su Python.


47
2018-03-30 11:18



Perché non ti introspeti?

sono veramente sorpreso nessuno ha eseguito la penetrante intuizione offerta da Python (2 e 3 applicare) su callables.

Data una semplice piccola funzione func definito come:

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

Quando Python lo incontra, la prima cosa che farà è compilarlo per creare un code oggetto per questa funzione. Mentre questo passo di compilazione è fatto, Pitone Esamina* e poi I negozi gli argomenti predefiniti (una lista vuota [] qui) nella funzione oggetto stesso. Come la risposta principale menzionata: la lista a ora può essere considerato un membro della funzione func.

Quindi, facciamo un'introspezione, una prima e una dopo per esaminare come l'elenco si espande dentro l'oggetto funzione. sto usando Python 3.x per questo, per Python 2 vale lo stesso (usare __defaults__ o func_defaults in Python 2; sì, due nomi per la stessa cosa).

Funzione prima dell'esecuzione:

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

Dopo che Python esegue questa definizione, prenderà i parametri predefiniti specificati (a = [] qui) e stipateli nel __defaults__ attributo per l'oggetto funzione (sezione pertinente: Callables):

>>> func.__defaults__
([],)

O.k, quindi una lista vuota come singola voce in __defaults__, proprio come previsto.

Funzione dopo l'esecuzione:

Eseguiamo ora questa funzione:

>>> func()

Ora, vediamo quelli __defaults__ ancora:

>>> func.__defaults__
([5],)

Stupito? Il valore all'interno dell'oggetto cambia! Le chiamate consecutive alla funzione ora verranno semplicemente aggiunte a quelle incorporate list oggetto:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Quindi, ecco qua, la ragione per cui questo 'difetto' succede, è perché gli argomenti predefiniti fanno parte dell'oggetto funzione. Non c'è niente di strano qui, è tutto solo un po 'sorprendente.

La soluzione comune per combattere questo è al solito None come predefinito e quindi inizializzare nel corpo della funzione:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Poiché il corpo della funzione viene eseguito di nuovo ogni volta, si ottiene sempre una nuova lista vuota se non è stato inoltrato alcun argomento a.


Per verificare ulteriormente che l'elenco in __defaults__ è uguale a quello utilizzato nella funzione func puoi semplicemente cambiare la tua funzione per restituire il id della lista a utilizzato all'interno del corpo della funzione. Quindi, confrontalo con la lista in __defaults__ (posizione [0] in __defaults__) e vedrai come questi si riferiscono effettivamente alla stessa istanza di lista:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Tutto con il potere dell'introspezione!


* Per verificare che Python valuti gli argomenti predefiniti durante la compilazione della funzione, provare ad eseguire quanto segue:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

come noterai, input() viene chiamato prima del processo di costruzione della funzione e vincolante per il nome bar è fatto.


42
2017-12-09 07:13



Questo comportamento è facilmente spiegato da:

  1. la dichiarazione di funzione (classe ecc.) viene eseguita una sola volta, creando tutti gli oggetti valore di default
  2. tutto è passato per riferimento

Così:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a non cambia: ogni chiamata di assegnazione crea un nuovo oggetto int - viene stampato un nuovo oggetto
  2. b non cambia: il nuovo array viene creato dal valore predefinito e stampato
  3. c cambia - l'operazione viene eseguita sullo stesso oggetto - e viene stampata

40
2017-07-15 19:15



Quello che stai chiedendo è perché questo:

def func(a=[], b = 2):
    pass

non è internamente equivalente a questo:

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

tranne per il caso di chiamare esplicitamente func (None, None), che ignoreremo.

In altre parole, invece di valutare i parametri di default, perché non memorizzarli e valutarli quando viene chiamata la funzione?

Probabilmente una risposta è proprio lì - trasformerebbe efficacemente ogni funzione con i parametri di default in una chiusura. Anche se è tutto nascosto nell'interprete e non in una chiusura completa, i dati devono essere memorizzati da qualche parte. Sarebbe più lento e userà più memoria.


30
2017-07-15 20:18



1) Il cosiddetto problema di "Mutable Default Argument" è in generale un esempio speciale che dimostra che:
"Tutte le funzioni con questo problema soffrire anche di problemi di effetti collaterali simili sul parametro attuale,"
Ciò è contrario alle regole della programmazione funzionale, in genere trascurabile e dovrebbe essere risolto insieme.

Esempio:

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]

Soluzione: a copia
Una soluzione assolutamente sicura è quella di copy o deepcopy prima l'oggetto di input e poi fare qualsiasi cosa con la copia.

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

Molti tipi mutabili incorporati hanno un metodo di copia come some_dict.copy() o some_set.copy()o può essere copiato facilmente come somelist[:] o list(some_list). Ogni oggetto può anche essere copiato da copy.copy(any_object) o più approfondito da copy.deepcopy() (quest'ultimo utile se l'oggetto mutabile è composto da oggetti mutabili). Alcuni oggetti sono fondamentalmente basati su effetti collaterali come l'oggetto "file" e non possono essere riprodotti in modo significativo dalla copia. copiatura

Esempio di problema per una domanda simile a 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]

Non dovrebbe essere né salvato in alcuno pubblico attributo di un'istanza restituita da questa funzione. (Supponendo che privato gli attributi di istanza non devono essere modificati dall'esterno di questa classe o sottoclassi per convenzione. cioè _var1 è un attributo privato)

Conclusione:
Gli oggetti dei parametri di input non devono essere modificati in posizione (mutati) né devono essere associati a un oggetto restituito dalla funzione. (Se preferiamo programmare senza effetti collaterali che è fortemente raccomandato Wiki su "effetto collaterale" (I primi due paragrafi sono rilevanti in questo contesto.) .)

2)
Solo se l'effetto collaterale sul parametro attuale è richiesto ma non desiderato sul parametro predefinito, allora la soluzione utile è def ...(var1=None):  if var1 is None:  var1 = []  Di Più..

3) In alcuni casi lo è utile il comportamento mutevole dei parametri di default.


29
2017-11-22 18:09