4. Altri Strumenti di Controllo del Flusso

Oltre all’istruzione while appena introdotta, Python ne utilizza altre che incontreremo in questo capitolo.

4.1. Istruzioni if

Forse il tipo di istruzione più conosciuto è l’istruzione if. Ad esempio:

>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
...     x = 0
...     print('Negative changed to zero')
... elif x == 0:
...     print('Zero')
... elif x == 1:
...     print('Single')
... else:
...     print('More')
...
More

Possiamo avere zero o più parti elif, e la parte else è facoltativa. La parola chiave “elif” è l’abbreviazione di “else if”, ed è utile per evitare un’eccessiva indentazione. Una sequenza ifelifelif … è un sostituto per le istruzioni switch o case trovate in altri linguaggi.

Se stai confrontando lo stesso valore con diverse costanti, o controllando tipi o attributi specifici, potresti trovare utile l’istruzione match. Per maggiori dettagli, consulta Istruzioni match.

4.2. Istruzioni for

L’istruzione for in Python differisce leggermente da ciò a cui potresti essere abitualmente abituato in C o Pascal. Piuttosto che iterare sempre su una progressione aritmetica di numeri (come in Pascal), o dare all’utente la possibilità di definire sia il passo di iterazione che la condizione di arresto (come in C), l’istruzione for di Python itera sugli elementi di qualsiasi sequenza (una lista o una stringa), nell’ordine in cui appaiono nella sequenza. Ad esempio:

>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

Il codice che modifica una collezione durante l’iterazione sulla stessa può essere difficile da scrivere correttamente. Invece, è generalmente più semplice iterare su una copia della collezione o crearne una nuova:

# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Strategy:  Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

4.3. La Funzione range()

Se hai bisogno di iterare su una sequenza di numeri, la funzione integrata range() è utile. Genera progressioni aritmetiche:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

Il valore finale fornito non fa parte della sequenza generata; range(10) genera 10 valori, gli indici legali per gli elementi di una sequenza di lunghezza 10. È possibile far partire l’intervallo da un altro numero, o specificare un incremento diverso (anche negativo; talvolta questo è chiamato “passo”):

>>> list(range(5, 10))
[5, 6, 7, 8, 9]

>>> list(range(0, 10, 3))
[0, 3, 6, 9]

>>> list(range(-10, -100, -30))
[-10, -40, -70]

Per iterare sugli indici di una sequenza, puoi combinare range() e len() nel seguente modo:

>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

Nella maggior parte di questi casi, però, è conveniente utilizzare la funzione enumerate(), vedi Tecniche di Looping.

Attenzione: succede una cosa strana se provi a stampare un range:

>>> range(10)
range(0, 10)

In molti casi, l’oggetto restituito da range() si comporta come se fosse una lista, ma in realtà non lo è. È un oggetto che restituisce gli elementi successivi della sequenza desiderata quando ci si itera sopra, ma non crea effettivamente la lista, risparmiando così spazio.

Diciamo che un tale oggetto è un iterable, cioè adatto per funzioni e costrutti che si aspettano qualcosa da cui possono ottenere elementi fino a quando il contenitore è vuoto. Abbiamo visto che l’istruzione for è un tale costrutto, mentre un esempio di una funzione che prende come argomento un oggetto iterabile è sum():

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

In seguito vedremo altre funzioni che restituiscono oggetti iterabili e prendono oggetti iterabili come argomenti. Nel capitolo Strutture Dati, discuteremo in modo più dettagliato riguardo a list().

4.4. Istruzioni break e continue, e clausole else nei cicli

L’istruzione break esce dal ciclo for o while più interno.

Un ciclo for o while può includere una clausola else.

In un ciclo for, la clausola else viene eseguita dopo che il ciclo ha raggiunto la sua iterazione finale.

In un ciclo while, viene eseguita dopo che la condizione del ciclo diventa falsa.

In entrambi i tipi di ciclo, la clausola else non viene eseguita se il ciclo è stato terminato da un break.

Questo è esemplificato nel seguente ciclo for, che cerca numeri primi:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # loop fell through without finding a factor
...         print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3

