"Minimo stupore" e l'argomento predefinito mutevole


2595

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

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

Novizi Python si aspetta questa funzione per restituire sempre una lista con un solo elemento: [5]. Il risultato è invece molto diverso e molto sorprendente (per un principiante):

>>> 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 definita "un difetto di progettazione drammatica" del linguaggio. Ho risposto che il comportamento aveva una spiegazione di fondo, ed è davvero molto sconcertante e inaspettato se non capisci gli interni. Tuttavia, non sono stato in grado di rispondere (a me stesso) alla seguente domanda: qual è la ragione per associare l'argomento predefinito alla definizione della funzione e non all'esecuzione della funzione? Dubito che il comportamento sperimentato abbia un uso pratico (chi ha veramente usato variabili statiche in C, senza allevare bug?)

Modifica :

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 sia stata relativa a dove mettere l'ambito dei parametri: all'interno della funzione o "insieme" con essa?

Fare l'associazione all'interno della funzione significherebbe che xè effettivamente legata all'impostazione predefinita specificata quando la funzione viene chiamata, non definita, qualcosa che presenterebbe un profondo difetto: la deflinea sarebbe "ibrida" nel senso che parte dell'associazione (di l'oggetto funzione) accadrebbe alla definizione e parte (assegnazione dei parametri predefiniti) al momento dell'invocazione della funzione.

Il comportamento effettivo è più coerente: ogni cosa di quella linea viene valutata quando viene eseguita quella linea, cioè alla definizione della funzione.



4
Non ho dubbi sul fatto che argomenti mutevoli violano il minimo principio di stupore per una persona media, e ho visto i principianti fare un passo lì, quindi sostituire eroicamente le mailing list con le tuple postali. Tuttavia, gli argomenti mutabili sono ancora in linea con Python Zen (Pep 20) e rientrano nella clausola "ovvio per l'olandese" (compreso / sfruttato dai programmatori di pit core hard core). La soluzione consigliata con la stringa di documenti è la migliore, ma la resistenza alle stringhe di documenti e ai documenti (scritti) non è così insolita al giorno d'oggi. Personalmente, preferirei un decoratore (diciamo @fixed_defaults).
Serge

5
La mia argomentazione quando mi imbatto in questo è: "Perché hai bisogno di creare una funzione che ritorni un mutevole che potrebbe essere facoltativamente un mutabile che dovresti passare alla funzione? O altera un mutabile o ne crea uno nuovo. Perché hai bisogno fare entrambi con una sola funzione? E perché l'interprete dovrebbe essere riscritto per permetterti di farlo senza aggiungere tre righe al tuo codice? " Perché stiamo parlando di riscrivere il modo in cui l'interprete gestisce le definizioni delle funzioni e le evocazioni qui. Questo è molto da fare per un caso d'uso appena necessario.
Alan Leuthard,

12
"I novizi di Python si aspetterebbero che questa funzione restituisca sempre un elenco con un solo elemento:. [5]" Sono un novizio di Python, e non me lo sarei aspettato, perché ovviamente foo([1])tornerà [1, 5], no [5]. Quello che intendevi dire è che un principiante si aspetterebbe che la funzione chiamata senza parametro tornerà sempre [5].
symplectomorphic il

2
Questa domanda chiede "Perché questo [nel modo sbagliato] è stato implementato così?" Non chiede "Qual è la strada giusta?" , che è coperto da [ Perché l'utilizzo di arg = None risolve il problema dell'argomento predefinito mutabile di Python? ] * ( stackoverflow.com/questions/10676729/… ). I nuovi utenti sono quasi sempre meno interessati al primo e molto di più al secondo, quindi a volte questo è un link / dupe molto utile da citare.
smci,

Risposte:


1613

In realtà, questo non è un difetto di progettazione e non è a causa degli interni o delle prestazioni.
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 pensare in questo modo, ha perfettamente senso: una funzione è un oggetto che viene valutato sulla sua definizione; i parametri di default sono tipo 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 bella spiegazione delle ragioni di questo comportamento in Valori dei parametri predefiniti in Python .
L'ho trovato molto chiaro e suggerisco davvero di leggerlo per una migliore conoscenza del funzionamento degli oggetti funzione.


80
A chiunque legga la risposta di cui sopra, consiglio vivamente di prenderti il ​​tempo di leggere l'articolo Effbot collegato. Oltre a tutte le altre informazioni utili, la parte su come questa funzione del linguaggio può essere utilizzata per la memorizzazione nella cache dei risultati / memoria è molto utile da sapere!
Cam Jackson,

85
Anche se si tratta di un oggetto di prima classe, si potrebbe comunque prevedere un progetto in cui il codice per ciascun valore predefinito viene archiviato insieme all'oggetto e rivalutato ogni volta che viene chiamata la funzione. Non sto dicendo che sarebbe meglio, solo che le funzioni essendo oggetti di prima classe non lo precludono completamente.
Gerrit,

312
Siamo spiacenti, ma qualsiasi cosa considerata "Il più grande WTF in Python" è sicuramente un difetto di progettazione . Questa è una fonte di bug per tutti ad un certo punto, perché nessuno si aspetta inizialmente quel comportamento - il che significa che non avrebbe dovuto essere progettato in questo modo per cominciare. Non mi interessa quali cerchi hanno dovuto saltare, avrebbero dovuto progettare Python in modo che gli argomenti predefiniti siano non statici.
BlueRaja - Danny Pflughoeft

192
Indipendentemente dal fatto che si tratti di un difetto di progettazione, la tua risposta sembra implicare che questo comportamento è in qualche modo necessario, naturale ed ovvio dato che le funzioni sono oggetti di prima classe, e semplicemente non è così. Python ha delle chiusure. Se si sostituisce l'argomento predefinito con un'assegnazione sulla prima riga della funzione, viene valutata l'espressione di ogni chiamata (utilizzando potenzialmente i nomi dichiarati in un ambito allegato). Non vi è alcun motivo per cui non sarebbe possibile o ragionevole valutare gli argomenti predefiniti ogni volta che la funzione viene chiamata esattamente nello stesso modo.
Mark Amery,

24
Il design non segue direttamente functions are objects. Nel tuo paradigma, la proposta sarebbe quella di implementare i valori predefiniti delle funzioni come proprietà anziché come attributi.
bukzor,

273

Supponiamo di avere il seguente codice

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

def eat(food=fruits):
    ...

Quando vedo la dichiarazione di mangiare, la cosa meno sorprendente è pensare che se il primo parametro non viene dato, 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 predefiniti fossero vincolati all'esecuzione della funzione piuttosto che alla dichiarazione della funzione, sarei stupito (in un modo molto brutto) di scoprire che i frutti erano stati cambiati. Questo sarebbe più sorprendente dell'IMO che scoprire che la tua foofunzione sopra stava mutando l'elenco.

Il vero problema sta nelle variabili mutabili, e tutte le lingue hanno questo problema in una certa misura. 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 della StringBufferchiave 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 Mapdall'uso di un valore identico a quello con cui lo ha inserito, o la persona che sembra non riuscire a recuperare il proprio oggetto anche se la chiave che stanno usando è letteralmente lo stesso oggetto che è stato usato per inserirlo nella mappa (questo è il motivo per cui Python non consente di utilizzare i suoi tipi di dati incorporati mutabili come chiavi del dizionario).

Il tuo esempio è un buon esempio di caso in cui i nuovi arrivati ​​di Python saranno sorpresi e morsi. Ma direi che se "risolvessimo" questo, ciò creerebbe solo una situazione diversa in cui verrebbero morsi, e che sarebbe ancora meno intuitivo. Inoltre, questo è sempre il caso quando si tratta di variabili mutabili; ti capita sempre di imbatterti in casi in cui qualcuno potrebbe intuitivamente aspettarsi uno o il comportamento opposto a seconda del codice che sta scrivendo.

Personalmente mi 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 fare un caso speciale usando un elenco vuoto, ma quel tipo di involucro speciale provocherebbe ancora più stupore, per non parlare dell'incompatibilità all'indietro.


30
Penso che sia una questione di dibattito. Stai agendo su una variabile globale. Qualsiasi valutazione eseguita in qualsiasi parte del codice che coinvolge la variabile globale ora farà riferimento (correttamente) ("mirtilli", "manghi"). il parametro predefinito potrebbe essere come qualsiasi altro caso.
Stefano Borini,

47
In realtà, non credo di essere d'accordo con il tuo primo esempio. Non sono sicuro che mi piaccia l'idea di modificare un inizializzatore come quello in primo luogo, ma se lo facessi, mi aspetto che si comporti esattamente come descrivi, cambiando il valore predefinito in ("blueberries", "mangos").
Ben Blank,

12
Il parametro predefinito è come qualsiasi altro caso. Ciò che è inaspettato è che il parametro è una variabile globale e non locale. Il che a sua volta è dovuto al fatto che il codice viene eseguito alla definizione della funzione, non alla chiamata. Una volta capito, e lo stesso vale per le lezioni, è perfettamente chiaro.
Lennart Regebro,

17
Trovo l'esempio fuorviante piuttosto che geniale. Se some_random_function()si aggiunge a fruitsinvece di assegnare ad essa, il comportamento di eat() si cambia. Questo per il meraviglioso design attuale. Se si utilizza un argomento predefinito a cui si fa riferimento altrove e quindi si modifica il riferimento dall'esterno della funzione, si stanno verificando problemi. Il vero WTF è quando le persone definiscono un nuovo argomento predefinito (un elenco letterale o una chiamata a un costruttore) e continuano a ricevere bit.
alexis,

13
Hai appena dichiarato globale riassegnato esplicitamente la tupla - non c'è assolutamente nulla di sorprendente se eatfunziona diversamente in seguito.
user3467349,

241

La parte pertinente della documentazione :

I valori dei parametri predefiniti vengono valutati da sinistra a destra quando viene eseguita la definizione della funzione. Ciò significa che l'espressione viene valutata una volta, quando viene definita la funzione, e che per ogni chiamata viene utilizzato lo stesso valore "pre-calcolato". 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 in effetti modificato. Questo non è generalmente ciò che era previsto. Un modo per aggirare questo è usare Nonecome predefinito e testarlo esplicitamente nel corpo della funzione, ad esempio:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
Le frasi "questo non è generalmente ciò che era previsto" e "un modo per aggirare questo" odore come se stessero documentando un difetto di progettazione.
bukzor,

4
@Matthew: lo so bene, ma non vale la pena. In genere vedrai le guide di stile e le linter incondizionatamente contrassegnare i valori di default mutabili come errati per questo motivo. Il modo esplicito di fare la stessa cosa è inserire un attributo nella funzione ( function.data = []) o meglio ancora, creare un oggetto.
Bukzor,

6
@bukzor: le insidie ​​devono essere annotate e documentate, motivo per cui questa domanda è buona e ha ricevuto così tanti voti. Allo stesso tempo, le insidie ​​non devono necessariamente essere rimosse. Quanti principianti di Python hanno passato un elenco a una funzione che lo ha modificato e sono rimasti scioccati nel vedere le modifiche mostrate nella variabile originale? Tuttavia, i tipi di oggetti mutabili sono meravigliosi, quando capisci come usarli. Immagino che si riduca all'opinione su questo particolare trabocchetto.
Matthew,

33
La frase "questo non è generalmente ciò che era previsto" significa "non ciò che il programmatore voleva effettivamente accadere", non "non ciò che Python dovrebbe fare".
holdenweb,

4
@holdenweb Wow, sono in ritardo alla festa. Dato il contesto, bukzor ha perfettamente ragione: stanno documentando comportamenti / conseguenze che non sono stati "intesi" quando hanno deciso che il linguaggio dovrebbe eseguire la definizione della funzione. Poiché è una conseguenza non intenzionale della loro scelta progettuale, è un difetto di progettazione. Se non fosse un difetto di progettazione, non sarebbe nemmeno necessario offrire "un modo per aggirare questo".
code_dredd

118

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

A condizione che gli oggetti Python siano mutabili, penso che questo dovrebbe essere preso in considerazione quando si progettano gli argomenti predefiniti. Quando si crea un'istanza di un elenco:

a = []

ti aspetti di ottenere un nuovo elenco a cui fa riferimento a.

Perché dovrebbe a=[]in

def x(a=[]):

creare un'istanza di un nuovo elenco sulla definizione della funzione e non sull'invocazione? È come se stessi chiedendo "se l'utente non fornisce l'argomento, quindi crea un'istanza di un nuovo elenco e usalo come se fosse stato prodotto dal chiamante". Penso invece che sia ambiguo:

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

utente, vuoi aimpostare automaticamente il datetime corrispondente a quando stai definendo o eseguendo x? In questo caso, come nel precedente, manterrò lo stesso comportamento come se l'argomento predefinito "assegnazione" fosse la prima istruzione della funzione ( datetime.now()chiamata sull'invocazione della funzione). D'altro canto, se l'utente desiderasse la mappatura dei tempi di definizione, potrebbe scrivere:

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

