7: Funzioni con un numero variabile di argomenti

Nella Lezione 18 del mio Tutorial base ho già parlato del meccanismo dei parametri di default e dei keyword arguments che permettono di chiamare una funzione omettendo alcuni dei suoi argomenti. Però in Python esistono alcune funzioni builtin che accettano un qualsiasi numero di argomenti. Un esempio classico è la print():


>>> print()                   # nessun argomento

>>> print("uno")
uno

>>> print("uno", "due", "tre")
uno due tre

>>> print("uno", "due", "tre", "quattro", "cinque",
          "sei", "sette", "otto", "nove", "dieci")
uno due tre quattro cinque sei sette otto nove dieci

Come è possibile definire una funzione che abbia un simile comportamento? Attenzione a non confondersi: in Python esistono anche funzioni, come la sum(), che accettano come argomento una lista ed operano sui suoi elementi. Quello che noi vorremmo fare, però, è diverso: passare non un solo parametro contenente tanti elementi (cioè quello che nella terminologia di Python viene chiamato un iterable), ma direttamente gli elementi stessi sui quali la funzione deve operare. Chiariamoci le idee con questo esempio:


>>> sum([1, 2, 3, 4])         # passo UNA lista contenente 4 elementi
10

>>> sum(1, 2, 3, 4)           # questo non si puo'fare!
Traceback (most recent call last):
  File "", line 1, in 
    sum(1, 2, 3, 4)
TypeError: sum() takes at most 2 arguments (4 given)

POSITIONAL ARGUMENTS E KEYWORD ARGUMENTS

Supponiamo di aver definito una funzione in questo modo:


>>> def func(arg1, arg2, arg3, arg4):
        .   .   .
arg1, arg2, arg3 e arg4 sono detti parametri formali della funzione. Quando chiameremo la funzione nel corso del programma principale Python sostituirà ad essi i parametri attuali indicati nella chiamata ed eseguirà il corpo della funzione (le righe indentate dopo la def). Per chiamare la nostra funzione potremmo scrivere, per esempio:

>>> func(1, 5, 6, 10)

Gli argomenti elencati in questo modo (nella chiamata) sono detti positional arguments e vengono assegnati ai parametri formali in base al loro ordine (quindi eseguendo la funzione arg1 assumerà il valore 1, arg2 il valore 5 eccetera).

Come saprete Python ci permette anche di indicare esplicitamente, nella chiamata, il nome degli argomenti, così:


>>> func(arg1=1, arg2=5, arg3=6, arg4=10)

Gli argomenti indicati in questo modo sono detti keyword arguments e sono assegnati ai parametri formali in base al nome fornito. Per i keyword arguments non è necessario rispettare l'ordine che abbiamo stabilito della definizione, in quanto diciamo noi esplicitamente a Python come deve associare gli argomenti. (ATTENZIONE! Non confondete i keyword arguments con i parametri di default, che sono invece elencati nella definizione, così:


>>> def func(arg1, arg2, arg3, arg4=10):
>>> # arg4 e' un argomento di default, che potra' essere presente o meno nella chiamata
        .   .   .

Python ci permette di mescolare liberamente, nella chiamata, argomenti positional e keyword, con l'unica restrizione che i keyword devono sempre seguire i positional. Per esempio:


>>> func(1, 5, 6, 10)                        # tutti positional, assegnati nel loro ordine
>>> func(1, 5, arg4=10, arg3=6)              # due positional e due keyword (non in ordine)
>>> func(arg3=6, arg4=10, arg1=1, arg2=5)    # tutti keyword
>>> func(arg1=1, arg2=5, 6, 10)              # errore: i positional devono precedere i keyword

Per una trattazione più approfondita vedi la mia Lezione 18.

IL PARAMETRO *args

Apriamo IDLE e scriviamo:


>>> def func(*args):
        print(args)
	
		
>>>

(vi ricordo che in IDLE, quando definiamo una funzione, Python interpreta le linee di codice successive come il suo corpo. Per interrompere la definizione e tornare al prompt dovete premere due volte <RETURN>). Il codice che abbiamo scritto viene accettato senza errori, ma qual è l'effetto di questa sintassi? Proviamo a chiamare la funzione che abbiamo definito:


>>> func(1)
(1, )

>>> func(1, 2)
(1, 2)

>>> func(1, 2, 3)
(1, 2, 3)

