2.16. Ambito

Quando Python cerca un nome all’interno di una funzione, esamina una sequenza specifica di ambiti. Capire questa sequenza spiega perché alcune assegnazioni mascherano i nomi esterni, perché altre li modificano e perché le funzioni annidate possono ricordare i valori del punto in cui sono state definite.

Scatole annidate che mostrano un ambito locale dentro un ambito di modulo dentro l'ambito dei builtin, con una freccia che indica come la ricerca dei nomi procede verso l'esterno a partire dal frame più interno.

La ricerca dei nomi inizia nell’ambito locale della funzione e procede verso l’esterno, attraverso l’ambito di modulo e quello dei builtin, finché non viene trovata una corrispondenza.

2.16.1. Ambito locale e ambito di modulo

I nomi definiti all’interno di una funzione sono locali a quella funzione e scompaiono al termine della chiamata:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

I nomi definiti al livello superiore di un file .py sono a livello di modulo (a volte chiamati globali) e sono visibili ovunque in quel file, anche all’interno delle funzioni:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

La lettura di un nome a livello di modulo dall’interno di una funzione è automatica. L”assegnazione a quel nome dall’interno di una funzione non lo è: l’assegnazione crea una nuova variabile locale che maschera quella a livello di modulo per il resto della chiamata:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

Il counter a sinistra rende counter un nome locale in bump, quindi la lettura a destra non trova alcun valore.

2.16.1.1. La parola chiave global

Per riassegnare davvero un nome a livello di modulo dall’interno di una funzione, dichiaralo prima come global:

counter = 0

def bump():
    global counter
    counter += 1

Ricorri a global con parsimonia. Le funzioni che modificano uno stato nascosto sono più difficili da comprendere rispetto a quelle che ricevono valori come argomenti e restituiscono nuovi valori. La soluzione abituale per «devo condividere uno stato» è passare un oggetto (una lista, un dict, un’istanza di classe) come argomento e modificare quello.

2.16.2. Lambda

Una lambda costruisce una piccola funzione anonima in un’unica espressione:

square = lambda x: x * x
square(7)             # 49

È esattamente equivalente a

def square(x):
    return x * x

Il corpo di una lambda deve essere un’unica espressione: niente istruzioni, niente righe multiple. L’uso principale è passare una funzione minuscola come argomento a qualcosa che accetta una funzione:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

Quando il corpo cresce oltre una singola espressione, passa a un vero def. Dare un nome a una funzione con def le assegna anche un nome nei traceback, cosa che una lambda non ha.

2.16.3. Closure

Una funzione definita all’interno di un’altra funzione può leggere i nomi dall’ambito della funzione che la racchiude. La funzione interna cattura quei nomi e continua a funzionare anche dopo che la chiamata esterna è terminata:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

Output:

105 110

add5 e add10 sono due funzioni distinte, ciascuna delle quali ricorda il proprio n. Una funzione che costruisce e restituisce in questo modo una funzione interna personalizzata si chiama closure. È il motivo principale per cui un linguaggio ha bisogno delle funzioni annidate: un modo per incorporare uno stato in un valore-funzione e poi consegnare quel valore come un unico callable.

La lettura dei nomi catturati avviene automaticamente. La riassegnazione di uno di essi richiede una parola chiave aggiuntiva. L’esempio seguente fa ciò che sembra corretto, e fallisce:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

L’assegnazione a count all’interno di tick rende count locale a tick, esattamente come lo avrebbe reso locale in una funzione di livello superiore. La parola chiave nonlocal dice a Python «questo nome risiede nella funzione che racchiude, riassegnalo lì»:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

Output:

1 2 3

nonlocal sta all’ambito della funzione che racchiude come global sta all’ambito di modulo. Nota che la modifica di un oggetto catturato (chiamando some_list.append(...), some_dict[k] = v) non richiede nonlocal: il nome non viene riassegnato, viene modificato solo l’oggetto a cui punta.