Lo so, lo so: è una chiusura. In alternativa, Python potrebbe fornire una parola chiave per forzare l'associazione in fase di definizione:

def x(static a=b):

11
Potresti fare: def x (a = None): E quindi, se a è None, imposta a = datetime.datetime.now ()
Anon,

20
Grazie per questo. Non potrei davvero mettere il dito sul perché questo mi irrita a non finire. L'hai fatto magnificamente con un minimo di confusione e confusione. Come qualcuno che proviene dalla programmazione di sistemi in C ++ e talvolta ingenuamente "traduce" le funzionalità del linguaggio, questo falso amico mi ha dato un calcio nel cuore della testa alla grande, proprio come gli attributi di classe. Capisco perché le cose vanno in questo modo, ma non posso fare a meno di non gradirle, non importa quale risultato positivo possa derivarne. Almeno è così contrario alla mia esperienza, che probabilmente (si spera) non lo dimenticherò mai ...
AndreasT

5
@Andreas una volta che usi Python per abbastanza tempo, inizi a vedere quanto sia logico per Python interpretare le cose come attributi di classe nel modo in cui lo fa - è solo a causa delle particolari stranezze e limitazioni di linguaggi come C ++ (e Java, e C # ...) che abbia senso class {}interpretare i contenuti del blocco come appartenenti alle istanze :) Ma quando le classi sono oggetti di prima classe, ovviamente la cosa naturale è che i loro contenuti (in memoria) riflettano i loro contenuti (nel codice).
Karl Knechtel,

6
La struttura normativa non è una stranezza o limitazione nel mio libro. So che può essere goffo e brutto, ma puoi chiamarlo una "definizione" di qualcosa. I linguaggi dinamici mi sembrano un po 'anarchici: certo che tutti sono liberi, ma hai bisogno di una struttura per convincere qualcuno a svuotare la spazzatura e aprire la strada. Immagino che io sia vecchio ... :)
AndreasT

4
La definizione della funzione viene eseguita al momento del caricamento del modulo. Il corpo della funzione viene eseguito al momento della chiamata della funzione. L'argomento predefinito fa parte della definizione della funzione, non del corpo della funzione. (Diventa più complicato per le funzioni nidificate.)
Lutz Prechelt

84

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

Confronta questo:

class BananaBunch:
    bananas = []

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

Questo codice soffre della stessa identica eventualità imprevista. bananas è un attributo di classe e, quindi, quando aggiungi elementi ad esso, viene aggiunto a tutte le istanze di quella classe. Il motivo è esattamente lo stesso.

È solo "Come funziona", e farlo funzionare diversamente nel caso funzionale sarebbe probabilmente complicato, e nel caso di classe probabilmente impossibile, o almeno rallentare molto l'istanza dell'oggetto, poiché dovresti mantenere il codice della classe ed eseguirlo quando vengono creati oggetti.

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 perché, capirai molto meglio il pitone.

Detto questo, dovrebbe essere ben visibile in qualsiasi buon tutorial su Python. Perché, come hai detto, prima o poi tutti incontrano questo problema.


Come si definisce un attributo di classe diverso per ogni istanza di una classe?
Kieveli,

19
Se è diverso per ogni istanza non è un attributo di classe. Gli attributi di classe sono attributi sulla CLASS. Da qui il nome. Quindi sono uguali per tutti i casi.
Lennart Regebro,

1
Come si definisce un attributo in una classe diverso per ogni istanza di una classe? (Ridefinito per coloro che non sono stati in grado di determinare che una persona che non ha familiarità con le convinzioni di denominazione di Python potrebbe chiedere informazioni sulle normali variabili membro di una classe).
Kieveli,

@Kievieli: stai parlando delle normali variabili membro di una classe. :-) Definisci gli attributi dell'istanza dicendo self.attribute = value in qualsiasi metodo. Ad esempio __init __ ().
Lennart Regebro,