Vediamo che ora la nostra funzione accetta un numero variabile di argomenti: quando la funzione viene chiamata il suo parametro formale args diventa una tuple, che contiene tutti gli argomenti che abbiamo passato alla funzione (nella prima chiamata la func() stampa una tuple con un solo elemento, che viene scritta (1, ) per distinguerla da un numero tra parentesi).

Ora possiamo scrivere una funzione simile alla sum(), che accetta un numero variabile di argomenti e restituisce la loro somma:


>>> def somma(*args):
        s = 0                            # comincio con 0
        for i in args:
            s += i                       # poi sommo tutti i numeri in args
        return s
	
		
>>> somma(1)
1

>>> somma(1, 2)
3

>>> somma(1, 2, 3, 4)
10

Vediamo di capire meglio la sintassi che abbiamo usato:

L'operatore * scritto davanti al nome di una variabile è detto unpacking operator: esso ha l'effetto di trasformare la variabile alla quale è applicato in un iterable, al quale può essere assegnato un numero indefinito di valori.

In particolare questo operatore è impiegato nella lista dei parametri formali quando definiamo una funzione; in questo caso notiamo che:

Facciamo qualche esempio per chiarire gli ultimi due punti, un po' difficili da comprendere a fondo:


>>> def func(arg1, *args):       # arg1 e' obbligatorio, args raccoglie tutti gli altri positional
        print("arg1 =", arg1, "args =", args)

  
>>> func(1)
arg1 = 1 args = ()

>>> func(1, 2)
arg1 = 1 args = (2,)

>>> func(1, 2, 3, 4, 5)
arg1 = 1 args = (2, 3, 4, 5)

>>> func(arg1=1)
arg1 = 1 args = ()

La nostra func() accetta un argomento arg1 obbligatorio, e poi un numero variabile di argomenti posizionali. Quando la chiamiamo il primo argomento che forniamo viene assegnato ad arg1 e tutti i successivi vanno in args. Possiamo anche chiamare arg1 come keyword a patto di non mettere altri argomenti (perchè nella chiamate i keyword arguments devono seguire i positional). Ora provate questa:


>>> def func(*args, arg1):      # args raccoglie tutti i positional, arg1 deve essere keyword
        print("args =", args, "arg1 =", arg1)

    
>>> func(1, 2, arg1=3)          # ok
args = (1, 2) arg1 = 3          

>>> func(arg1=3)                # nessun positional
args = () arg1 = 3

>>> func(1)                     # errore: arg1 e' obbligatorio
Traceback (most recent call last):
  File "", line 1, in 
    func(1)
TypeError: func() missing 1 required keyword-only argument: 'arg1'

Ora abbiamo dichiarato un argomento arg1 dopo *args. Quando chiamiamo la funzione arg1 deve essere obbligatoriamente un keyword, mentre tutti i positional vengono assegnati ad args (notate che nel messaggio di errore Python ci avvisa che manca un keyword).

ESERCIZIO 7.1: Definite una funzione massimo che accetti almeno un argomento e restituisca il massimo tra tutti i suoi argomenti.
ESERCIZIO 7.2: Definite una funzione massimo1 che accetti un numero variabile di argomenti (anche nessuno) e restituisca 0 se chiamata senza argomenti, oppure il massimo tra i suoi argomenti.
ESERCIZIO 7.3 Definite una funzione divisibile che accetti un numero variabile di positional, più un keyword div, e restituisca True se tutti gli argomenti sono divisibili per div, False altrimenti (se chiamata con 0 positional deve restituire True).
Potete fare questi esercizi direttamente in IDLE, provando poi a chiamare le vostre funzioni in maniera interattiva.

IL PARAMETRO **kwargs

Oltre ad *args Python ammette anche una sintassi specifica per i keyword arguments. Definiamo in IDLE quest'altra funzione:


>>> def func(**kwargs):
        print(kwargs)

    
>>> func(arg1=1)
{'arg1': 1}

>>> func(1)
Traceback (most recent call last):
  File "", line 1, in 
    func(1)
TypeError: func() takes 0 positional arguments but 1 was given

L'operatore ** scritto davanti al nome di una variabile ha l'effetto di trasformare la variabile alla quale è applicato in un dizionario, al quale può essere assegnato un numero indefinito di coppie chiave-valore.

