Created on 13 Sep 2013 ;    Modified on 29 Sep 2016

Python: l'uso degli asterischi nelle definizioni e nelle chiamate di funzione

Questa nota deriva da questo chiaro e conciso articolo pubblicato anni fa nel blog SaltyCrane Blob

In python è possibile definire funzioni con un numero variabile di argomenti utilizzando le sintassi *args e **kwargs.

Ad esempio, eseguento nell'interprete python:

>>> def tf(*args):
...    print type(args), args

>>> tf(1, 2, 3, 4)    
<type 'tuple'> (1, 2, 3, 4)
>>> tf('a', 'b')    
<type 'tuple'> ('a', 'b')

Abbiamo definito la funzione tf che ci dice (print) il tipo della struttura dati args e il suo valore. La prima chiama a tf ha 4 parametri. La seconda ne ha 2. Come si vede python non fa una piega e le esegue entrambe. args è una tupla. Quindi potremo accedere agli argomenti tramite posizione. Ad esempio args[0] ci restituirà il primo argomento. Mentre len(args) ci indica con quanti argomenti è stata chiamata la funzione.

Se si utilizza **wkargs potremmo avere:

>>> def tf2(**kwargs):
...     print type(kwargs), kwargs
>>> tf2(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tf2() takes exactly 0 arguments (2 given)
>>> tf2(uno=1, due=2)
<type 'dict'> {'due': 2, 'uno': 1}
>>> tf2(uno=1, due=2, altro='roma')
<type 'dict'> {'altro': 'roma', 'due': 2, 'uno': 1}

In questo caso abbiamo definito la funzione tf2. Anche questa ci indica il tipo della struttura dati accettata dalla funzione (kwargs) e il suo valore. Quando proviamo a chiamare tf2 con una serie di parametri posizionali, otteniamo un errore (TypeError). Errore che personalmente trovo un po' fuorviante. Infatti il problema non consiste nel numero di parametri usati, ma nel loro tipo. Chiamando tf2 utilizzando parametri con nome, ecco che il tutto prende vita. Qui vediamo che la struttura dati kwargs è un dizionario. Quindi l'acceso agli elementi avviene tramite chiave. Ad esempio kwargs['uno'] ci restituirà il valore 1. Mentre la quantità di argomenti la possiamo conoscere, come nel caso precedente, utilizzando len(kwargs).

E' possibile utilizzare entrambe le strutture dati contemporaneamente. A patto di usare prima args e poi kwargs. Ad esempio:

>>> def tf3(*args, **kwargs):
...     print type(args), args
...     print type(kwargs), kwargs
...
>>> tf3()
<type 'tuple'> ()
<type 'dict'> {}
>>> tf3(1, due=2)
<type 'tuple'> (1,)
<type 'dict'> {'due': 2}
>>> tf3(1,2,tre=3,quattro=4)
<type 'tuple'> (1, 2)
<type 'dict'> {'quattro': 4, 'tre': 3}
>>> tf3(uno=1)
<type 'tuple'> ()
<type 'dict'> {'uno': 1}
>>> tf3(1)
<type 'tuple'> (1,)
<type 'dict'> {}
>>> tf3(uno=1, 2)
  File "<stdin>", line 1
SyntaxError: non-keyword arg after keyword arg

tf3 può essere chiamata con un numero variabile di parametri posizionali e parametri con nome. Ma i parametri posizionali, se presenti, devono precedere tutti i parametri con nome. Come ci mette bene in evidenza l'ultima chiamata a tf3 in cui abbiamo osato scrivere prima un parametro con nome e poi un posizionale.

Bene. Fin qui abbiamo visto l'uso di * e ** nella definizione della funzione.

Ma se li usassimo in fase di chiamata alla funzione? Che accadrebbe? Python lo interpreterebbe come un ordine di usare la struttura dati in modo acconcio per la chiamata in corso. Vediamo questo esempio.

>>> def tf4(uno, due, tre=None, quattro=None):
...     print type(uno), uno
...     print type(due), due
...     print type(tre), tre
...     print type(quattro), quattro
...
>>> tf4(1,2)
<type 'int'> 1
<type 'int'> 2
<type 'NoneType'> None
<type 'NoneType'> None
>>> tf4(1,2,3,4)
<type 'int'> 1
<type 'int'> 2
<type 'int'> 3
<type 'int'> 4
>>> tf4(1,2,quattro=4,tre=3)
<type 'int'> 1
<type 'int'> 2
<type 'int'> 3
<type 'int'> 4
>>> p=('uno','due',)
>>> tf4(*p)
<type 'str'> uno
<type 'str'> due
<type 'NoneType'> None
<type 'NoneType'> None
>>> kw={'tre':'tre', 'quattro':'quattro'}
>>> tf4(*p,**kw)
<type 'str'> uno
<type 'str'> due
<type 'str'> tre
<type 'str'> quattro
>>> tf4(**kw)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tf4() takes at least 2 arguments (2 given)
>>> tf4(**p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tf4() argument after ** must be a mapping, not tuple

In questo esempio tf4 è una normale funzione cou un numero fisso (4) di parametri. I primi due posizionali e i secondi due con nome.

Le chiamate tf4(1,2) ; tf4(1,2,3,4) ; tf4(1,2,quattro=4,tre=3), mostrano il comportamento canonico della funzione.

Dopo di che definiamo la struttura dati p come una tupla di due elementi e chiamiamo tf4(*p). Python spacchetterà la tupla nei parametri posizionali della funzione ed eseguirà la chiamata. E' come se avessimo chiamato tf4('uno','due'). I None sono i valori di default dei parametri con nome.

Se definiamo il dizionario kw con le stesse parole chiave dei parametri con nome (tre e quattro), e chiamiamo tf4(*p, **kw) ecco che python organizza l'esecuzione della funzione sia con i parametri posizionali contenuti in p, che con quelli con nome contenuti in kw.

Le ultime due chiamate dimostrano possibili errori. Nella penultima abbiamo chiesto a python di eseguire tf4 senza parametri posizionali, che invece sono obbligatori. Nell'ultima abbiamo sbagliato la tipizzazione della struttura passata. ** attende un dizionario, non una tupla.

E se nel dizionario un nome di parametro fosse errato? O in eccesso? Presto fatto:

>>> kw2={'tre':'tre', 'q':'quattro'}
>>> kw3={'tre':'tre', 'quattro':'quattro', 'cinque':'cinque'}
>>> tf4(*p, **kw2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tf4() got an unexpected keyword argument 'q'
>>> tf4(*p, **kw3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: tf4() got an unexpected keyword argument 'cinque'

In entrambi i casi Python segnala il fatto che vi è un parametro con nome sconosciuto.