@Kieveli: Due risposte: non puoi, perché qualsiasi cosa tu definisca a livello di classe sarà un attributo di classe e qualsiasi istanza che accede a tale attributo accederà allo stesso attributo di classe; puoi, / sort of /, usando propertys - che sono in realtà funzioni a livello di classe che agiscono come normali attributi ma salvano l'attributo nell'istanza anziché nella classe (usando self.attribute = valuecome ha detto Lennart).
Ethan Furman,

66

Perché non introspetti?

Sono davvero sorpreso che nessuno abbia eseguito l'introspezione penetrante offerta da Python ( 2e si 3applichi) sui callable.

Data una piccola funzione funcdefinita come:

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

Quando Python lo incontra, la prima cosa che farà è compilarlo per creare un codeoggetto per questa funzione. Mentre questa fase della compilazione viene eseguita, Python valuta * e quindi memorizza gli argomenti predefiniti (un elenco vuoto []qui) nell'oggetto funzione stesso . Come menzionato nella risposta principale: l'elenco aora può essere considerato un membro della funzione func.

Quindi, facciamo un po 'di introspezione, un prima e un dopo per esaminare come l'elenco viene espanso all'interno dell'oggetto funzione. Sto usando Python 3.xper questo, per Python 2 vale lo stesso (usa __defaults__o func_defaultsin Python 2; sì, due nomi per la stessa cosa).

Funzione prima dell'esecuzione:

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

Dopo che Python ha eseguito questa definizione, prenderà tutti i parametri predefiniti specificati ( a = []qui) e li riempirà __defaults__nell'attributo per l'oggetto funzione (sezione rilevante: Callables):

>>> func.__defaults__
([],)

Ok, quindi un elenco vuoto come la singola voce in __defaults__, proprio come previsto.

Funzione dopo l'esecuzione:

Eseguiamo ora questa funzione:

>>> func()

Ora, vediamo di __defaults__nuovo quelli :

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

Stupito? Il valore all'interno dell'oggetto cambia! Le chiamate consecutive alla funzione ora verranno semplicemente aggiunte listall'oggetto incorporato :

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

Quindi, il gioco è fatto, il motivo per cui questo "difetto" si verifica è perché gli argomenti predefiniti fanno parte dell'oggetto funzione. Non c'è niente di strano qui, è tutto un po 'sorprendente.

La soluzione comune per combattere questo è usare Nonecome 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 un nuovo elenco vuoto nuovo se non viene passato alcun argomento a.


Per verificare ulteriormente che l'elenco in __defaults__sia uguale a quello utilizzato nella funzione, funcè sufficiente modificare la funzione per restituire idl'elenco autilizzato all'interno del corpo della funzione. Quindi, confrontalo con l'elenco in __defaults__(posizione [0]in __defaults__) e vedrai come questi si stanno effettivamente riferendo alla stessa istanza di elenco:

>>> 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 a 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 che barvenga creato il processo di creazione della funzione e di associazione con il nome .


1
È id(...)necessario per quest'ultima verifica o l' isoperatore risponderebbe alla stessa domanda?
das-g,

1
@ das-g isandrebbe benissimo, l'ho usato solo id(val)perché penso che potrebbe essere più intuitivo.
Dimitris Fasarakis Hilliard

L'uso Nonecome predefinito limita fortemente l'utilità __defaults__dell'introspezione, quindi non penso che funzioni bene come difesa di avere il __defaults__lavoro come fa. La valutazione pigra farebbe di più per mantenere utili le impostazioni predefinite delle funzioni da entrambe le parti.
Brilliand,

58

Pensavo che creare gli oggetti in fase di esecuzione sarebbe l'approccio migliore. Sono meno sicuro ora, dal momento che perdi alcune funzionalità utili, anche se può valerne la pena, indipendentemente dal fatto di prevenire la confusione dei principianti. 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 un argomento. O pagheresti un prezzo costoso per ogni chiamata o devi memorizzare manualmente il valore nella cache esternamente, inquinando il tuo spazio dei nomi e aggiungendo verbosità.

2. Forzare i parametri associati

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

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

Ciò restituisce un elenco di funzioni che restituiscono rispettivamente 0,1,2,3 .... Se il comportamento viene modificato, verranno invece associati ial valore del tempo di chiamata di i, in modo da ottenere un elenco di funzioni che sono state restituite 9.

L'unico modo per implementarlo altrimenti sarebbe quello di 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 sui valori predefiniti usando il inspectmodulo, che

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

Queste informazioni sono molto utili per cose come generazione di documenti, metaprogrammazione, decoratori ecc.

Supponiamo ora che il comportamento delle impostazioni predefinite 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 vedere che cosa gli argomenti di default sono . Poiché gli oggetti non sono stati costruiti, non possiamo mai ottenerli senza effettivamente chiamare la funzione. Il meglio che possiamo fare è archiviare il codice sorgente e restituirlo come stringa.


1
potresti ottenere introspezione anche se per ognuna ci fosse una funzione per creare l'argomento predefinito invece di un valore. il modulo inspect chiamerà semplicemente quella funzione.
yairchu,

@SilentGhost: sto parlando se il comportamento è stato modificato per ricrearlo: crearlo una volta è il comportamento corrente e perché esiste il problema predefinito mutabile.
Brian,

