Scope e predefiniti mutabili
Dove un nome vive e chi può vederlo, la differenza tra leggere e scrivere un nome esterno, UnboundLocalError, e la trappola più famosa di Python — la lista vuota come predefinito.
Come vuoi imparare?
Il problema che stiamo risolvendo
L'elenco "quando si rompe" del capitolo diciannove ha lasciato una riga in sospeso: "Una variabile dentro la funzione non è visibile fuori — è normale, e perché è l'argomento del capitolo ventuno."
Questo è quel capitolo.
def add_tax(price):
tax = price * 0.15
return price + tax
print(add_tax(100))
print(tax)115.0
NameError: name 'tax' is not defined. Did you mean: 'max'?La prima riga ha funzionato, quindi tax doveva esistere. E fuori, non esiste. (Prima della riga dell'errore Python stampa anche alcune righe di traceback; in questo capitolo viene mostrata solo l'ultima.)
Questa è una protezione, non una limitazione. In un programma con cinquanta funzioni, se ogni nome interno fuoriuscisse all'esterno, le variabili total di due funzioni si sovrascriverebbero a vicenda — e nessuno se ne accorgerebbe. I nomi dentro una funzione restano dentro, così scriverne una non richiede di conoscere i nomi delle altre quarantanove.
Questo capitolo tratta quella regola, e una sua conseguenza che è la trappola più famosa di Python.
Alla fine di questo capitolo sarai in grado di
- Dire dove un nome "vive" e chi può vederlo
- Leggere un nome esterno da dentro una funzione, e dire perché non puoi scriverne uno
- Leggere un
UnboundLocalErrore dire cosa è successo - Spiegare perché una lista passata a una funzione può tornare cambiata
- Riconoscere ed evitare la trappola più famosa di Python: il predefinito mutabile
Prerequisiti: Argomenti di funzione.
Dentro resta dentro
Ogni nome creato dentro una funzione — parametri inclusi — vive solo per quella chiamata. Quando la funzione finisce, i nomi spariscono.
Questo si chiama scope (ambito): la regione in cui un nome è noto.
Leggere un nome esterno funziona
RATE = 0.15
def add_tax(price):
return price + price * RATE
print(add_tax(100))115.0Non c'è RATE dentro la funzione, quindi Python guarda fuori e ne trova uno. Leggere un nome esterno da dentro va bene.
Il nome in maiuscolo è una convenzione, non una regola di Python — dice a chi legge "questa è una costante, non cambia". Usato in quel modo, un nome esterno è al suo più sicuro.
Ma scriverne uno non funziona
count = 10
def bump():
count = 99
return count
print(bump())
print(count)99
10L'interno count = 99 non ha mai toccato l'esterno count. Ha creato un nuovo, locale nome che vive solo dentro la funzione e ha nascosto quello esterno per la durata.
Questa è la regola, ed è deliberata: una funzione non può cambiare qualcosa fuori per caso.
E da essa nasce un errore dall'aspetto strano:
count = 10
def bump():
count = count + 1
return count
print(bump())UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueLa riga sembra leggere l'esterno count e aggiungere uno. Ma Python ispeziona l'intera funzione prima di eseguirla, vede che qualcosa assegna a count, e decide che count è locale a questa funzione dall'inizio alla fine.
Poi, valutando count + 1 a destra, cerca il locale count, che non ha ancora ricevuto un valore — e si ferma.
Il messaggio è quindi insolitamente accurato: il nome è locale, e nulla gli è stato ancora associato.
La correzione è quasi sempre la stessa, e non è global: prendi il valore come argomento e return il risultato.
def bump(count):
return count + 1
count = bump(count)Python ha una parola chiave global, e questo corso non la insegna, perché il problema per cui la gente la cerca ha una risposta migliore. Una funzione che cambia lo stato esterno non può essere capita dal punto in cui è chiamata — e questa è la definizione di codice difficile da testare e difficile da fidarsi.
Ma la cosa che hai passato può cambiare
Lo scope protegge i nomi, non gli oggetti. La distinzione è sottile e importante.
def add_item(items):
items.append("pen")
return items
basket = []
add_item(basket)
add_item(basket)
print(basket)['pen', 'pen']L'esterno basket è cambiato, anche se nessun return è stato tenuto.
La ragione è di nuovo il capitolo dodici: il nome items è nuovo, ma punta alla stessa lista. append modifica quella lista, e entrambi i nomi mostrano il cambiamento.
Confronta:
def replace(items):
items = ["new"]
return items
basket = ["old"]
result = replace(basket)
print(basket)
print(result)['old']
['new']Qui items = [...] ha assegnato un nuovo valore, puntando il nome locale a una lista diversa. L'esterno basket è dov'era.
La regola in una riga: modificare l'oggetto (append, sort, [i] =) è visibile fuori; puntare il nome a un altro oggetto (=) non lo è.
La trappola più famosa di Python
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))
print(add_item("ink"))['pen']
['pen', 'bag']
['pen', 'bag', 'ink']Tre chiamate separate, e ognuna conserva ciò che le precedenti hanno lasciato.
Capisci la ragione e non la dimenticherai mai: il valore predefinito è valutato una volta, quando la riga def viene eseguita — non quando la funzione è chiamata. Quella lista vuota è una singola lista attaccata alla funzione, e ogni chiamata la condivide.
La correzione è convenzionale e semplice:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
print(add_item("pen"))
print(add_item("bag"))['pen']
['bag']None è immutabile, quindi condividerlo non costa nulla, e la nuova lista è creata fresca a ogni chiamata.
Come regola: non scrivere mai una lista, dizionario o insieme come valore predefinito. Numeri, testo, True/False e None sono sicuri, proprio perché non possono essere cambiati.
Perchéis Nonee non== None?ischiede "è proprio lo stesso oggetto?", e c'è un soloNonein tutto il programma.==testa l'uguaglianza, che alcuni tipi definiscono in modi sorprendenti. PerNone,isè sia convenzionale che affidabile.
Un esempio completo
basket.py:
# Where a name lives decides who can see it and who can change it
TAX_RATE = 0.15
def line_total(price, quantity):
"""Pure: takes values, returns a value, touches nothing outside."""
return round(price * quantity * (1 + TAX_RATE), 2)
def add_line(basket, name, price, quantity):
"""Impure on purpose: it changes the basket it was handed."""
basket.append({"name": name, "total": line_total(price, quantity)})
return basket
def summarise(basket, note=None):
"""`None` as the default, so no list or dict is shared between calls."""
notes = [] if note is None else [note]
total = sum(line["total"] for line in basket)
notes.append(f"{len(basket)} lines, {total:.2f} in total")
return notes
shopping = []
add_line(shopping, "pen", 15.0, 3)
add_line(shopping, "bag", 850.0, 1)
for line in shopping:
print(f"{line['name']:<6} {line['total']:>8.2f}")
print()
for note in summarise(shopping, "morning order"):
print(note)
print()
print("basket outside the function:", len(shopping), "lines")pen 51.75
bag 977.50
morning order
2 lines, 1029.25 in total
basket outside the function: 2 linesTre funzioni con tre relazioni diverse col mondo fuori di loro, ed è quello su cui guardare.
line_total non tocca nulla. Prende valori e restituisce un valore. Legge TAX_RATE e non la cambia mai. Una funzione così si chiama pura — lo stesso input dà sempre lo stesso output, senza effetti collaterali da nessuna parte. È il tipo più facile da testare e il più difficile da sbagliare.
add_line cambia qualcosa fuori apposta. Le è passata una lista e vi aggiunge — e l'ultima riga prova che l'esterno shopping contiene davvero due righe. Non c'è nulla di male, finché il nome lo dice. Leggendo add_line sai che qualcosa sarà aggiunto.
summarise usa None come suo predefinito. Scritta note=[], ogni chiamata condividerebbe una lista, e il secondo rapporto porterebbe la nota del primo.
Quando si rompe
NameError: name 'tax' is not defined Un nome da dentro una funzione è stato usato fuori. return tutto ciò che serve fuori.
UnboundLocalError: cannot access local variable ... where it is not associated with a value Un nome è assegnato dentro la funzione e letto prima di quell'assegnazione. Python ha deciso che era locale. Prendi il valore come argomento e return il risultato.
La mia lista è cambiata dopo averla passata a una funzione È il comportamento previsto — il nome è nuovo, la lista è la stessa. Per evitarlo, scrivi items = items.copy() all'inizio della funzione, o costruisci e return una nuova lista.
La funzione ricorda i suoi risultati precedenti Un predefinito mutabile. Non def f(x, acc=[]) ma def f(x, acc=None), con if acc is None: acc = [] dentro.
Ho usato lo stesso nome in due funzioni — è un problema? Non lo è, ed è proprio lo scopo dello scope. Le variabili total di due funzioni sono completamente separate.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Un valore è assegnato a count dentro la funzione. Cosa stampano le due righe?
count = 10
def bump():
count = 99
return count
print(bump())
print(count)- A10 10
- B99 10
- CUn `UnboundLocalError`
- D99 99
Non c'è return, eppure la lista esterna è stampata. Cosa appare?
def add_item(items):
items.append("pen")
basket = []
add_item(basket)
add_item(basket)
print(basket)- A[]
- B['pen']
- CNone
- D['pen', 'pen']
Due chiamate separate, con una lista vuota come predefinito. Cosa viene stampato?
def collect(item, box=[]):
box.append(item)
return box
print(collect("pen"))
print(collect("bag"))- A['pen', 'bag'] ['pen', 'bag']
- B['pen'] ['bag']
- C['pen'] ['pen', 'bag']
- D['pen'] ['pen']
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Scrivi un file chiamato cart.py con tre funzioni:
line_total(price, quantity, tax_rate=0.15)— pura, non tocca nullaadd_to(cart, name, price, quantity)— aggiunge al cart che le è passatoreceipt(cart, header=None)— restituisce una lista di righe, inventando un header quando nessuno è dato
Poi costruisci un cart vuoto, aggiungi alcune cose, e stampa la ricevuta.
Infine, tre esperimenti, tutti che vale la pena vedere con i propri occhi:
- Cambia il predefinito di
receiptinheader=[], poi chiama la funzione due volte e stampa entrambi i risultati. Cosa vedi la seconda volta? - Aggiungi
cart = cart.copy()come prima riga diadd_to, poi stampa il cart esterno. Quante cose ci sono, e perché? - Assegna
TAX_RATE = 0.2dentro una funzione, poi stampaTAX_RATEfuori. Quale valore vedi?
Le tre risposte insieme sono tutto il capitolo: quali nomi vivono dove, e quali cambiamenti raggiungono l'esterno.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz