Lezione 14: Argomenti opzionali, scope e funzioni annidate ✅

In questa lezione esploriamo argomenti opzionali e scope in Python, imparando a definire valori di default, usare *args e **kwargs, dichiarare argomenti keyword‑only e comprendere come funzionano variabili locali, globali e lo scope (regola LEGB). Vedrai anche come creare funzioni annidate e closure con nonlocal. Concludiamo con esempi pratici, esercizi, quiz e risorse.


🔗 Ripassi consigliati (interni):
Lezione 2 – Il tuo primo script: Hello, Python!https://www.saperesemplice.com/primo-script-python-hello/
Lezione 3 – Variabili e Tipi di Datohttps://www.saperesemplice.com/variabili-tipi-dato-python/
Lezione 9 – Ciclo for e range()https://www.saperesemplice.com/ciclo-for-range-python/
Lezione 6 – Condizioni con if, else, elifhttps://www.saperesemplice.com/condizioni-if-else-elif-python/


Dettagli lezione

⏱️ Tempo stimato: 45–55 minuti
🧩 Difficoltà: Base → Intermedio
🎯 Obiettivi specifici

  • Definire argomenti opzionali con valori di default in modo sicuro.
  • Usare *args e **kwargs, oltre agli argomenti keyword‑only (dopo *).
  • Comprendere scope e namespace: regola LEGB e comportamento dei nomi.
  • Sapere quando (e come) usare global e nonlocal.
  • Scrivere funzioni annidate e closure per incapsulare stato.
  • Evitare errori comuni (default mutabili, shadowing di nomi, leaky abstractions).

🧱 Argomenti opzionali con valori di default

Gli argomenti opzionali hanno un valore di default nella firma della funzione. Se il chiamante non fornisce l’argomento, viene usato il default.

def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(greet("Alice"))             # Hello, Alice!
print(greet("Bob", greeting="Hi"))  # Hi, Bob!

⚠️ Errore comune da evitare – default mutabili
Non usare liste/dict/set come default: vengono valutati una sola volta, alla definizione della funzione. Tutte le chiamate con quel default condividono lo stesso oggetto.

❌ Sbagliato:

def add_item(x, bucket=[]):
    bucket.append(x)
    return bucket

✔️ Corretto (usa None come sentinella):

def add_item(x, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(x)
    return bucket

Domanda per te: quali valori di default sono sempre sicuri? (Suggerimento: oggetti immutabili come numeri, stringhe, tuple.)


🔁 *args e **kwargs + keyword‑only

  • *args raccoglie argomenti posizionali variabili in una tupla.
  • **kwargs raccoglie argomenti keyword in un dizionario.
  • Gli argomenti keyword‑only si dichiarano dopo un * nella firma: possono essere passati solo per nome.
def report(title, *items, sep=", ", uppercase=False, **meta):
    text = sep.join(str(x) for x in items)
    if uppercase:
        text = text.upper()
    return f"{title}: {text} | meta={meta}"

print(report("Numbers", 1, 2, 3, sep=" - ", uppercase=True, author="Alessio"))
# Numbers: 1 - 2 - 3 | meta={'author': 'Alessio'}

Keyword‑only espliciti

def save(path, *, overwrite=False, encoding="utf-8"):
    # 'overwrite' ed 'encoding' sono keyword‑only
    return f"Saving to {path} with overwrite={overwrite}, encoding={encoding}"

print(save("/tmp/out.txt", overwrite=True))

💡 I keyword‑only migliorano la leggibilità delle chiamate e riducono ambiguità su parametri booleani.

Errore comune da evitare

Passare due volte lo stesso argomento (posizionale e keyword): f(1, x=1)TypeError.


🧭 Scope e namespace: la regola LEGB

Lo scope è la regione testuale in cui un nome è visibile. Python cerca i nomi in questa sequenza:

  1. Local (della funzione corrente),
  2. Enclosing (funzioni esterne annidate),
  3. Global (modulo corrente),
  4. Built‑in (nomi predefiniti come len, print).
x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        return x
    return inner()

print(outer())  # local

🔎 Assegnare a un nome dentro una funzione lo rende, per default, locale a quella funzione. Senza dichiarazioni speciali, le assegnazioni non toccano nomi globali.

Shadowing (ombreggiamento)
Evitare di riusare nomi come list, sum, dir: nascondono i built‑in e creano confusione.


🌍 global e 🧭 nonlocal

  • global name dice al compilatore che tutte le assegnazioni a name nella funzione si riferiscono al nome globale (del modulo).
  • nonlocal name permette di riassegnare un nome nell’enclosing scope (funzione esterna), non globale.
# Esempio global
counter = 0

def tick_global():
    global counter
    counter += 1
    return counter

# Esempio nonlocal

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

c1 = make_counter()
print(c1())  # 1
print(c1())  # 2

⚠️ Attenzione: usa global con parsimonia: rende il codice più difficile da testare. Preferisci ritornare valori o usare closure/oggetti.

Errore comune da evitare

Usare nonlocal x quando non esiste un x nell’enclosing scope → SyntaxError. Definisci prima x nell’outer function.


🧩 Funzioni annidate e closure

Una closure è una funzione che cattura variabili dal suo enclosing scope.

def make_formatter(prefix="[LOG]"):
    def format_message(msg):
        return f"{prefix} {msg}"
    return format_message

log = make_formatter(prefix="[APP]")
print(log("Started"))  # [APP] Started

Con stato modificabile con nonlocal

def make_limiter(limit):
    used = 0
    def use(n=1):
        nonlocal used
        used += n
        return used <= limit
    return use

is_allowed = make_limiter(3)
print(is_allowed())  # True (1)
print(is_allowed())  # True (2)
print(is_allowed())  # True (3)
print(is_allowed())  # False (4)

Domanda per te: in quali casi useresti una closure invece di una variabile globale? Fai un esempio concreto del tuo progetto.


🔶 Esempi pratici guidati

1) API user‑friendly con keyword‑only