1
@yairchu: Questo presuppone che la costruzione sia sicura (cioè non ha effetti collaterali). L'introspezione degli arg non dovrebbe fare nulla, ma la valutazione di codice arbitrario potrebbe finire per avere un effetto.
Brian,

1
Un design del linguaggio diverso spesso significa semplicemente scrivere le cose in modo diverso. Il tuo primo esempio potrebbe essere facilmente scritto come: _expensive = expensive (); def foo (arg = _expensive), se in particolare non lo si desidera rivalutare.
Glenn Maynard,

@Glenn - questo è ciò a cui mi riferivo con "cache la variabile esternamente" - è un po 'più prolisso e alla fine si trovano variabili extra nel proprio spazio dei nomi.
Brian,

55

5 punti in difesa di Python

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

  2. Coerenza : Python passa sempre oggetti, non nomi. Il parametro predefinito fa ovviamente parte dell'intestazione della funzione (non il corpo della funzione). Dovrebbe quindi essere valutato al momento del caricamento del modulo (e solo al momento del caricamento del modulo, a meno che non sia nidificato), non al momento della chiamata della funzione.

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

  4. Documentazione sufficiente : nella documentazione di base di Python, l'esercitazione, il problema è fortemente annunciato come "Avviso importante" nella prima sottosezione della sezione "Ulteriori informazioni sulla definizione delle funzioni" . L'avvertimento usa anche il grassetto, che raramente viene applicato al di fuori delle intestazioni. RTFM: leggere il manuale.

  5. Meta-learning : Cadere nella trappola è in realtà un momento molto utile (almeno se sei uno studente riflessivo), perché in seguito capirai meglio il punto "Coerenza" sopra e che ti insegnerà molto su Python.


18
Mi ci è voluto un anno per scoprire che questo comportamento stava rovinando il mio codice durante la produzione, finendo per rimuovere una funzionalità completa fino a quando non mi sono imbattuto in questo difetto di progettazione per caso. Sto usando Django. Poiché l'ambiente di gestione temporanea non aveva molte richieste, questo errore non ha mai avuto alcun impatto sul QA. Quando siamo entrati in funzione e abbiamo ricevuto molte richieste simultanee - alcune funzioni di utilità hanno iniziato a sovrascrivere i reciproci parametri! Fare buchi di sicurezza, bug e cosa no.
Oriadam,

7
@oriadam, senza offesa, ma mi chiedo come hai imparato Python senza imbatterti in questo prima. Sto solo imparando Python ora e questo possibile errore è menzionato nel tutorial ufficiale di Python proprio accanto alla prima menzione degli argomenti predefiniti. (Come menzionato al punto 4 di questa risposta.) Suppongo che la morale sia — piuttosto senza simpatia — leggere i documenti ufficiali della lingua che usi per creare software di produzione.
Wildcard il

Inoltre, sarebbe sorprendente (per me) se fosse stata chiamata una funzione di complessità sconosciuta oltre alla chiamata di funzione che sto effettuando.
Vatine,

52

Questo comportamento è facilmente spiegato da:

  1. la dichiarazione di funzione (classe ecc.) viene eseguita una sola volta, creando tutti gli oggetti valore predefiniti
  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 - il nuovo oggetto viene stampato
  2. b non cambia: il nuovo array viene creato dal valore predefinito e stampato
  3. c modifiche - l'operazione viene eseguita sullo stesso oggetto - e viene stampata

(In realtà, aggiungere è un cattivo esempio, ma il fatto che gli interi siano immutabili è ancora il mio punto principale.)
Anon,

L'ho realizzato con mio disappunto dopo aver verificato che, con b impostato su [], b .__ add __ ([1]) restituisce [1] ma lascia anche b ancora [] anche se gli elenchi sono modificabili. Colpa mia.
Anon,

@ANon: c'è __iadd__, ma non funziona con int. Ovviamente. :-)
Veky

35

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

fatta eccezione per il caso di chiamare esplicitamente func (Nessuno, Nessuno), che ignoreremo.

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

Una risposta è probabilmente proprio lì: trasformerebbe effettivamente ogni funzione con parametri predefiniti in una chiusura. Anche se è tutto nascosto nell'interprete e non una chiusura completa, i dati devono essere archiviati da qualche parte. Sarebbe più lento e userebbe più memoria.


6
Non avrebbe bisogno di essere una chiusura - un modo migliore di pensarci sarebbe semplicemente quello di rendere il bytecode la creazione delle impostazioni predefinite la prima riga di codice - dopo tutto quello che stai compilando il corpo in quel punto comunque - non c'è vera differenza tra il codice negli argomenti e nel codice nel corpo.
Brian,

10
Vero, ma rallenterebbe comunque Python, e in realtà sarebbe abbastanza sorprendente, a meno che tu non faccia lo stesso per le definizioni di classe, il che renderebbe stupidamente lento poiché dovresti rieseguire l'intera definizione di classe ogni volta che crei un'istanza di classe. Come accennato, la correzione sarebbe più sorprendente del problema.
Lennart Regebro,

Concordato con Lennart. Come Guido ama dire, per ogni funzione linguistica o libreria standard, c'è qualcuno là fuori che lo usa.
Jason Baker,

6
Cambiarlo ora sarebbe follia - stiamo solo esplorando perché è così. Se all'inizio avesse fatto una valutazione predefinita in ritardo, non sarebbe necessariamente sorprendente. È sicuramente vero che un tale nucleo una differenza di analisi avrebbe effetti ampi e probabilmente oscuri sulla lingua nel suo insieme.
Glenn Maynard,

35

1) Il cosiddetto problema di "Mutable Default Argument" è in generale un esempio speciale che dimostra che:
"Tutte le funzioni con questo problema soffrono anche di un problema di effetti collaterali simile sul parametro reale ",
Questo è contro le regole della programmazione funzionale, di solito indesiderabile e dovrebbe essere riparato 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 : una copia
Una soluzione assolutamente sicura è copyo deepcopyl'oggetto di input prima e poi fare qualunque 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 possono essere copiati facilmente come somelist[:]o list(some_list). Ogni oggetto può anche essere copiato da copy.copy(any_object)o più accuratamente 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 significativamente riprodotti dalla copia.copiatura

Esempio di problema per una domanda SO simile

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 alcun attributo pubblico di un'istanza restituita da questa funzione. (Supponendo che gli attributi privati dell'istanza non debbano essere modificati al di fuori di questa classe o sottoclassi per convenzione_var1 è un attributo privato)

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

2)
Solo se l'effetto collaterale sul parametro effettivo è richiesto ma indesiderato sul parametro predefinito, la soluzione utile èdef ...(var1=None): if var1 is None: var1 = [] Altro.

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


5
Spero che tu sia consapevole che Python non è un linguaggio di programmazione funzionale.
Veky,

6
Sì, Python è un linguaggio multi-paragigma con alcune caratteristiche funzionali. ("Non far sembrare ogni problema un chiodo solo perché hai un martello.") Molti di loro sono nelle migliori pratiche di Python. Python ha un interessante HOWTO Programmazione funzionale Altre caratteristiche sono chiusure e curry, non menzionate qui.
hynekcer,

1
Aggiungerei, in questa fase avanzata, che la semantica di assegnazione di Python è stata progettata esplicitamente per evitare la copia dei dati ove necessario, quindi la creazione di copie (e soprattutto di copie profonde) influenzerà negativamente sia il tempo di esecuzione che la memoria. Dovrebbero quindi essere usati solo quando necessario, ma i nuovi arrivati ​​hanno spesso difficoltà a capire quando è così.
holdenweb,