(Sì, questo codice è corretto. Guarda attentamente: la clausola else appartiene al ciclo for, non all’istruzione if.)

Quando viene utilizzata con un ciclo, la clausola else ha più in comune con la clausola else di un’istruzione try rispetto a quella delle istruzioni if: la clausola else di un’istruzione try viene eseguita quando non si verifica alcuna eccezione, e la clausola else di un ciclo viene eseguita quando non si verifica alcun break. Per ulteriori informazioni sull’istruzione try e sulle eccezioni, consulta Gestione delle Eccezioni.

L’istruzione continue, presa in prestito dal C, continua con la prossima iterazione del ciclo:

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print("Found an even number", num)
...         continue
...     print("Found an odd number", num)
...
Found an even number 2
Found an odd number 3
Found an even number 4
Found an odd number 5
Found an even number 6
Found an odd number 7
Found an even number 8
Found an odd number 9

4.5. Istruzioni pass

L’istruzione pass non fa nulla. Può essere utilizzata quando è richiesta un’istruzione sintatticamente, ma il programma non richiede alcuna azione. Per esempio:

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

Questo è comunemente usato per creare classi minime:

>>> class MyEmptyClass:
...     pass
...

Un altro posto in cui pass può essere usato è come segnaposto per il corpo di una funzione o condizionale quando si sta lavorando su nuovo codice, permettendo di continuare a pensare a un livello più astratto. L’istruzione pass viene ignorata silenziosamente:

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

4.6. Istruzioni match

Un’istruzione match prende un’espressione e confronta il suo valore con schemi successivi forniti come uno o più blocchi case. Questo è superficialmente simile a un’istruzione switch in C, Java o JavaScript (e molti altri linguaggi), ma è più simile al pattern matching in linguaggi come Rust o Haskell. Solo il primo schema che corrisponde viene eseguito e può anche estrarre componenti (elementi di sequenze o attributi di oggetti) dal valore in variabili.

La forma più semplice confronta un valore con uno o più letterali:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

Nota l’ultimo blocco: il «nome della variabile» _ funge da wildcard e non fallisce mai. Se nessun case corrisponde, nessuno dei rami viene eseguito.

Puoi combinare diversi letterali in un singolo schema usando | («or»):

case 401 | 403 | 404:
    return "Not allowed"

Gli schemi possono sembrare assegnazioni di decomposizione e possono essere utilizzati per associare variabili:

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

Studialo attentamente! Il primo schema ha due letterali, e può essere pensato come un’estensione dello schema letterale mostrato sopra. Ma i successivi due schemi combinano un letterale e una variabile, e la variabile associa un valore dal soggetto (point). Il quarto schema cattura due valori, che lo rende concettualmente simile all’assegnazione con unpacking (x, y) = point.

Se stai utilizzando classi per strutturare i tuoi dati, puoi usare il nome della classe seguito da un elenco di argomenti che assomiglia a un costruttore, ma con la capacità di catturare attributi in variabili:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

Puoi utilizzare parametri posizionali con alcune classi integrate che forniscono un ordinamento per i loro attributi (ad esempio le dataclassi). Puoi anche definire una posizione specifica per gli attributi negli schemi impostando l’attributo speciale __match_args__ nelle tue classi. Se è impostato su («x», «y»), gli schemi seguenti sono tutti equivalenti (e tutti associano l’attributo y alla variabile var):

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

Un modo consigliato per leggere gli schemi è considerarli come una forma estesa di quello che metteresti a sinistra di un’assegnazione, per capire quali variabili verrebbero impostate su cosa. Solo i nomi autonomi (come var qui sopra) vengono assegnati da un’istruzione di match. I nomi puntati (come foo.bar), i nomi degli attributi (il x= e y= qui sopra) o i nomi delle classi (riconosciuti dalle «(…)» accanto a loro come Point qui sopra) non vengono mai assegnati.

Gli schemi possono essere arbitrariamente nidificati. Ad esempio, se abbiamo una breve lista di Punti, con __match_args__ aggiunto, potremmo abbinarla così:

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