Quando questo operatore viene impiegato nella lista dei parametri formali nella definizione di una funzione essa accetterà un numero variabile di keyword arguments, che saranno assegnati alle coppie chiave-valore del dizionario kwargs.

Facciamo anche qui qualche osservazione:


>>> def func(arg1, **kwargs):      # un positional, poi un numero variabile di keyword
        print("arg1 =", arg1, "kwargs =", kwargs)

    
>>> func(1, b=2, c=3)
arg1 = 1 kwargs = {'b': 2, 'c': 3}

>>> def func(*args, **kwargs):     # positional e keyword in numero variabile
        print("args =", args, "kwargs =", kwargs)

    
>>> func(1, 2, 3, d=4)
args = (1, 2, 3) kwargs = {'d': 4}

Bisogna fare un'osservazione importante: quando definiamo una funzione dobbiamo indicare nella def i nomi dei suoi parametri formali; nella chiamata Python controlla che i nomi dei nostri keyword arguments siano effettivamente presenti tra i nomi dei parametri formali, e se abbiamo sbagliato si ferma con un errore. Ad esempio:


>>> def area(base, altezza):
        return base * altezza


>>> area(base=5, altezz= 10)
Traceback (most recent call last):
  File "", line 1, in 
    area(base=5, altezz=10)
TypeError: area() got an unexpected keyword argument 'altezz'

Qui ho volutamente commesso un errore scrivendo altezz, ma Python si è accorto che questo nome non è un parametro formale. Se invece usiamo **kwargs la nostra funzione accetterà qualsiasi nome di argomento, e sarà responsabilità di chi scrive la funzione controllare che i nomi siano corretti. Questo potrebbe complicare notevolmente il corpo della funzione. Vediamo la stessa funzione area definita in questo modo:


>>> def area(**kwargs):
        if "base" in kwargs.keys() and \
        "altezza" in kwargs.keys() and \
        len(kwargs) == 2:
            return kwargs["base"] * kwargs["altezza"]
        else:
            raise TypeError("Inappropriate argument type")

    
>>> area(altezza=5, base=8)
40

area(alt=5, base=8)
>>> Traceback (most recent call last):
  File "", line 1, in 
    area(alt=5, base=8)
  File "", line 7, in area
    raise TypeError("Inappropriate argument type")
TypeError: Inappropriate argument type

Le righe #2-#4 servono a controllare che i parametri inseriti siano esatti: nella #2 controlliamo se base è contenuto nelle chiavi di kwargs, nella #3 controlliamo se c'è altezza e nella #4 controlliamo di non aver inserito nessun altro nome. A questo punto se tutto è corretto calcoliamo l'area, altrimenti(nella #7) solleviamo un'eccezione. Per queste difficoltà l'uso di **kwargs è di solito limitato a funzioni che accettano un gran numero di parametri, evitando così di elencarli tutti nella definizione, ma è controproducente in funzioni abbastanza semplici.

ESERCIZIO 7.4: Definire una funzione provaparametri1 che accetti un solo parametro **kwargs e che stampi i nomi dei keyword arguments passati nella chiamata (se non ricordate come si trovano le chiavi di un dizionario guardate qui).
ESERCIZIO 7.5: Definire una funzione provaparametri2 che accetti un solo parametro **kwargs e che stampi le coppie nome - valore dei keyword arguments passati nella chiamata (guardate sempre qui).
ESERCIZIO 7.6: Definire una funzione provaparametri3 che accetti un solo parametro **kwargs. La funzione deve verificare che i nomi dei keyword arguments passati nella chiamata siano compresi tra par1 par2 par3 par4 e restituire True se questo è vero, False altrimenti.
SUGGERIMENTO: per rendere più facile il test potete usare l'operatore in ed una lista.

ANCORA SULL'OPERATORE *

Facciamo per ultima cosa notare che l'uso dell'unpacking operator * non è limitato alla lista dei parametri formali nella dichiarazione di una funzione, ma esso può essere usato anche nelle normali istruzioni di assegnazione. Python richiede però che esso faccia parte di una tuple (cioè che sia in un'istruzione di assegnazione multipla).


>>> a, *args = 1, 2, 3, 4
>>> a
1

>>> args
[2, 3, 4]
 
>>> *args = 1, 2, 3
SyntaxError: starred assignment target must be in a list or tuple         

Fine della lezione