1
@holdenweb Sono d'accordo. Una copia temporanea è il modo più comune e talvolta l'unico modo possibile per proteggere i dati mutabili originali da una funzione estranea che li modifica potenzialmente. Fortunatamente una funzione che modifica irragionevolmente i dati è considerata un bug e quindi non comune.
Hynekcer,

Sono d'accordo con questa risposta. E non capisco perché il def f( a = None )costrutto sia raccomandato quando intendi davvero qualcos'altro. La copia è ok, perché non dovresti mutare gli argomenti. E quando lo fai if a is None: a = [1, 2, 3], copi comunque l'elenco.
Koddo,

30

Questo in realtà non ha nulla a che fare con i valori predefiniti, a parte il fatto che spesso si presenta come un comportamento imprevisto quando si scrivono funzioni con valori predefiniti mutabili.

>>> 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]

Nessun valore predefinito in vista in questo codice, ma si ottiene esattamente lo stesso problema.

Il problema è che si foosta modificando una variabile mutabile passata dal chiamante, quando il chiamante non si aspetta questo. Codice come questo andrebbe bene se la funzione fosse chiamata qualcosa del genereappend_5 ; quindi il chiamante chiamerebbe la funzione per modificare il valore che passa e ci si aspetterebbe dal comportamento. Ma una tale funzione sarebbe molto improbabile che prenda un argomento predefinito e probabilmente non restituirebbe l'elenco (dal momento che il chiamante ha già un riferimento a quell'elenco; quello appena passato).

Il tuo originale foo, con un argomento predefinito, non dovrebbe essere modificatoa se è stato esplicitamente passato o ottenuto il valore predefinito. Il tuo codice dovrebbe lasciare solo argomenti mutabili a meno che non sia chiaro dal contesto / nome / documentazione che gli argomenti dovrebbero essere modificati. L'uso di valori mutabili passati come argomenti come provvisori locali è una pessima idea, sia che ci troviamo in Python o meno e che siano coinvolti argomenti predefiniti o meno.

Se hai bisogno di manipolare in modo distruttivo un temporaneo locale nel corso del calcolo di qualcosa e devi iniziare la tua manipolazione da un valore di argomento, devi fare una copia.


7
Sebbene correlato, penso che si tratti di un comportamento distinto (poiché prevediamo appenddi cambiare a"sul posto"). Il fatto che un mutabile predefinito non venga riattivato su ogni chiamata è il bit "imprevisto" ... almeno per me. :)
Andy Hayden,

2
@AndyHayden se si prevede che la funzione modifichi l'argomento, perché avrebbe senso avere un valore predefinito?
Mark Ransom,

@MarkRansom l'unico esempio che mi viene in mente è cache={}. Tuttavia, sospetto che questo "minimo stupore" si presenti quando non ti aspetti (o vuoi) la funzione che stai chiamando per mutare l'argomento.
Andy Hayden,

1
@AndyHayden Ho lasciato la mia risposta qui con un ampliamento di quel sentimento. Fatemi sapere cosa ne pensate. Potrei aggiungere il tuo esempio di cache={}esso per completezza.
Mark Ransom,

1
@AndyHayden Il punto della mia risposta è che se sei mai stupito dalla mutazione accidentale del valore predefinito di un argomento, allora hai un altro bug, che è che il tuo codice può mutare accidentalmente il valore di un chiamante quando il valore predefinito non è stato usato. E nota che l'uso Nonee l'assegnazione del vero valore predefinito se arg None non risolve quel problema (lo considero un modello anti per quel motivo). Se correggi l'altro bug evitando i valori degli argomenti mutanti indipendentemente dal fatto che abbiano valori predefiniti, non ti accorgerai o ti preoccuperai mai di questo comportamento "sorprendente".
Ben

27

Argomento già occupato, ma da quello che ho letto qui, il seguente mi ha aiutato a capire come funziona internamente:

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

2
in realtà questo potrebbe essere un po 'confuso per i nuovi arrivati ​​come a = a + [1]sovraccarichi a... considera di cambiarlo b = a + [1] ; print id(b)e aggiungere una linea a.append(2). Ciò renderà più ovvio che +su due elenchi crea sempre un nuovo elenco (assegnato a b), mentre un modificato apuò comunque avere lo stesso id(a).
Jörn Hees,

25

È un'ottimizzazione delle prestazioni. Come risultato di questa funzionalità, quale di queste due chiamate di funzione pensi sia più veloce?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Ti do un suggerimento. Ecco lo smontaggio (vedi http://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

Dubito che il comportamento sperimentato abbia un uso pratico (chi ha veramente usato variabili statiche in C, senza allevare bug?)

Come si può vedere, c'è un miglioramento delle prestazioni quando si usano argomenti predefiniti immutabili. Questo può fare la differenza se si tratta di una funzione chiamata frequentemente o se l'argomento predefinito richiede molto tempo per essere costruito. Inoltre, tieni presente che Python non è C. In C hai delle costanti che sono praticamente libere. In Python non hai questo vantaggio.


24

Python: l'argomento predefinito mutevole

Gli argomenti predefiniti vengono valutati al momento della compilazione della funzione in un oggetto funzione. Se utilizzati dalla funzione, più volte da quella funzione, sono e rimangono lo stesso oggetto.

Quando sono mutabili, quando mutati (ad esempio aggiungendo un elemento ad esso) rimangono mutati per chiamate consecutive.

Rimangono mutati perché sono lo stesso oggetto ogni volta.

Codice equivalente:

Poiché l'elenco è associato alla funzione quando l'oggetto funzione viene compilato e istanziato, questo:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

è quasi esattamente equivalente a questo:

_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

Dimostrazione

Ecco una dimostrazione: puoi verificare che siano lo stesso oggetto ogni volta a cui fanno riferimento

  • visto che l'elenco viene creato prima che la funzione abbia completato la compilazione in un oggetto funzione,
  • osservando che l'id è lo stesso ogni volta che si fa riferimento all'elenco,
  • osservando che l'elenco rimane modificato quando la funzione che lo utilizza viene richiamata una seconda volta,
  • osservando l'ordine in cui l'output viene stampato dalla fonte (che ho opportunamente numerato per te):

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

e eseguendolo con 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']

Ciò viola il principio di "minimo stupore"?

Questo ordine di esecuzione è spesso fonte di confusione per i nuovi utenti di Python. Se capisci il modello di esecuzione di Python, allora diventa abbastanza previsto.

Le solite istruzioni per i nuovi utenti di Python:

Ma questo è il motivo per cui le solite istruzioni per i nuovi utenti sono invece di creare i loro argomenti predefiniti come questo:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Questo utilizza il singleton Nessuno come oggetto sentinella per dire alla funzione se abbiamo ottenuto o meno un argomento diverso da quello predefinito. Se non riceviamo alcun argomento, vogliamo effettivamente utilizzare un nuovo elenco vuoto [], come predefinito.

Come dice la sezione tutorial sul flusso di controllo :

Se non desideri che il valore predefinito sia condiviso tra le chiamate successive, puoi invece scrivere la funzione in questo modo:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

La risposta più breve sarebbe probabilmente "la definizione è esecuzione", quindi l'intero argomento non ha senso. Come esempio più ingegnoso, puoi citare questo:

def a(): return []

def b(x=a()):
    print x

Speriamo che sia sufficiente mostrare che non eseguire le espressioni degli argomenti predefinite al momento dell'esecuzione defdell'istruzione non è facile o non ha senso, o entrambi.

Sono d'accordo che è un gotcha quando provi a usare i costruttori predefiniti, però.


20

Una semplice soluzione usando Nessuno

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

Questo comportamento non è sorprendente se si considera quanto segue:

  1. Il comportamento degli attributi di classe di sola lettura durante i tentativi di assegnazione e quello
  2. Le funzioni sono oggetti (spiegate bene nella risposta accettata).

Il ruolo di (2) è stato ampiamente trattato in questo thread. (1) è probabilmente il fattore che causa lo stupore, poiché questo comportamento non è "intuitivo" quando proviene da altre lingue.

(1) è descritto nel tutorial di Python sulle classi . Nel tentativo di assegnare un valore a un attributo di classe di sola lettura:

... tutte le variabili trovate al di fuori dell'ambito più interno sono di sola lettura ( un tentativo di scrivere su tale variabile creerà semplicemente una nuova variabile locale nell'ambito più interno, lasciando invariata la variabile esterna denominata identicamente ).

Guarda l'esempio originale e considera i punti sopra:

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

Ecco fooun oggetto ed aè un attributo di foo(disponibile su foo.func_defs[0]). Poiché aè un elenco, aè modificabile ed è quindi un attributo di lettura-scrittura difoo . Viene inizializzato nell'elenco vuoto come specificato dalla firma quando la funzione è istanziata ed è disponibile per la lettura e la scrittura finché esiste l'oggetto funzione.

La chiamata foosenza sovrascrivere un valore predefinito utilizza il valore di tale valore predefinito da foo.func_defs. In questo caso, foo.func_defs[0]viene utilizzato anell'ambito del codice dell'oggetto funzione. Modifiche al acambiamento foo.func_defs[0], che fa parte foodell'oggetto e persiste tra l'esecuzione del codice in foo.

Ora, confronta questo con l'esempio della documentazione sull'emulazione del comportamento argomento predefinito di altre lingue , in modo tale che le impostazioni predefinite della firma della funzione vengano utilizzate ogni volta che la funzione viene eseguita:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Tenendo conto di (1) e (2) , si può vedere perché questo compie il comportamento desiderato:

  • Quando l' foooggetto funzione è istanziato, foo.func_defs[0]è impostato su None, un oggetto immutabile.
  • Quando la funzione viene eseguita con valori predefiniti (senza alcun parametro specificato Lnella chiamata di funzione), foo.func_defs[0]( None) è disponibile nell'ambito locale come L.
  • Al momento L = [], l'assegnazione non può avere esito positivo foo.func_defs[0], poiché tale attributo è di sola lettura.
  • Per (1) , anche una nuova variabile locale denominata Lviene creata nell'ambito locale e utilizzata per il resto della chiamata di funzione. foo.func_defs[0]rimane quindi invariato per future invocazioni di foo.

19

Ho intenzione di dimostrare una struttura alternativa per passare un valore di elenco predefinito a una funzione (funziona ugualmente bene con i dizionari).

Come altri hanno ampiamente commentato, il parametro list è associato alla funzione quando è definito rispetto a quando viene eseguito. Poiché gli elenchi e i dizionari sono modificabili, qualsiasi modifica a questo parametro influenzerà altre chiamate a questa funzione. Di conseguenza, le chiamate successive alla funzione riceveranno questo elenco condiviso che potrebbe essere stato modificato da qualsiasi altra chiamata alla funzione. Peggio ancora, due parametri utilizzano contemporaneamente il parametro condiviso di questa funzione ignaro delle modifiche apportate dall'altro.

Metodo errato (probabilmente ...) :

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

Puoi verificare che siano lo stesso oggetto usando id:

>>> id(a)
5347866528

>>> id(b)
5347866528

"Effective Python: 59 modi specifici per scrivere meglio Python" di Brett Slatkin, Item 20: Use Noneand Docstrings per specificare argomenti predefiniti dinamici (p. 48)

La convenzione per ottenere il risultato desiderato in Python è fornire un valore predefinito Nonee documentare il comportamento effettivo nella documentazione.

Questa implementazione garantisce che ogni chiamata alla funzione riceva l'elenco predefinito oppure l'elenco passato alla funzione.

Metodo preferito :

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]

Potrebbero esserci casi d'uso legittimi per il "Metodo sbagliato" in base al quale il programmatore intendeva condividere il parametro dell'elenco predefinito, ma è più probabile che l'eccezione rispetto alla regola.


17

Le soluzioni qui sono:

  1. Utilizzare Nonecome valore predefinito (o nonce object) e attivarlo per creare i valori in fase di esecuzione; o
  2. Usa a lambdacome parametro predefinito e chiamalo all'interno di un blocco try per ottenere il valore predefinito (questo è il genere di cosa a cui serve l'astrazione lambda).

La seconda opzione è utile perché gli utenti della funzione possono passare in un callable, che potrebbe essere già esistente (come un type)


16

Quando lo facciamo:

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

... assegniamo l'argomento aa un elenco senza nome , se il chiamante non passa il valore di a.

Per semplificare le cose per questa discussione, diamo temporaneamente un nome all'elenco senza nome. Che ne dici pavlo?

def foo(a=pavlo):
   ...

In qualsiasi momento, se il chiamante non ci dice cos'è a, riutilizziamo pavlo.

Se pavloè mutabile (modificabile) e foofinisce per modificarlo, un effetto che notiamo la prossima volta fooviene chiamato senza specificare a.

Quindi questo è ciò che vedi (Ricorda, pavloè inizializzato su []):

 >>> foo()
 [5]

Ora pavloè [5].

Richiamare foo()nuovamente modifica pavlo:

>>> foo()
[5, 5]

Specificando aquando chiamata foo()assicura pavlonon viene toccato.

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

Quindi, pavloè ancora [5, 5].

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

16

A volte sfrutto questo comportamento in alternativa al seguente schema:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Se singletonviene utilizzato solo da use_singleton, mi piace il seguente modello in sostituzione:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

L'ho usato per istanziare classi client che accedono a risorse esterne e anche per creare dicts o elenchi per la memoization.

Dato che non penso che questo modello sia ben noto, faccio un breve commento per proteggermi da futuri equivoci.


2
Preferisco aggiungere un decoratore per la memoizzazione e inserire la cache di memoization sull'oggetto funzione stesso.
Stefano Borini,

Questo esempio non sostituisce il modello più complesso che mostri, perché chiami _make_singletonal momento della definizione nell'esempio predefinito dell'argomento, ma al momento della chiamata nell'esempio globale. Una sostituzione vera userebbe una sorta di scatola mutabile per il valore dell'argomento predefinito, ma l'aggiunta dell'argomento offre l'opportunità di passare valori alternativi.
Yann Vernier,

15

Puoi aggirare il problema sostituendo l'oggetto (e quindi il legame con l'ambito):

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

Brutto, ma funziona.


3
Questa è una buona soluzione nei casi in cui si sta utilizzando un software di generazione automatica della documentazione per documentare i tipi di argomenti previsti dalla funzione. Mettere a = None e quindi impostare a su [] se a è None non aiuta un lettore a capire a colpo d'occhio cosa ci si aspetta.
Michael Scott Cuthbert,

Fantastica idea: il rifiuto di quel nome garantisce che non potrà mai essere modificato. Mi piace molto
holdenweb,