Possiamo aggiungere una clausola if a uno schema, nota come «guardia». Se la guardia è falsa, match passa a provare il blocco case successivo. Nota che la cattura del valore avviene prima che la guardia venga valutata:

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

Altre caratteristiche chiave di questa istruzione:

  • Come le assegnazioni di decomposizione, gli schemi di tuple e liste hanno esattamente lo stesso significato e corrispondono effettivamente a sequenze arbitrarie. Un’importante eccezione è che non corrispondano a iteratori o stringhe.

  • Le sequenze di schemi supportano l’unpacking esteso: [x, y, *rest] e (x, y, *rest) funzionano in modo simile alle assegnazioni di decomposizione. Il nome dopo * può anche essere _, quindi (x, y, *_) corrisponde a una sequenza di almeno due elementi senza la necessità di associare gli elementi rimanenti.

  • Schemi di mapping: {"bandwidth": b, "latency": l} cattura i valori "bandwidth" e "latency" da un dizionario. A differenza degli schemi di sequenza, le chiavi aggiuntive vengono ignorate. È supportato anche un unpacking come **rest. (Ma **_ sarebbe ridondante, quindi non è possibile utilizzarlo.)

  • I sotto-schemi possono essere catturati utilizzando la parola chiave as:

    case (Point(x1, y1), Point(x2, y2) as p2): ...
    

    acquisirà il secondo elemento dell’input come p2 (a patto che l’input sia una sequenza di due punti)

  • La maggior parte dei letterali sono confrontati per uguaglianza, tuttavia i singleton True, False e None sono confrontati per identità.

  • Gli schemi possono utilizzare costanti nominate. Queste devono essere nomi puntati per impedire che vengano interpretati come variabili di cattura:

    from enum import Enum
    class Color(Enum):
        RED = 'red'
        GREEN = 'green'
        BLUE = 'blue'
    
    color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
    
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")
    

Per una spiegazione più dettagliata e ulteriori esempi, puoi consultare PEP 636 che è scritta in un formato simile a un tutorial.

4.7. Definizione di Funzioni

Possiamo creare una funzione che scrive la serie di Fibonacci fino a un limite arbitrario:

>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

La parola chiave def introduce una definizione di funzione. Deve essere seguita dal nome della funzione e dall’elenco tra parentesi dei parametri formali. Le istruzioni che formano il corpo della funzione iniziano alla riga successiva, e devono essere indentate.

La prima istruzione del corpo della funzione può opzionalmente essere una stringa letterale; questa stringa letterale è la stringa di documentazione della funzione, o docstring. (Maggiori informazioni sulle docstring possono essere trovate nella sezione Stringhe di Documentazione.) Ci sono strumenti che utilizzano le docstring per produrre automaticamente documentazione online o stampata, o per consentire all’utente di navigare interattivamente attraverso il codice; è una buona pratica includere le docstring nel codice che scrivi, quindi prendilo come abitudine.

L”esecuzione di una funzione introduce una nuova tabella dei simboli utilizzata per le variabili locali della funzione. Più precisamente, tutte le assegnazioni di variabili in una funzione memorizzano il valore nella tabella dei simboli locali; mentre i riferimenti alle variabili cercano prima nella tabella dei simboli locali, poi nelle tabelle dei simboli locali delle funzioni circostanti, poi nella tabella dei simboli globali, e infine nella tabella dei nomi incorporati. Pertanto, le variabili globali e le variabili delle funzioni circostanti non possono essere direttamente assegnate un valore all’interno di una funzione (a meno che, per le variabili globali, siano nominate in un’istruzione global, o, per le variabili delle funzioni circostanti, siano nominate in un’istruzione nonlocal), anche se possono essere referenziate.

I parametri effettivi (argomenti) di una chiamata di funzione vengono introdotti nella tabella dei simboli locali della funzione chiamata quando viene chiamata; quindi, gli argomenti sono passati utilizzando il passaggio per valore (dove il valore è sempre un riferimento all’oggetto, non il valore dell’oggetto). [1] Quando una funzione chiama un’altra funzione, o si chiama ricorsivamente, viene creata una nuova tabella dei simboli locali per quella chiamata.

