8.1 Fraintendimento delle regole di scope in Python
Lo scope in Python si basa su quello che viene chiamato la regola LEGB, che è un acronimo:
-
Local
(nomi assegnati in qualsiasi modo all'interno di una funzione (def
olambda
), e non dichiarati globali in questa funzione); Enclosing
(nomi nell'ambito locale di qualsiasi funzione includente staticamente (def
olambda
), dall'interno all'esterno);Global
(nomi assegnati al livello superiore del file di un modulo, o tramite l'esecuzione della direttivaglobal
all'interno didef
nel file);-
Built-in
(nomi assegnati in precedenza nel modulo dei nomi incorporati:open
,range
,SyntaxError
, e altri).
Sembra abbastanza semplice, giusto?
Tuttavia, ci sono alcune sfumature nel funzionamento in Python, che ci porta a un problema complesso nella programmazione in Python. Consideriamo il seguente esempio:
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
Qual è il problema?
L'errore sopra citato si verifica perché, quando assegni un valore a una variabile nell'ambito
,
Python considera automaticamente come locale
quella variabile per quell'ambito e nasconde qualsiasi variabile con
lo stesso nome in qualsiasi ambito superiore.
Di conseguenza, molti rimangono sorpresi quando ricevono un UnboundLocalError
in un codice che prima funzionava, quando viene
modificato con l'aggiunta di un'istruzione di assegnazione da qualche parte nel corpo della funzione.
Questa caratteristica è particolarmente sconcertante per gli sviluppatori quando si utilizzano liste. Consideriamo il seguente esempio:
lst = [1, 2, 3]
def foo1():
lst.append(5) # Questo funziona normalmente...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... ma questo va in crash!
foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
Perché foo2
va in crash mentre foo1
funziona senza problemi?
La risposta è la stessa dell'esempio precedente, ma comunemente si ritiene che questo caso sia più sottile. foo1
non
applica un'istruzione di assegnazione a lst
, mentre foo2
lo fa. Ricordando che lst += [5]
è semplicemente
un'abbreviazione per lst = lst + [5]
, vediamo che stiamo cercando di assegnare un valore a lst
(quindi Python presume
che sia nell'ambito locale). Tuttavia, il valore che vogliamo assegnare a lst
si basa su
lst
stesso (ancora una volta, ora si presume che sia nell'ambito locale), che non è stato ancora
definito. E otteniamo un errore.
8.2 Modifica di una lista durante l'iterazione su di essa
Il problema nel seguente pezzo di codice dovrebbe essere abbastanza evidente:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
for i in range(len(numbers)):
if odd(numbers[i]):
del numbers[i] # MALE: Cancellazione di un elemento da una lista durante l'iterazione
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
Eliminare un elemento da una lista o array durante l'iterazione su di esso è un problema di Python ben noto a qualsiasi sviluppatore software esperto. Ma, mentre l'esempio sopra potrebbe essere abbastanza evidente, anche sviluppatori esperti possono incappare in questo errore in un codice molto più complesso.
Per fortuna, Python include una serie di paradigmi di programmazione eleganti che, se correttamente utilizzati, possono portare a una significativa semplificazione e ottimizzazione del codice. Un piacevole effetto collaterale è che in un codice più semplice, la probabilità di incappare nell'errore di eliminazione accidentale di un elemento dalla lista durante l'iterazione è notevolmente ridotta.
Uno di questi paradigmi sono le list comprehension. Inoltre, comprendere come funzionano le list comprehension è particolarmente utile per evitare questo problema specifico, come mostrato in questa alternativa del codice sopra, che funziona perfettamente:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # semplicemente filtrando nuovi elementi
print(numbers)
# [0, 2, 4, 6, 8]
Importante!
Qui non si sta assegnando un nuovo oggetto lista. L'uso di
numbers[:]
è un'assegnazione di gruppo di nuovi valori a tutti gli elementi della lista.
8.3 Fraintendimento di come Python associa le variabili nei closure
Consideriamo il seguente esempio:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #Restituisce un elenco di funzioni!
for multiplier in create_multipliers():
print(multiplier(2))
Potresti aspettarti il seguente output:
0
2
4
6
8
Ma in realtà otterrai questo:
8
8
8
8
8
Sorpresa!
Questo avviene a causa del binding ritardato in Python, il che significa che i valori delle variabili utilizzate nei closure vengono cercati al momento della chiamata della funzione interna.
Quindi, nel codice sopra, ogni volta che viene chiamata una delle funzioni restituite, il valore di i
viene
cercato nell'ambito circostante al momento della chiamata (e a quel punto il ciclo è già terminato, quindi a i
è stato già assegnato il valore finale — 4).
La soluzione a questo comune problema in Python sarebbe:
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0
# 2
# 4
# 6
# 8
Voilà! Utilizziamo qui gli argomenti di default per generare funzioni anonime per ottenere il comportamento desiderato. Alcuni lo definirebbero una soluzione elegante. Altri la considerano sottile. Alcuni odiano questo tipo di cose. Ma se sei uno sviluppatore Python, è importante capirlo in ogni caso.
GO TO FULL VERSION