Questo è esattamente il modo di farlo. Python non crea una copia del parametro, quindi spetta a te fare la copia esplicitamente. Una volta che hai una copia, è tua da modificare a tuo piacimento senza effetti collaterali imprevisti.
Mark Ransom,

13

Può essere vero che:

  1. Qualcuno sta usando tutte le funzionalità di lingua / libreria e
  2. Cambiare il comportamento qui sarebbe sconsigliato, ma

è del tutto coerente mantenere entrambe le funzionalità sopra e ancora sottolineare un altro punto:

  1. È una funzionalità confusa ed è sfortunato in Python.

Le altre risposte, o almeno alcune di esse, fanno i punti 1 e 2 ma non 3, oppure fanno il punto 3 e minimizzano i punti 1 e 2. Ma tutti e tre sono veri.

Può essere vero che il passaggio di cavalli a metà corrente qui richiederebbe una rottura significativa e che potrebbero esserci più problemi creati cambiando Python per gestire intuitivamente lo snippet di apertura di Stefano. E può essere vero che qualcuno che conosceva bene gli interni di Python potrebbe spiegare un campo minato di conseguenze. Però,

Il comportamento esistente non è Pythonic e Python ha successo perché pochissimo sul linguaggio viola il principio del minimo stupore ovunque vicinoquesto male. È un vero problema, sia che sia saggio sradicarlo o meno. È un difetto di progettazione. Se capisci molto meglio il linguaggio cercando di rintracciare il comportamento, posso dire che C ++ fa tutto questo e altro; impari molto navigando, ad esempio, su sottili errori del puntatore. Ma questo non è Pythonic: le persone che si preoccupano di Python abbastanza da perseverare di fronte a questo comportamento sono persone che sono attratte dal linguaggio perché Python ha molte meno sorprese rispetto ad altre lingue. Dabblers e curiosi diventano pitonisti quando sono stupiti da quanto poco tempo ci vuole per far funzionare qualcosa - non a causa di un progetto fl - intendo un puzzle di logica nascosta - che si contrappone alle intuizioni dei programmatori che sono attratti da Python perché funziona .


6
-1 Sebbene sia una prospettiva difendibile, questa non è una risposta e non sono d'accordo. Troppe eccezioni speciali generano i loro casi d'angolo.
Marcin,

3
Quindi, è "incredibilmente ignorante" dire che in Python avrebbe più senso che un argomento predefinito di [] rimanga [] ogni volta che viene chiamata la funzione?
Christos Hayward,

3
Ed è ignorante considerare come un linguaggio sfortunato impostare un argomento predefinito su Nessuno, e quindi nel corpo del corpo dell'impostazione della funzione se argomento == Nessuno: argomento = []? È ignorante considerare questo idioma sfortunato come spesso le persone vogliono ciò che un nuovo arrivato ingenuo si aspetterebbe, che se si assegna f (argomento = []), l'argomento verrà automaticamente impostato su un valore di []?
Christos Hayward,

3
Ma in Python, parte dello spirito del linguaggio è che non devi fare troppe immersioni profonde; array.sort () funziona e funziona indipendentemente da quanto poco capisci su ordinamento, big-O e costanti. La bellezza di Python nel meccanismo di ordinamento degli array, per dare uno degli innumerevoli esempi, è che non è necessario immergersi profondamente negli interni. E per dirlo diversamente, la bellezza di Python è che non si è normalmente tenuti a fare un tuffo nell'implementazione per ottenere qualcosa che funzioni. E c'è una soluzione alternativa (... se argomento == Nessuno: argomento = []), FAIL.
Christos Hayward,

3
Come indipendente, l'istruzione x=[]significa "crea un oggetto elenco vuoto e associa il nome 'x' ad esso". Quindi, in def f(x=[]), viene anche creato un elenco vuoto. Non viene sempre associato a x, quindi viene associato al surrogato predefinito. Successivamente, quando viene chiamato f (), il valore predefinito viene rimosso e associato a x. Dal momento che era la stessa lista vuota che è stata eliminata, quella stessa lista è l'unica cosa disponibile per legare a x, se qualcosa è stato bloccato al suo interno o meno. Come potrebbe essere altrimenti?
Jerry B,

10

Questo non è un difetto di progettazione . Chiunque inciampa su questo sta facendo qualcosa di sbagliato.

Ci sono 3 casi che vedo dove potresti imbatterti in questo problema:

  1. Si intende modificare l'argomento come effetto collaterale della funzione. In questo caso non ha mai senso avere un argomento predefinito. L'unica eccezione è quando stai abusando dell'elenco degli argomenti per avere attributi di funzione, ad esempio cache={}, e non ti aspetteresti di chiamare la funzione con un argomento reale.
  2. Avete intenzione di lasciare l'argomento non modificato, ma per sbaglio ha fatto modificarlo. È un bug, risolvilo.
  3. Intendi modificare l'argomento da utilizzare all'interno della funzione, ma non ti aspettavi che la modifica fosse visualizzabile al di fuori della funzione. In tal caso, devi creare una copia dell'argomento, sia esso predefinito o no! Python non è un linguaggio di chiamata per valore, quindi non crea la copia per te, devi essere esplicito al riguardo.

L'esempio nella domanda potrebbe rientrare nella categoria 1 o 3. È strano che entrambi modifichi l'elenco passato e lo restituisca; dovresti scegliere l'uno o l'altro.


"Fare qualcosa di sbagliato" è la diagnosi. Detto questo, penso che ci siano momenti in cui il pattern = Nessuno è utile, ma in genere non si desidera modificare se si passa un mutabile in quel caso (2). Lo cache={}schema è davvero una soluzione solo per le interviste, nel vero codice che probabilmente vorrai @lru_cache!
Andy Hayden,

9

Questo "bug" mi ha dato molte ore di lavoro straordinario! Ma sto cominciando a vederne un potenziale utilizzo (ma mi sarebbe piaciuto che fosse al momento dell'esecuzione, ancora)

Ti darò quello che vedo come esempio utile.

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

stampa quanto segue

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Basta cambiare la funzione per essere:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Penso che la risposta a questa domanda risieda nel modo in cui Python passa i dati al parametro (passa per valore o per riferimento), non nella mutabilità o nel modo in cui Python gestisce l'istruzione "def".

Una breve introduzione. Innanzitutto, ci sono due tipi di tipi di dati in Python, uno è un tipo di dati elementare semplice, come i numeri, e un altro tipo di dati sono gli oggetti. In secondo luogo, quando si passano i dati ai parametri, python passa il tipo di dati elementare per valore, ovvero crea una copia locale del valore su una variabile locale, ma passa l'oggetto per riferimento, ovvero i puntatori all'oggetto.

Ammettendo i due punti precedenti, spieghiamo cosa è successo al codice Python. È solo a causa del passaggio per riferimento degli oggetti, ma non ha nulla a che fare con mutabile / immutabile, o probabilmente il fatto che l'istruzione "def" viene eseguita una sola volta quando è definita.

[] è un oggetto, quindi Python passa il riferimento di [] a a, cioè aè solo un puntatore a [] che si trova nella memoria come oggetto. Esiste una sola copia di [] con, tuttavia, molti riferimenti ad essa. Per il primo pippo (), l'elenco [] viene modificato in 1 con il metodo append. Ma nota che esiste solo una copia dell'oggetto elenco e questo oggetto ora diventa 1 . Quando si esegue il secondo pippo (), ciò che dice la pagina web di effbot (gli elementi non vengono più valutati) è sbagliato. aviene valutato come oggetto elenco, sebbene ora il contenuto dell'oggetto sia 1 . Questo è l'effetto del passaggio per riferimento! Il risultato di foo (3) può essere facilmente derivato allo stesso modo.

Per convalidare ulteriormente la mia risposta, diamo un'occhiata a due codici aggiuntivi.

====== No. 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]