Una definizione di funzione associa il nome della funzione all’oggetto funzione nella tabella dei simboli corrente. L’interprete riconosce l’oggetto puntato da quel nome come una funzione definita dall’utente. Altri nomi possono anche puntare a quello stesso oggetto funzione e possono anche essere utilizzati per accedere alla funzione:

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

Provenendo da altri linguaggi, potresti obiettare che fib non è una funzione ma una procedura poiché non restituisce un valore. Infatti, anche le funzioni senza un’istruzione return restituiscono un valore, sebbene piuttosto noioso. Questo valore si chiama None (è un nome riservato del linguaggio). Scrivere il valore None è normalmente soppresso dall’interprete se sarebbe l’unico valore scritto. Puoi vederlo se lo desideri davvero utilizzando print():

>>> fib(0)
>>> print(fib(0))
None

È semplice scrivere una funzione che restituisce una lista dei numeri della serie di Fibonacci, invece di stamparla:

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Questo esempio, come al solito, dimostra alcune nuove funzionalità di Python:

  • L’istruzione return restituisce un valore da una funzione. return senza un argomento espressione restituisce None. Anche uscire dalla fine di una funzione restituisce None.

  • L’istruzione result.append(a) chiama un metodo dell’oggetto lista result. Un metodo è una funzione che “appartiene” a un oggetto e si chiama obj.methodname, dove obj è un qualche oggetto (questo può essere un’espressione), e methodname è il nome di un metodo definito dal tipo dell’oggetto. Diversi tipi definiscono metodi diversi. Metodi di tipi diversi possono avere lo stesso nome senza causare ambiguità. (È possibile definire i propri tipi di oggetto e metodi, utilizzando classi, vedi Classi) Il metodo append() mostrato nell’esempio è definito per gli oggetti lista; aggiunge un nuovo elemento alla fine della lista. In questo esempio è equivalente a result = result + [a], ma più efficiente.

4.8. Ulteriori informazioni sulla Definizione di Funzioni

È anche possibile definire funzioni con un numero variabile di argomenti. Esistono tre forme, che possono essere combinate.

4.8.1. Valori predefiniti degli argomenti

La forma più utile è specificare un valore predefinito per uno o più argomenti. Questo crea una funzione che può essere chiamata con meno argomenti di quelli che è definita per consentire. Ad esempio:

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

Questa funzione può essere chiamata in diversi modi:

  • dando solo l’argomento obbligatorio: ask_ok('Vuoi davvero uscire?')

  • dando uno degli argomenti opzionali: ask_ok('OK sovrascrivere il file?', 2)

  • o addirittura dando tutti gli argomenti: ask_ok('OK sovrascrivere il file?', 2, 'Dai su, solo o no!')

Questo esempio introduce anche la parola chiave in. Questo testa se una sequenza contiene o meno un certo valore.

I valori predefiniti vengono valutati nel punto di definizione della funzione nello scope di definizione, in modo che

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

stamperà 5.

Avvertenza importante: Il valore predefinito viene valutato solo una volta. Questo fa la differenza quando il valore predefinito è un oggetto mutabile come una lista, un dizionario, o istanze della maggior parte delle classi. Ad esempio, la seguente funzione accumula gli argomenti passati ad essa nelle chiamate successive:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

Questo stamperà

[1]
[1, 2]
[1, 2, 3]

Se non vuoi che il valore predefinito sia condiviso tra le chiamate successive, puoi scrivere la funzione in questo modo:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

4.8.2. Argomenti chiave-valore

Le funzioni possono anche essere chiamate utilizzando argomenti chiave-valore della forma kwarg=value. Ad esempio, la seguente funzione:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

accetta un argomento obbligatorio (voltage) e tre argomenti opzionali (state, action, e type). Questa funzione può essere chiamata in uno dei seguenti modi:

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

ma tutte le chiamate seguenti non sarebbero valide:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

