Corso Python / Scope, alias e oggetti modificabili

Lo scope stabilisce dove Python cerca un nome; la mutabilità stabilisce quali cambiamenti sono visibili attraverso nomi che condividono un oggetto. Distinguere una nuova assegnazione da una modifica dell’oggetto aiuta a spiegare perché una lista può cambiare dopo una chiamata.

Separare nomi locali e nomi esterni

Un nome a cui assegni un valore dentro una funzione è normalmente locale, salvo una dichiarazione esplicita diversa. Leggere un valore esterno e riassegnare un nome esterno sono operazioni distinte. Passare i dati necessari come argomenti e restituire i risultati rende più esplicite le dipendenze rispetto all’uso di stato globale nascosto.

Nelle funzioni ordinarie, la ricerca viene spesso descritta con LEGB: Local, Enclosing, Global, Builtins, cioè ambito locale, funzioni esterne, modulo e nomi predefiniti. È una sintesi delle regole per questi casi, non una descrizione universale di ogni contesto speciale di Python. La guida a scope e namespace presenta nomi e oggetti.

global si riferisce al nome nel modulo; nonlocal a un nome già definito in una funzione esterna. Prima di aggiungere una dichiarazione, verifica se ricevere un valore e restituire il nuovo risultato renda più chiaro il flusso. Non serve global per modificare con append una lista già raggiungibile: non stai riassegnando il nome globale.

Osservare liste condivise e copie

Salva il seguente programma completo in UTF-8 come scope.py, dentro una cartella di esercitazione. Esegui python scope.py con l’interprete del progetto. Fuori dall’ambiente virtuale su macOS o Ubuntu usa python3 se è il comando dell’interprete scelto. Non servono pacchetti esterni.

def add_fee(items, fee):
    items.append(fee)

original = [3500, 8000]
alias = original
copy = original.copy()
add_fee(alias, 500)
print(original)
print(copy)

nested = [[1], [2]]
shallow = nested.copy()
shallow[0].append(9)
print(nested)

Il risultato atteso è:

[3500, 8000, 500]
[3500, 8000]
[[1, 9], [2]]

alias = original crea un secondo nome per la stessa lista: add_fee cambia l’oggetto condiviso e original mostra 500 aggiunto in fondo. original.copy() crea invece un’altra lista esterna, preparata prima dell’aggiunta, che conserva soltanto 3500 e 8000.

Nella parte annidata, shallow è una nuova lista esterna ma il suo primo elemento è la stessa lista interna visibile tramite nested. Append aggiunge quindi 9 proprio a quell’oggetto, e anche nested mostra [1, 9].

Confrontare assegnazione, copia superficiale e nomi locali

La copia superficiale rende indipendente il contenitore esterno, non tutti gli oggetti raggiungibili al suo interno. copy.deepcopy() può copiare ricorsivamente molte strutture, ma non è una soluzione universale per file aperti, connessioni e oggetti che devono restare condivisi. Ricostruire soltanto i dati che devono essere indipendenti può esprimere meglio il contratto. La documentazione del modulo copy descrive anche i limiti delle copie profonde.

Una nuova assegnazione come items = items + [fee] cambia il riferimento del nome locale items e non sostituisce la lista del chiamante. Append, invece, cambia l’oggetto a cui items si riferisce. Il parametro è locale, ma può riferirsi a un oggetto condiviso.

Salva questo secondo programma completo come scope_names.py, così conservi anche scope.py, ed esegui python scope_names.py nella stessa cartella:

value = 10
def local_read():
    value = 20
    return value
def broken():
    value += 1
    return value
print(local_read(), value)
try:
    broken()
except UnboundLocalError:
    print("local name not initialized")
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment
counter = make_counter()
print(counter(), counter())

Il risultato atteso è:

20 10
local name not initialized
1 2

Local_read crea un value locale uguale a 20, mentre il value del modulo resta 10. In broken, value += 1 implica un’assegnazione locale, ma prova prima a leggere quel locale non inizializzato: compare UnboundLocalError, intercettato nell’esempio. Increment usa nonlocal per aggiornare il count della funzione esterna, e due chiamate allo stesso contatore producono 1 e 2.

La specifica della risoluzione dei nomi chiarisce perché un’assegnazione nella funzione può rendere locale un nome anche prima della riga che assegna il valore.

Dichiarare se una funzione modifica il suo input

Il chiamante deve poter capire se una funzione modifica la lista ricevuta. Add_fee dell’esempio modifica la lista e, non avendo un return esplicito, restituisce None. Se vuoi conservare l’input, puoi invece costruire e restituire una nuova lista. Controlla sia il risultato sia lo stato dell’input: una sola verifica del valore restituito può non rilevare una modifica inattesa.

  1. In scope.py aggiungi, dopo la prima chiamata, add_fee(copy, 100). Verifica che copy contenga 100 e original non lo riceva.
  2. Ripristina il programma e sostituisci nel corpo della funzione append con items = items + [fee]. La lista originale resta invariata: la nuova lista rimane associata soltanto al parametro locale e non viene restituita.
  3. Ripristina append e rimuovi la chiamata aggiunta. Riesegui entrambi i file e confronta i rispettivi output originali.

La guida ai valori predefiniti delle funzioni mostra un’altra forma di condivisione fra chiamate. Individua separatamente quale nome viene riassegnato, quale oggetto viene modificato e quali altri riferimenti permettono di vedere il cambiamento.