[]è un oggetto, così è None(il primo è mutabile mentre il secondo è immutabile. Ma la mutabilità non ha nulla a che fare con la domanda). Nessuno è da qualche parte nello spazio ma sappiamo che è lì e c'è solo una copia di Nessuno lì. Quindi ogni volta che viene invocato foo, gli elementi vengono valutati (al contrario di una risposta che viene valutata una sola volta) come Nessuno, per essere chiari, il riferimento (o l'indirizzo) di Nessuno. Quindi nel foo, l'elemento viene cambiato in [], cioè punta a un altro oggetto che ha un indirizzo diverso.

====== No. 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

L'invocazione di foo (1) fa in modo che gli elementi puntino a un oggetto elenco [] con un indirizzo, ad esempio 11111111. il contenuto dell'elenco viene modificato in 1 nella funzione foo nel sequel, ma l'indirizzo non viene modificato, tuttavia 11111111 Quindi sta arrivando foo (2, []). Sebbene [] in foo (2, []) abbia lo stesso contenuto del parametro predefinito [] quando si chiama foo (1), il loro indirizzo è diverso! Dal momento che forniamo il parametro esplicitamente, itemsdeve prendere l'indirizzo di questo nuovo[] , diciamo 2222222, e restituirlo dopo aver apportato alcune modifiche. Ora viene eseguito foo (3). da soloxviene fornito, gli articoli devono riprendere il loro valore predefinito. Qual è il valore predefinito? Viene impostato quando si definisce la funzione foo: l'oggetto elenco situato in 11111111. Quindi gli elementi vengono valutati come l'indirizzo 11111111 con un elemento 1. L'elenco situato in 2222222 contiene anche un elemento 2, ma non è puntato da elementi Di Più. Di conseguenza, un'appendice di 3 farà items[1,3].

Dalle spiegazioni precedenti, possiamo vedere che la pagina web di effbot raccomandata nella risposta accettata non ha dato una risposta pertinente a questa domanda. Inoltre, penso che un punto della pagina web di effbot sia sbagliato. Penso che il codice relativo all'interfaccia utente sia corretto:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Ogni pulsante può contenere una funzione di richiamata distinta che visualizzerà un valore diverso di i. Posso fornire un esempio per mostrare questo:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Se eseguiamo x[7]()avremo 7 come previsto e x[9]()daremo 9, un altro valore di i.


5
Il tuo ultimo punto è sbagliato. Provalo e vedrai che lo x[7]()è 9.
Duncan,

2
"Python passa il tipo di dati elementare per valore, cioè crea una copia locale del valore in una variabile locale" è completamente errato. Sono sorpreso dal fatto che qualcuno possa ovviamente conoscere molto bene Python, eppure avere un terribile fraintendimento dei fondamenti. :-(
Veky,

6

TLDR: i valori predefiniti di Definire-time sono coerenti e strettamente più espressivi.


La definizione di una funzione influisce su due ambiti: l'ambito di definizione contenente la funzione e l'ambito di esecuzione contenuto dalla funzione. Mentre è abbastanza chiaro come i blocchi mappano agli ambiti, la domanda è dove def <name>(<args=defaults>):appartiene:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

La def nameparte deve essere valutata nell'ambito di definizione name: dopo tutto, vogliamo essere disponibili lì. Valutare la funzione solo al suo interno la renderebbe inaccessibile.

Dato che parameterè un nome costante, possiamo "valutarlo" contemporaneamente def name. Ciò ha anche il vantaggio di produrre la funzione con una firma nota come name(parameter=...):, anziché una nuda name(...):.

Ora, quando valutare default?

La coerenza dice già "alla definizione": tutto il resto def <name>(<args=defaults>):è meglio valutato alla definizione. Ritardare parti di essa sarebbe la scelta sorprendente.

Le due scelte non sono equivalenti: neanche se defaultvalutato al momento della definizione, può comunque influire sul tempo di esecuzione. Se defaultvalutato al momento dell'esecuzione, non può influire sul tempo di definizione. Scegliere "alla definizione" consente di esprimere entrambi i casi, mentre scegliere "all'esecuzione" può esprimere solo uno:

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

"La coerenza dice già" alla definizione ": tutto il resto def <name>(<args=defaults>):è meglio valutato anche alla definizione." Non credo che la conclusione derivi dalla premessa. Solo perché due cose sono sulla stessa linea non significa che dovrebbero essere valutate nello stesso ambito. defaultè una cosa diversa rispetto al resto della linea: è un'espressione. La valutazione di un'espressione è un processo molto diverso dalla definizione di una funzione.
LarsH,

@LarsH Le definizioni delle funzioni sono valutate in Python. Sia che provenga da un'istruzione ( def) o da expression ( lambda) non cambia il fatto che creare una funzione significhi una valutazione, specialmente della sua firma. E i valori predefiniti fanno parte della firma di una funzione. Ciò non significa che i valori predefiniti debbano essere valutati immediatamente, ad esempio i suggerimenti sul tipo potrebbero non essere disponibili. Ma certamente suggerisce che dovrebbero a meno che non ci sia una buona ragione per non farlo.
Mister Miyagi,

OK, creare una funzione significa valutare in un certo senso, ma ovviamente non nel senso che ogni espressione al suo interno viene valutata al momento della definizione. La maggior parte non lo è. Non mi è chiaro in che senso la firma sia specialmente "valutata" al momento della definizione più di quanto il corpo della funzione sia "valutato" (analizzato in una rappresentazione adatta); mentre le espressioni nel corpo della funzione non sono chiaramente valutate in senso pieno. Da questo punto di vista, la coerenza direbbe che neanche le espressioni nella firma dovrebbero essere valutate "completamente".
LarsH,

Non voglio dire che ti sbagli, solo che le tue conclusioni non seguono solo la coerenza.
LarsH,

@LarsH I valori predefiniti non fanno parte del corpo, né sto affermando che la coerenza sia l'unico criterio. Puoi dare un suggerimento su come chiarire la risposta?
Mister Miyagi,

3

Ogni altra risposta spiega perché questo è in realtà un comportamento piacevole e desiderato o perché non dovresti averne bisogno comunque. Il mio è per quei testardi che vogliono esercitare il loro diritto di piegare la lingua alla loro volontà, non viceversa.

"Ripareremo" questo comportamento con un decoratore che copierà il valore predefinito invece di riutilizzare la stessa istanza per ogni argomento posizionale lasciato al suo valore predefinito.

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

Ora ridefiniamo la nostra funzione usando questo decoratore:

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

foo() # '[5]'
foo() # '[5]' -- as desired

Ciò è particolarmente accurato per le funzioni che accettano più argomenti. Confrontare:

# 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

con

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

È importante notare che la soluzione di cui sopra si interrompe se si tenta di utilizzare parole chiave args, in questo modo:

foo(a=[4])

Il decoratore potrebbe essere regolato per consentirlo, ma lo lasciamo come esercizio per il lettore;)

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.