In una chiamata di funzione, gli argomenti chiave-valore devono seguire gli argomenti posizionali. Tutti gli argomenti chiave passati devono corrispondere a uno degli argomenti accettati dalla funzione (ad esempio, actor non è un argomento valido per la funzione parrot), e il loro ordine non è importante. Questo include anche gli argomenti non opzionali (ad esempio, parrot(voltage=1000) è valido). Nessun argomento può ricevere un valore più di una volta. Ecco un esempio che fallisce a causa di questa restrizione:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for argument 'a'

Quando è presente un parametro formale finale della forma **name, riceve un dizionario (vedi Tipi di Mapping — dict) contenente tutti gli argomenti chiave tranne quelli corrispondenti a un parametro formale. Questo può essere combinato con un parametro formale della forma *name (descritto nella prossima sottosezione) che riceve una tupla contenente gli argomenti posizionali oltre all’elenco dei parametri formali. (*name deve occorrere prima di **name.) Ad esempio, se definiamo una funzione in questo modo:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

Potrebbe essere chiamato in questo modo:

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

e naturalmente stamperà:

-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch

Nota che l’ordine in cui gli argomenti chiave-valore vengono stampati è garantito corrispondere all’ordine in cui sono stati forniti nella chiamata della funzione.

4.8.3. Parametri speciali

Per impostazione predefinita, gli argomenti possono essere passati a una funzione Python sia per posizione che esplicitamente per chiave. Per leggibilità e prestazioni, ha senso limitare il modo in cui gli argomenti possono essere passati in modo che uno sviluppatore debba solo guardare la definizione della funzione per determinare se gli elementi sono passati per posizione, per posizione o per chiave, o per chiave.

Ecco come potrebbe essere la definizione di una funzione:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

dove / e * sono opzionali. Se utilizzati, questi simboli indicano il tipo di parametro in base a come gli argomenti possono essere passati alla funzione: solo per posizione, per posizione o per chiave, e solo per chiave. I parametri chiave-valore sono anche chiamati parametri nominali.

4.8.3.1. Argomenti Posizionali o per Chiave

Se / e * non sono presenti nella definizione della funzione, gli argomenti possono essere passati a una funzione per posizione o per chiave.

4.8.3.2. Parametri Solo per Posizione

Guardando questo in modo più dettagliato, è possibile contrassegnare certi parametri come solo per posizione. Se lo sono, l’ordine dei parametri è importante, e i parametri non possono essere passati per chiave. I parametri solo per posizione sono posizionati prima di un / (barra obliqua). Il / è utilizzato per separare logicamente i parametri posizionali dal resto dei parametri. Se non c’è un / nella definizione della funzione, non ci sono parametri posizionali.

I parametri che seguono il / possono essere posizionali o per chiave o solo per chiave.

4.8.3.3. Argomenti Solo per Chiave

Per contrassegnare i parametri come solo per chiave, indicando che i parametri devono essere passati per argomento chiave, posiziona un * nell’elenco degli argomenti subito prima del primo parametro solo per chiave.

4.8.3.4. Esempi di Funzioni

Considera le seguenti definizioni di funzioni di esempio prestando particolare attenzione ai marcatori / e *:

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

Nella prima definizione di funzione, standard_arg, la forma più familiare, non pone restrizioni sulla convenzione di chiamata e gli argomenti possono essere passati per posizione o per chiave:

>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

La seconda funzione pos_only_arg è limitata a utilizzare solo parametri posizionali poiché c’è un / nella definizione della funzione:

>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

La terza funzione kwd_only_args consente solo argomenti chiave-valore come indicato da un * nella definizione della funzione:

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3

E l’ultimo utilizza tutte e tre le convenzioni di chiamata nella stessa definizione di funzione:

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

Infine, considera questa definizione di funzione che ha un potenziale conflitto tra l’argomento posizionale name e **kwds che ha name come chiave:

def foo(name, **kwds):
    return 'name' in kwds

Non esiste una chiamata possibile che lo farà restituire True poiché la chiave 'name' si legherà sempre al primo parametro. Ad esempio:

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

