Perché gli argomenti predefiniti vengono valutati al momento della definizione in Python?

StackOverflow https://stackoverflow.com/questions/1651154

  •  22-07-2019
  •  | 
  •  

Domanda

Ho avuto un momento molto difficile nel comprendere la causa principale di un problema in un algoritmo. Quindi, semplificando le funzioni passo dopo passo, ho scoperto che la valutazione degli argomenti predefiniti in Python non si comporta come mi aspettavo.

Il codice è il seguente:

class Node(object):
    def __init__(self, children = []):
        self.children = children

Il problema è che ogni istanza della classe Node condivide lo stesso attributo children , se l'attributo non viene fornito in modo esplicito, come:

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

Non capisco la logica di questa decisione progettuale? Perché i designer di Python hanno deciso che gli argomenti predefiniti devono essere valutati al momento della definizione? Questo mi sembra molto intuitivo.

È stato utile?

Soluzione

L'alternativa sarebbe piuttosto pesante: memorizzare " valori di argomenti predefiniti " nell'oggetto funzione come " thunk " di codice da eseguire più volte ogni volta che la funzione viene chiamata senza un valore specificato per quell'argomento - e renderebbe molto più difficile ottenere l'associazione anticipata (associazione in fase di definizione), che è spesso ciò che si desidera. Ad esempio, in Python così com'è:

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

... scrivere una funzione memorizzata come sopra è un compito abbastanza elementare. Allo stesso modo:

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

... il semplice i = i , basandosi sull'associazione anticipata (tempo di definizione) dei valori arg predefiniti, è un modo banalmente semplice per ottenere l'associazione anticipata. Quindi, la regola attuale è semplice, diretta e ti consente di fare tutto ciò che desideri in un modo estremamente facile da spiegare e comprendere: se vuoi un legame tardivo del valore di un'espressione, valuta quell'espressione nel corpo della funzione; se si desidera l'associazione anticipata, valutarlo come valore predefinito di un arg.

L'alternativa, forzando l'associazione tardiva per entrambe le situazioni, non offrirebbe questa flessibilità e ti costringerebbe a passare attraverso i cerchi (come avvolgere la tua funzione in una fabbrica di chiusura) ogni volta che hai bisogno di un'associazione anticipata, come negli esempi precedenti - ancora più pesante scaldabagno imposto al programmatore da questa ipotetica decisione progettuale (al di là di quelle "invisibili" di generare e valutare ripetutamente thunk dappertutto).

In altre parole, dovrebbe esserci un modo, e preferibilmente solo uno, ovvio per farlo [1] " ;: quando si desidera un legame tardivo, esiste già un modo perfettamente ovvio per raggiungerlo (dal momento che tutto il il codice della funzione viene eseguito solo al momento della chiamata, ovviamente tutto ciò che viene valutato è in ritardo); fare in modo che la valutazione di default di arg produca un'associazione anticipata ti dà un modo ovvio per ottenere anche un'associazione anticipata (un vantaggio! -) piuttosto che dare DUE modi ovvi per ottenere un'associazione tardiva e nessun modo ovvio per ottenere un'associazione anticipata (meno! -).

[1]: " Anche se all'inizio in questo modo potrebbe non essere ovvio a meno che tu non sia olandese. "

Altri suggerimenti

Il problema è questo.

È troppo costoso valutare una funzione come inizializzatore ogni volta che la funzione viene chiamata .

  • 0 è un semplice valore letterale. Valutalo una volta, usalo per sempre.

  • int è una funzione (come la lista) che dovrebbe essere valutata ogni volta che è richiesta come inizializzatore.

Il costrutto [] è letterale, come 0 , che significa " questo oggetto esatto " ;.

Il problema è che alcune persone sperano che significhi list come in " valuta questa funzione per me, per favore, per ottenere l'oggetto che è l'inizializzatore " ;.

Sarebbe un enorme onere aggiungere l'istruzione if necessaria per fare sempre questa valutazione. È meglio prendere tutti gli argomenti come letterali e non fare alcuna valutazione aggiuntiva delle funzioni come parte del tentativo di fare una valutazione delle funzioni.

Inoltre, fondamentalmente, è tecnicamente impossibile implementare le impostazioni predefinite degli argomenti come valutazioni delle funzioni.

Considera, per un momento, l'orrore ricorsivo di questo tipo di circolarità. Diciamo che invece che i valori predefiniti siano letterali, permettiamo loro di essere funzioni che vengono valutate ogni volta che sono richiesti i valori predefiniti di un parametro.

[Questo sarebbe parallelo al modo in cui collections.defaultdict funziona.]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

Qual è il valore di another_func () ? Per ottenere il valore predefinito per b , è necessario valutare aFunc , che richiede una valutazione di another_func . Oops.

Naturalmente nella tua situazione è difficile da capire. Ma devi vedere che la valutazione degli argomenti predefiniti ogni volta comporterebbe un pesante carico di runtime sul sistema.

Inoltre dovresti sapere che in caso di tipi di container questo problema può verificarsi, ma potresti aggirarlo rendendo esplicita la cosa:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

La soluzione alternativa per questo, discussa qui (e molto solido), è:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

Per quanto riguarda il motivo per cui cerchi una risposta da von L & # 246; wis, ma è probabilmente perché la definizione della funzione crea un oggetto codice a causa dell'architettura di Python e potrebbe non esserci una possibilità per lavorare con tipi di riferimento come questo negli argomenti predefiniti.

Ho pensato che anche questo fosse controintuitivo, fino a quando non ho imparato come Python implementa gli argomenti predefiniti.