def resize(image, *, width=None, height=None, keep_aspect=True):
    """Return a resize plan; demo only."""
    return {
        "width": width,
        "height": height,
        "keep_aspect": keep_aspect,
    }

print(resize("img.jpg", width=800))

2) Somma sicura con *args e default immutabili

def sum_safe(*nums, start=0):
    total = start
    for n in nums:
        total += n
    return total

print(sum_safe(1, 2, 3))           # 6
print(sum_safe(1, 2, 3, start=10)) # 16

3) Router con **kwargs (dispatch per nome)

def route(action, **handlers):
    handler = handlers.get(action, lambda: "Unknown action")
    return handler()

print(route("create", create=lambda: "Created"))

4) Closure contatore formato (con nonlocal)

def make_id(prefix="USR"):
    i = 0
    def next_id():
        nonlocal i
        i += 1
        return f"{prefix}-{i:03d}"
    return next_id

uid = make_id("ORD")
print(uid())  # ORD-001
print(uid())  # ORD-002

💬 Domanda per il lettore: quali parametri renderesti keyword‑only in una tua funzione esistente per migliorarne la leggibilità?


📖 Sezione teorica

  • Default arguments: valutati una sola volta alla definizione → evita mutabili come default; usa None + inizializzazione interna.
  • *args/**kwargs: collezionano argomenti variabili; keyword‑only dopo * migliorano chiarezza.
  • LEGB: ricerca nomi in Local → Enclosing → Global → Built‑in.
  • global riassegna nel modulo; nonlocal riassegna nell’enclosing.
  • Closure: funzioni annidate che catturano variabili; usa nonlocal per aggiornare stato.

📚 Glossario tecnico

  • Scope: regione in cui un nome è visibile e risolvibile.
  • Namespace: insieme di associazioni nome → oggetto (es. globals(), locals()).
  • LEGB: ordine di risoluzione dei nomi.
  • Keyword‑only: parametro che può essere passato solo per nome (def f(a, *, b): ...).
  • Closure: funzione che conserva riferimenti a variabili non locali.

📝 Esercizio finale

Realizza un mini pacchetto di utilità composto da:

  1. make_logger(prefix="[APP]") → ritorna una closure che stampa/ritorna messaggi formattati; usa nonlocal per contare quante volte è stata chiamata (esporre count).
  2. accumulate(*nums, start=0, *, op="sum") → supporta operazioni "sum" e "product"; op è keyword‑only; usa default immutabili.
  3. configure(**options) → accetta qualunque chiave, ma valida che mode (se presente) sia in {"dev","prod"}.

Requisiti:

  • Messaggi di output in inglese.
  • Evita global; usa closure o ritorni di valori.
  • Scrivi una demo che mostri errori comuni (es. default mutabili) e la versione corretta.

Schema d’aiuto:

# Your code here...
logger = make_logger(prefix="[UTIL]")
print(logger("Init"))
print(accumulate(1, 2, 3, start=10))
print(accumulate(2, 3, 4, start=1, op="product"))
print(configure(mode="dev", retries=3))

🧠 Quiz finale

Non guardare le risposte subito! Sono più sotto 👇

  1. Cosa succede ai valori di default in Python?
    A) Sono valutati a ogni chiamata
    B) Sono valutati una volta alla definizione
    C) Dipende dal tipo
  2. Quale firma rende limit keyword‑only?
    A) def f(limit, *args): ...
    B) def f(*args, limit): ...
    C) def f(limit=10): ...
  3. In assenza di global/nonlocal, un’assegnazione dentro una funzione…
    A) Modifica il globale se esiste
    B) Crea/usa un nome locale
    C) Fallisce con NameError
  4. Cosa fa nonlocal?
    A) Crea una variabile globale
    B) Permette di riassegnare un nome nell’enclosing scope
    C) Importa un nome dal modulo
  5. Quale default è sicuro?
    A) []
    B) {}
    C) 0
  6. Quale chiamata è valida data def save(p, *, overwrite=False): ...?
    A) save("a.txt", True)
    B) save("a.txt", overwrite=True)
    C) save(p="a.txt", True)
  7. In una closure, dove viene cercato il nome catturato?
    A) Nel Local della inner function
    B) Nell’Enclosing della outer function
    C) Nel Built‑in
  8. Cosa evita l’uso di *args/**kwargs?
    A) Firme troppo rigide
    B) Il bisogno dei default
    C) La necessità di documentare i parametri

❓ FAQ

1) È meglio usare global o passare valori di ritorno?
Meglio ritornare valori (o usare closure/oggetti). global è una scorciatoia che spesso rende il codice meno testabile.

2) Posso combinare *args e **kwargs con parametri normali?
Sì: l’ordine tipico è posizionali, *args, keyword‑only, **kwargs.

3) Le closure sono lente?
No in modo rilevante per i casi comuni. Vantaggio principale: incapsulazione dello stato.

4) Come documento bene una funzione con molti parametri?
Usa docstring chiare e keyword‑only per i parametri più ambigui; aggiungi esempi d’uso.

5) Posso rendere posizionali‑only alcuni parametri?
Sì (sintassi / nella firma), ma è un tema avanzato: utile per API pulite e performance.



Esterni autorevoli
More on Defining Functions (default, keyword‑only, *args/**kwargs) → https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions
Scopes and Namespaces (LEGB, binding) → https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces
Reference – global e nonlocalhttps://docs.python.org/3/reference/simple_stmts.html
PEP 3102 – Keyword‑Only Argumentshttps://peps.python.org/pep-3102/
PEP 3104 – Access to Names in Outer Scopeshttps://peps.python.org/pep-3104/

SapereSemplice
• Lezione 2 – Primo script Python → https://www.saperesemplice.com/primo-script-python-hello/
• Lezione 3 – Variabili e Tipi di Dato → https://www.saperesemplice.com/variabili-tipi-dato-python/
• Lezione 9 – Ciclo for e range()https://www.saperesemplice.com/ciclo-for-range-python/
• Lezione 6 – Condizioni con if, else, elifhttps://www.saperesemplice.com/condizioni-if-else-elif-python/


🔁 Collegamento alla lezione successiva

➡️ Prosegui alla Lezione 15: Importare moduli in Python
(Il link verrà inserito non appena l’articolo sarà online.)


🗨️ Hai dubbi? Lascia un commento!
📩 Vuoi il PDF della lezione? Scrivici!
🔗 Condividi con un compagno di corso.


🧪 Risposte del quiz

  1. B
  2. B
  3. B
  4. B
  5. C
  6. B
  7. B
  8. A

📅 Aggiornato il 06/09/2025

Lascia un commento