Ma utilizzando / (argomenti solo per posizione), è possibile poiché consente name come argomento posizionale e 'name' come chiave negli argomenti chiave-valore:

>>> def foo(name, /, **kwds):
...     return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True

In altre parole, i nomi dei parametri solo per posizione possono essere utilizzati in **kwds senza ambiguità.

4.8.3.5. Riepilogo

Il caso d’uso determinerà quali parametri utilizzare nella definizione della funzione:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

Come guida:

  • Usa solo argomenti per posizione se vuoi che il nome dei parametri non sia disponibile per l’utente. Questo è utile quando i nomi dei parametri non hanno un vero significato, se vuoi imporre l’ordine degli argomenti quando la funzione viene chiamata o se devi prendere alcuni parametri posizionali e argomenti chiave arbitrari.

  • Usa solo argomenti per chiave quando i nomi hanno significato e la definizione della funzione è più comprensibile essendo esplicita con i nomi o vuoi impedire agli utenti di fare affidamento sulla posizione dell’argomento passato.

  • Per un’API, usa solo per posizione per evitare rotture delle modifiche API se il nome del parametro viene modificato in futuro.

4.8.4. Liste di Argomenti Arbitrarie

Infine, l’opzione meno utilizzata è specificare che una funzione può essere chiamata con un numero arbitrario di argomenti. Questi argomenti saranno racchiusi in una tupla (vedi Tuple e Sequenze). Prima del numero variabile di argomenti, possono verificarsi zero o più argomenti normali.

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

Normalmente, questi argomenti variadici saranno gli ultimi nell’elenco dei parametri formali, perché raccolgono tutti gli argomenti di input rimanenti che vengono passati alla funzione. Qualsiasi parametro formale che si verifica dopo il parametro *args sono argomenti “solo per chiave”, il che significa che possono essere utilizzati solo come argomenti chiave piuttosto che argomenti posizionali.

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

4.8.5. Spacchettamento delle Liste di Argomenti

La situazione inversa si verifica quando gli argomenti sono già in una lista o tupla ma devono essere spacchettati per una chiamata di funzione che richiede argomenti posizionali separati. Ad esempio, la funzione built-in range() si aspetta argomenti start e stop separati. Se non sono disponibili separatamente, scrivi la chiamata di funzione con l’operatore *per spacchettare gli argomenti da una lista o tupla:

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

Nello stesso modo, i dizionari possono fornire argomenti chiave-valore con l’operatore **::

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

4.8.6. Espressioni Lambda

Piccole funzioni anonime possono essere create con la parola chiave lambda. Questa funzione restituisce la somma dei suoi due argomenti: lambda a, b: a+b. Le funzioni lambda possono essere utilizzate ovunque siano richiesti oggetti funzione. Sono sintatticamente limitate a una singola espressione. Semanticamente, sono solo zucchero sintattico per una normale definizione di funzione. Come le definizioni di funzione nidificate, le funzioni lambda possono fare riferimento a variabili dallo scope contenente:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

Nell’esempio precedente viene utilizzata un’espressione lambda per restituire una funzione. Un altro uso è passare una piccola funzione come argomento:

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

4.8.7. Stringhe di Documentazione

Ecco alcune convenzioni sul contenuto e la formattazione delle stringhe di documentazione.

La prima riga dovrebbe essere sempre un breve e conciso riassunto dello scopo dell’oggetto. Per brevità, non dovrebbe esplicitamente indicare il nome o il tipo dell’oggetto, poiché questi sono disponibili in altri modi (tranne se il nome è un verbo che descrive l’operazione di una funzione). Questa riga dovrebbe iniziare con una lettera maiuscola e terminare con un punto.

Se ci sono più righe nella stringa di documentazione, la seconda riga dovrebbe essere vuota, separando visivamente il riassunto dal resto della descrizione. Le righe seguenti dovrebbero essere uno o più paragrafi che descrivono le convenzioni di chiamata dell’oggetto, i suoi effetti collaterali, ecc.