Una funzione è un oggetto. Al momento del caricamento, Python crea l'oggetto funzione, valuta i valori predefiniti nell'istruzione def , li inserisce in una tupla e aggiunge quella tupla come attributo della funzione denominata func_defaults . Quindi, quando viene chiamata una funzione, se la chiamata non fornisce un valore, Python estrae il valore predefinito da func_defaults .

Ad esempio:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

Quindi tutte le chiamate a f che non forniscono un argomento useranno la stessa istanza di C , perché quello è il valore predefinito.

Per quanto riguarda il motivo per cui Python lo fa in questo modo: beh, quella tupla potrebbe contenere funzioni che verrebbero chiamate ogni volta che era necessario un valore di argomento predefinito. A parte il problema immediatamente evidente delle prestazioni, inizi a entrare in un universo di casi speciali, come la memorizzazione di valori letterali invece di funzioni per tipi non mutabili per evitare chiamate di funzioni non necessarie. E ovviamente ci sono molte implicazioni sulle prestazioni.

Il comportamento reale è davvero semplice. E c'è una soluzione banale, nel caso in cui desideri venga prodotto un valore predefinito da una chiamata di funzione in fase di esecuzione:

def f(x = None):
   if x == None:
      x = g()

Ciò deriva dall'enfasi di python sulla sintassi e sulla semplicità dell'esecuzione. una dichiarazione def si verifica ad un certo punto durante l'esecuzione. Quando l'interprete python raggiunge quel punto, valuta il codice in quella riga, quindi crea un oggetto codice dal corpo della funzione, che verrà eseguito in seguito, quando la chiamate.

È una semplice divisione tra la dichiarazione di funzione e il corpo della funzione. La dichiarazione viene eseguita quando viene raggiunta nel codice. Il corpo viene eseguito al momento della chiamata. Si noti che la dichiarazione viene eseguita ogni volta che viene raggiunta, quindi è possibile creare più funzioni eseguendo il ciclo.

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

Sono state create cinque funzioni separate, con un elenco separato creato ogni volta che è stata eseguita la dichiarazione di funzione. Ad ogni ciclo attraverso funcs , la stessa funzione viene eseguita due volte ad ogni passaggio, usando ogni volta lo stesso elenco. Questo dà i risultati:

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

Altri ti hanno dato la soluzione alternativa, usando param = None, e assegnando un elenco nel corpo se il valore è None, che è un pitone completamente idiomatico. È un po 'brutto, ma la semplicità è potente e la soluzione alternativa non è troppo dolorosa.

Modificato per aggiungere: per ulteriori discussioni al riguardo, consultare l'articolo di effbot qui: http: // effbot.org/zone/default-values.htm e il riferimento linguistico, qui: http://docs.python.org/reference/compound_stmts.html#function

Le definizioni delle funzioni Python sono solo codice, come tutti gli altri codici; non sono " magici " nel modo in cui alcune lingue lo sono. Ad esempio, in Java puoi fare riferimento a " ora " a qualcosa di definito "più tardi":

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

ma in Python

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

Quindi, l'argomento predefinito viene valutato nel momento in cui viene valutata quella riga di codice!

Perché se lo avessero fatto, allora qualcuno avrebbe posto una domanda chiedendo perché non fosse il contrario :-p

Supponi ora di averlo fatto. Come implementeresti l'attuale comportamento se necessario? È facile creare nuovi oggetti all'interno di una funzione, ma non puoi "non creare" " (puoi eliminarli, ma non è lo stesso).

Fornirò un'opinione dissenziente, aggiungendo gli argomenti principali negli altri post.

  

La valutazione degli argomenti predefiniti quando viene eseguita la funzione sarebbe dannosa per le prestazioni.

Lo trovo difficile da credere. Se le assegnazioni di argomenti predefinite come foo = 'some_string' aggiungono davvero un ammontare inaccettabile di costi generali, sono sicuro che sarebbe possibile identificare le assegnazioni a valori letterali immutabili e pre-calcolarle.

  

Se si desidera un'assegnazione predefinita con un oggetto mutabile come foo = [] , basta usare foo = None , seguito da foo = foo o [] nel corpo della funzione.

Anche se questo può essere problematico in singoli casi, come modello di progettazione non è molto elegante. Aggiunge il codice boilerplate e oscura i valori degli argomenti predefiniti. Pattern come foo = foo o ... non funzionano se foo può essere un oggetto come un array intorpidito con un valore di verità indefinito. E in situazioni in cui None è un valore di argomento significativo che può essere passato intenzionalmente, non può essere utilizzato come sentinella e questa soluzione alternativa diventa davvero brutta.

  

Il comportamento corrente è utile per gli oggetti predefiniti mutabili che devono essere condivisi attraverso le chiamate di funzione.

Sarei felice di vedere prove contrarie, ma nella mia esperienza questo caso d'uso è molto meno frequente degli oggetti mutabili che dovrebbero essere creati di nuovo ogni volta che viene chiamata la funzione. A me sembra anche un caso d'uso più avanzato, mentre le assegnazioni accidentali di default con contenitori vuoti sono un gotcha comune per i nuovi programmatori Python. Pertanto, il principio del minimo stupore suggerisce che i valori degli argomenti predefiniti dovrebbero essere valutati quando viene eseguita la funzione.

Inoltre, mi sembra che esista una semplice soluzione per gli oggetti mutabili che dovrebbero essere condivisi tra le chiamate di funzione: inizializzarle al di fuori della funzione.

Quindi direi che questa è stata una cattiva decisione di progettazione. La mia ipotesi è che sia stato scelto perché la sua implementazione è in realtà più semplice e perché ha un caso d'uso valido (anche se limitato). Sfortunatamente, non penso che questo cambierà mai, dal momento che gli sviluppatori Python di base vogliono evitare una ripetizione della quantità di incompatibilità all'indietro introdotta da Python 3.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top