Il parser di Python non rimuove l’indentazione dalle stringhe letterali multilinea in Python, quindi gli strumenti che elaborano la documentazione devono rimuovere l’indentazione se desiderato. Questo viene fatto utilizzando la seguente convenzione. La prima riga non vuota dopo la prima riga della stringa determina la quantità di indentazione per l’intera stringa di documentazione. (Non possiamo usare la prima riga poiché è generalmente adiacente alle virgolette di apertura della stringa quindi la sua indentazione non è evidente nella stringa letterale.) Lo spazio bianco «equivalente» a questa indentazione viene quindi rimosso dall’inizio di tutte le righe della stringa. Le righe che sono indentate di meno non dovrebbero verificarsi, ma se si verificano tutto il loro spazio bianco iniziale dovrebbe essere rimosso. L’equivalenza dello spazio bianco dovrebbe essere testata dopo l’espansione delle tabulazioni (a 8 spazi, normalmente).

Di seguito è riportato un esempio di una stringa di documentazione multilinea:

>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.

4.8.8. Annotationi delle Funzioni

Le annotationi delle funzioni sono informazioni di metadati completamente opzionali sui tipi utilizzati dalle funzioni definite dall’utente (vedi PEP 3107 e PEP 484 per ulteriori informazioni).

Le annotazioni sono memorizzate nell’attributo __annotations__ della funzione come un dizionario e non hanno alcun effetto su qualsiasi altra parte della funzione. Le annotazioni dei parametri sono definite da due punti dopo il nome del parametro, seguito da un’espressione che valuta il valore dell’annotazione. Le annotazioni di ritorno sono definite da una freccia ->, seguita da un’espressione, tra l’elenco dei parametri e i due punti che indicano la fine della dichiarazione def. L’esempio seguente ha un argomento obbligatorio, un argomento opzionale, e il valore di ritorno annotato:

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

4.9. Intermezzo: Stile di Codifica

Ora che stai per scrivere pezzi di Python più lunghi e complessi, è un buon momento per parlare dello stile di codifica. La maggior parte dei linguaggi può essere scritta (o più concisamente, formattata) in stili diversi; alcuni sono più leggibili di altri. Rendere facile per gli altri leggere il tuo codice è sempre una buona idea, e adottare uno stile di codifica piacevole aiuta enormemente.

Per Python, la PEP 8 è la guida di stile a cui aderiscono la maggior parte dei progetti; promuove uno stile di codifica molto leggibile e piacevole alla vista. Ogni sviluppatore Python dovrebbe leggerla prima o poi; ecco i punti più importanti estratti per te:

  • Usa l’indentazione a 4 spazi, e niente tabulazioni.

    4 spazi sono un buon compromesso tra una piccola indentazione (permette una maggiore profondità di nidificazione) e una grande indentazione (più facile da leggere). Le tabulazioni introducono confusione, ed è preferibile non usarle.

  • Manda a capo le righe in modo che non superino i 79 caratteri.

    Questo aiuta gli utenti con schermi piccoli e rende possibile avere diversi file di codice affiancati su schermi più grandi.

  • Usa righe vuote per separare funzioni e classi, e blocchi di codice più grandi all’interno delle funzioni.

  • Quando possibile, metti i commenti su una riga a parte.

  • Usa le stringhe di documentazione.

  • Usa spazi attorno agli operatori e dopo le virgole, ma non direttamente all’interno dei costrutti tra parentesi: a = f(1, 2) + g(3, 4).

  • Nomina le tue classi e funzioni in modo coerente; la convenzione è utilizzare UpperCamelCase per le classi e lowercase_with_underscores per le funzioni e i metodi. Usa sempre self come nome per il primo argomento del metodo (vedi Una prima occhiata alle classi per ulteriori informazioni su classi e metodi).

  • Non utilizzare codifiche complesse se il tuo codice è destinato ad essere utilizzato in ambienti internazionali. Il predefinito di Python, UTF-8, o anche il semplice ASCII funzionano meglio in ogni caso.

  • Allo stesso modo, non utilizzare caratteri non-ASCII negli identificatori se c’è solo la minima possibilità che persone che parlano una lingua diversa leggano o mantengano il codice.

Note a piè di pagina