03 settembre 2020

Argparse Tutorial

Tutorial su Argparse in Italiano

Tutorial su Argparse in Italiano

Autore:Tshepang Lekhonkhobe
Traduzione:Lewis
Data della traduzione:2 Settembre 2020

Questo tutorial deve essere inteso come una lieve introduzione al modulo argparse, il modulo python della standard library consigliato per l'analisi (parsing) degli argomento da linea di comando.

Note

Ci sono due altri moduli che per fare la stessa cosa, uno è getopt (l'equivalente in linguaggio C di getopt ) e l'altro è optparse , che però è deprecato. Nota anche che argparse è basato su optparse e quindi l'utilizzo è molto simile.

Concetti

Andiamo a vedere le caratteristiche funzionali che esploreremo in questo tutorial introduttivo usando il comando ls :

$ ls
cpython  devguide  prog.py  pypy  rm-unused-function.patch
$ ls pypy
ctypes_configure  demo  dotviewer  include  lib_pypy  lib-python ...
$ ls -l
total 20
drwxr-xr-x 19 wena wena 4096 Feb 18 18:51 cpython
drwxr-xr-x  4 wena wena 4096 Feb  8 12:04 devguide
-rwxr-xr-x  1 wena wena  535 Feb 19 00:05 prog.py
drwxr-xr-x 14 wena wena 4096 Feb  7 00:59 pypy
-rw-r--r--  1 wena wena  741 Feb 18 01:01 rm-unused-function.patch
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
...

Quello che possiamo imparare da questi quettro semplici comandi sono i seguenti concetti:

  • Il comando ls funziona anche quando viene lanciato senza nessuna opzione. Di default mostra il contenuto della directory in cui ci troviamo.
  • Se vogliamo andare aldilà di ciò che esso prevede di default dovremo aggiungere qualcosa. In questo caso, vorremmo mostrare il contenuto della directory pypy. Quello che in questo caso abbiamo fatto è stato specificare ciò che è noto come argomento posizionale. È chiamato così perché il programma sa cosa fare del valore in base a dove esso compare sulla linea di comando (ossia se è il primo, il secondo o il terzo argomento). Questo concetto è ancora più rilevante in comandi come cp dove l'uso di base è cp SORGENTE DESTINAZIONE. Infatti la prima posizione è ciò che vuoi venga copiato e la seconda posizione è dove vuoi venga copiato.
  • Ora diciamo che vogliamo cambiare il comportamento del programma. Nel nostro terzo comando vedremo più informazioni su ogni file piuttosto che solo vedere i nomi dei file. Il -l in questo caso è conosciuto come argomento opzionale.
  • Il quarto comando si riferisce allo al testo di aiuto. Questo è molto utile quando incroci un programma che non avevi mai usato prima e puoi farti un'idea di come funziona semplicemente leggendo il testo di aiuto (l'help).

Le basi

Partiamo con un esempio davvero molto semplice e che non fa quasi nulla:

import argparse
parser = argparse.ArgumentParser()
parser.parse_args()

Quello che segue è il risultato del codice eseguito:

$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h]

argomenti opzionali:
  -h, --help  mostra questo messaggio ed esci
$ python3 prog.py --verbose
usage: prog.py [-h]
prog.py: error: argomenti non riconosciuti: --verbose
$ python3 prog.py foo
usage: prog.py [-h]
prog.py: error: argomenti non riconosciuti: foo

Ecco qua quello che succede:

  • Far girare il programma senza nessun argomento opzionale non mostrerà risultati nello standard output. Quindi poco utile.
  • Il secondo punto inizia mostrando la piena utilità del modulo argparse . Non abbiamo fatto quasi nulla ma già abbiamo un bel messaggio di aiuto.
  • l'opzione --help, che puo' essere anche accorciato con -h, è l'unica opzione che ci viene data gratis (cioè non c'è bisogno di specificarla nella scrittura del codice). Specificare qualsiasi altra cosa ci restituirebbe un errore. Ma anche in questo otterremmo un messaggio utile e gratis (nel senso di automatico).

Introduzione agli argomenti posizionali

Un esempio:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo")
args = parser.parse_args()
print(args.echo)

e qui il codice che gira:

$ python3 prog.py
usage: prog.py [-h] echo
prog.py: error: the following arguments are required: echo
$ python3 prog.py --help
usage: prog.py [-h] echo

positional arguments:
  echo

optional arguments:
  -h, --help  show this help message and exit
$ python3 prog.py foo
foo

Questo è quello che succede:

  • Abbiamo aggiunto il metodo add_argument che è ciò che useremo per specificare le opzioni a linea di comando che il programma è disposto ad accettare. In questo caso, l'ho chiamato echo perché è in linea con la sua funzione.
  • Ora facendo girare il nostro programma ci richiede di specificare un'opzione.
  • Il metodo parse_args effettivamente restituisce alcuni dati dalle opzioni specificate, in questo caso echo.
  • La variabile (echo) viene creata da argparse magicamente e gratis (automaticamente, cioè non c'è bisogno di specificare in quale variabile il valore viene memorizzato). Quindi noterai che il suo nome corrisponde al nome della stringa dell'argomento passato al metodo, in questo caso echo.

Nota inoltre che, sebbene l'aiuto sia bello e tutto il resto, attualmente non è sufficientemente utile come dovrebbe essere. Per esempio vediamo che abbiamo echo come argomento posizionale, ma non possiamo sapere cosa faccia se non indovinando o leggendo il sorgente. Quindi facciamolo un po' più utile:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo", help="echo la stringa che vedi qui")
args = parser.parse_args()
print(args.echo)

E questo è quello che otteniamo:

$ python3 prog.py -h
usage: prog.py [-h] echo

positional arguments:
  echo        echo la stringa che vedi qui

optional arguments:
  -h, --help  mostra questo help ed esci

Ed ora come fare qualcosa di ancora più utile:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", help="calcola il quadrato di un dato numero")
args = parser.parse_args()
print(args.quadrato**2)

Quello che segue è il risultato del codice eseguito:

$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 5, in <module>
    print(args.quadrato**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

Non è andata così bene. Questo perchè argparse tratta le opzioni che gli abbiamo passato come stringhe, a menochè non gli diciamo diversamente. Quindi diciamo ad argparse di trattare l'input come se fosse un intero:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", help="calcola il quadrato di un dato numero",
                    type=int)
args = parser.parse_args()
print(args.quadrato**2)

Qui di seguito il risultato del codice eseguito:

$ python3 prog.py 4
16
$ python3 prog.py four
usage: prog.py [-h] quadrato
prog.py: error: argument quadrato: invalid int value: 'four'

Questa è andata bene. Il programma ora, oltre a fornire l'help, esce anche correttamente se gli viene fornito un input non corretto.

Introduzione agli argomenti opzionali

Finora abbiamo lavorato con gli argomenti posizionali. Adesso diamo un'occhiata a come aggiungere quelli opzionali:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="aumenta la verbosità dell'output")
args = parser.parse_args()
if args.verbosity:
    print("verbosity turned on")

E l'output:

$ python3 prog.py --verbosity 1
verbosity turned on
$ python3 prog.py
$ python3 prog.py --help
usage: prog.py [-h] [--verbosity VERBOSITY]

optional arguments:
  -h, --help            mostra il messaggio di help ed esci
  --verbosity VERBOSITY
                        aumenta la verbosità dell'output
$ python3 prog.py --verbosity
usage: prog.py [-h] [--verbosity VERBOSITY]
prog.py: error: argument --verbosity: expected one argument

Ecco quello che sta succedendo:

  • Il programma è scritto affinchè mostri qualcosa quando --verbosity è specificato e non mostri nulla quando non lo è.
  • Mostra che l'opzione è effettivamente opzionale, non ci sono errori quando il programma gira senza di essa. Nota che di default, se un argomento opzionale non è usato, alla variabile rilevante, in questo caso args.verbosity , come valore è stato dato None, che è la ragione per cui fallisce il vero test dell'espressione if .
  • Il messaggio di help un lievemente differente.
  • Quando si usa l'opzione --verbose, uno deve anche specificare dei valori, nessun valore.

L'esempio summenzionato accetta valori interi arbitrari per --verbosity, ma per il nostro semplice programma , solo due valori sono veramente utili, True o False. Andiamo a modificare il codice di conseguenza:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", help="aumenta la verbosità dell'output",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

E l'output:

$ python3 prog.py --verbose
verbosity turned on
$ python3 prog.py --verbose 1
usage: prog.py [-h] [--verbose]
prog.py: error: unrecognized arguments: 1
$ python3 prog.py --help
usage: prog.py [-h] [--verbose]

optional arguments:
  -h, --help  show this help message and exit
  --verbose   increase output verbosity

Ecco quello che succede:

  • L'opzione ora è più di un flag quindi qualcosa che richiede un valore. Abbiamo persino cambiato il nome dell'opzione in modo che riflettesse questa idea. Nota che ora specifichiamo una nuova keywaord, action, e gli diamo il valore store_true. Questo significa che, se specificato, assegnamo a args.verbose il valore di True. Mentre se non specifichiamo nulla significa False.
  • al secondo comando si arrabbia se specifichi un valore, nel vero spirito di cosa realmente sia un flag.
  • al terzo comando ti restituisce un messaggio di aiuto differente.

Opzioni brevi

Se hai familiarità con la linea di comando, ti sarai accorto che non ho ancora affrontato della versione breve delle opzioni. Ma è abbastanza semplice:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="Aumenta la verbosità dell'output",
                    action="store_true")
args = parser.parse_args()
if args.verbose:
    print("verbosity turned on")

Ecco quello che abbiamo:

$ python3 prog.py -v
verbosity turned on
$ python3 prog.py --help
usage: prog.py [-h] [-v]

optional arguments:
  -h, --help     mostra il messaggio di help ed esci
  -v, --verbose  aumenta la verbosità dell'output

Nota che la nuova abilità (l'opzione breve ndt) si riflette anche nel messaggio di help.

Combinare argomenti posizionali e opzionali

Il nostro programma continua a crescere in complessità:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="calcola il quadrato di un dato numero")
parser.add_argument("-v", "--verbose", action="store_true",
                    help="aumenta la verbosità dell'output")
args = parser.parse_args()
risposta = args.quadrato**2
if args.verbose:
    print(f"Il quadrato di {args.quadrato} uguale {risposta}")
else:
    print(risposta)

Ed ecco ora l'output:

$ python3 prog.py
usage: prog.py [-h] [-v] quadrato
prog.py: errore: i seguenti argomenti sono richiesti: quadrato
$ python3 prog.py 4
16
$ python3 prog.py 4 --verbose
the square of 4 equals 16
$ python3 prog.py --verbose 4
the square of 4 equals 16
  • Abbiamo rimesso l'argomento posizionale per questo si è arrabbiato.
  • Nota che l'ordine non importa.

Che ne dite di dare al nostro programma l'abilità di potere assegnare alla verbosità livelli multipli e poi usarli davvero:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="calcola il quadrato di un dato numero")
parser.add_argument("-v", "--verbosity", type=int,
                    help="aumenta la verbosità dell'output")
args = parser.parse_args()
risposta = args.quadrato**2
if args.verbosity == 2:
    print(f"il quadrato di {args.quadrato} uguale {risposta}")
elif args.verbosity == 1:
    print(f"{args.quadrato}^2 == {risposta}")
else:
    print(risposta)

E l'output:

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
usage: prog.py [-h] [-v VERBOSITY] quadrato
prog.py: error: argomento -v/--verbosity: un argomento richiesto
$ python3 prog.py 4 -v 1
4^2 == 16
$ python3 prog.py 4 -v 2
il quadrato di 4 uguale 16
$ python3 prog.py 4 -v 3
16

Questi vanno tutti bene eccetto l'ultimo, il quale espone un bug nel nostro programma. Andiamo a risolverlo restringendo i valori che puo' accettare l'opzione --verbosity:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="calcola il quadrato di un dato numero")
parser.add_argument("-v", "--verbosity", type=int, choices=[0, 1, 2],
                    help="autmenta la verbosità dell'output")
args = parser.parse_args()
risposta = args.quadrato**2
if args.verbosity == 2:
    print(f"il quadrato di {args.quadrato} è uguale a {risposta}")
elif args.verbosity == 1:
    print(f"{args.quadrato}^2 == {risposta}")
else:
    print(risposta)

E l'output:

$ python3 prog.py 4 -v 3
usage: prog.py [-h] [-v {0,1,2}] quadrato
prog.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v {0,1,2}] quadrato

argomenti posizionali:
  quadrato                calcola il quadrato di un dato numero

argomenti opzionali:
  -h, --help            mostra questo help ed esci
  -v {0,1,2}, --verbosity {0,1,2}
                        aumenta la verbosità dell'output

Nota che il cambiamento si riflette sia nel messaggio di errore che nella stringa di help. Ora usiamo un approccio differente, ma che è abbastanza usuale, per divertirci con la verbosità. Questo modo di gestire l'argomento della verbosità è lo stesso utilizzato dall'eseguibile CPython (prova tu stesso consultando l'output di python --help):

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="calcola il quadrato di un dato numero")
parser.add_argument("-v", "--verbosity", action="count",
                    help="aumenta la verbosità dell'output")
args = parser.parse_args()
risposta = args.quadrato**2
if args.verbosity == 2:
    print(f"il quadrato di {args.quadrato} è uguale a {risposta}")
elif args.verbosity == 1:
    print(f"{args.quadrato}^2 == {risposta}")
else:
    print(risposta)

qui abbiamo introdotto un'altra azione, "count", per contare il numero di volte che uno specifico argomento opzionale viene usato:

$ python3 prog.py 4
16
$ python3 prog.py 4 -v
4^2 == 16
$ python3 prog.py 4 -vv
il quadrato di 4 è uguale a 16
$ python3 prog.py 4 --verbosity --verbosity
the quadrato of 4 equals 16
$ python3 prog.py 4 -v 1
usage: prog.py [-h] [-v] quadrato
prog.py: error: unrecognized arguments: 1
$ python3 prog.py 4 -h
usage: prog.py [-h] [-v] quadrato

argomenti posizionali:
  quadrato           display a quadrato of a given number

argomenti opzionali:
  -h, --help       mostra questo messaggio di help ed esci
  -v, --verbosity  aumenta il livello di verbosità
$ python3 prog.py 4 -vvv
16
  • Si, ora c'è più di un flag (in modo simile a action="store_true") rispetto alla versione precedente del nostro scriptino. Questo dovrebbe spiegare perchè da errore.
  • Si comporta anche in modo simile all'azione "store_true".
  • Ora qui abbiamo una dimostrazione di ciò che l'azione "count" dà. Probabilmente hai già visto prima questa specie di uso.
  • E se non specifichi il flag -v, allora il flag viene considerato come se avesse valore None.
  • Come ci si dovrebbe aspettare specificando la forma lunga del flag dovremmo ottenere lo stesso output.
  • Purtroppo l'output del nostro help non è molto informativo sulla nuova abilità che il nostro script ha acquisito, ma questo problema puo' sempre essere risolto migliorando la qualità della nostra documentazione (per esempio mediante la l'argomento help).
  • quest'ultimo output mostra un bug nel nostro programma.

Risolviamolo:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="display a quadrato of a given number")
parser.add_argument("-v", "--verbosity", action="count",
                    help="increase output verbosity")
args = parser.parse_args()
risposta = args.quadrato**2

# bugfix: replace == with >=
if args.verbosity >= 2:
    print(f"Il quadrato di {args.quadrato} è uguale a {risposta}")
elif args.verbosity >= 1:
    print(f"{args.quadrato}^2 == {risposta}")
else:
    print(risposta)

E questo è ciò che ci dà:

$ python3 prog.py 4 -vvv
Il quadrato di 4 è uguale a 16
$ python3 prog.py 4 -vvvv
il quadrato di 4 è uguale a 16
$ python3 prog.py 4
Traceback (most recent call last):
  File "prog.py", line 11, in <module>
    if args.verbosity >= 2:
TypeError: '>=' not supported between instances of 'NoneType' and 'int'
  • il primo output è andato bene ed è stato risolto il bug che avevamo prima. Questo è come segue: vogliamo che qualsiasi valore >=2 sia il più verboso possibile.
  • Il terzo output non è un granchè.

Risolviamo questo bug:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("quadrato", type=int,
                    help="display a quadrato of a given number")
parser.add_argument("-v", "--verbosity", action="count", default=0,
                    help="aumenta il livello di verbosità dell'output")
args = parser.parse_args()
risposta = args.quadrato**2
if args.verbosity >= 2:
    print(f"il quadrato di {args.quadrato} è uguale a {risposta}")
elif args.verbosity >= 1:
    print(f"{args.quadrato}^2 == {risposta}")
else:
    print(risposta)

Abbiamo appena introdotto un'altra keyword, default. E l'abbiamo settata a 0 in modo da renderla comparabile ad altri valori interi. Ricordalo di default, se un argomento opzionale non è stato specificato. Esso restituisce il valore None e questo non puo' essere comparato ad un valore intero (perciò abbiamo l'eccezzione TypeError ).

E:

$ python3 prog.py 4
16

Con quello che abbiamo imparato finora possiamo starcene tranquilli, e abbiamo appena guardato in superficie. Il modulo argparse è molto potente, e lo esploreremo ancora un po' prima di concludere questo tutorial.

Addentriamoci in qualcosa di un po' più avanzato

Che fare se vogliamo espandere il nostro piccolo programma in modo che faccia altre potenze, non solo i quadrati:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="la base")
parser.add_argument("y", type=int, help="l'esponente")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
risposta = args.x**args.y
if args.verbosity >= 2:
    print(f"{args.x} la potenza di {args.y} è uguale a {risposta}")
elif args.verbosity >= 1:
    print(f"{args.x}^{args.y} == {risposta}")
else:
    print(risposta)

Output:

$ python3 prog.py
usage: prog.py [-h] [-v] x y
prog.py: error: the following arguments are required: x, y
$ python3 prog.py -h
usage: prog.py [-h] [-v] x y

positional arguments:
  x                la base
  y                l'esponente

argomenti opzionali:
  -h, --help       mostra questo help ed esci
  -v, --verbosity
$ python3 prog.py 4 2 -v
4^2 == 16

Considera che finora abbiamo usato il livello di verbosità a cambia il testo che è stato mostrato. Il seguente esempio infatti usa un livello di verbosità da mostrare invece more testo:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int, help="la base")
parser.add_argument("y", type=int, help="l'esponente")
parser.add_argument("-v", "--verbosity", action="count", default=0)
args = parser.parse_args()
risposta = args.x**args.y
if args.verbosity >= 2:
    print(f"lanciando '{__file__}'")
if args.verbosity >= 1:
    print(f"{args.x}^{args.y} == ", end="")
print(risposta)

Output:

$ python3 prog.py 4 2
16
$ python3 prog.py 4 2 -v
4^2 == 16
$ python3 prog.py 4 2 -vv
Running 'prog.py'
4^2 == 16

Conflitti di opzioni

Finora abbiamo lavorato con i due metodi dell'istanza di ArgumentParser . Adesso ne introdurremo un terzo, add_mutually_exclusive_group . Esso ci permette di specificare opzioni che sono in conflitto fra loro. Quindi cambiamo il resto del programma per dare senso alla nuova funzionalità: introduciamo l'opzione -quiet, la quale sarà in opposizione a --verbose:

import argparse

parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="la base")
parser.add_argument("y", type=int, help="l'esponente")
args = parser.parse_args()
answer = args.x**args.y

if args.quiet:
    print(risposta)
elif args.verbose:
    print(f"{args.x} la potenza  di {args.y} è uguale a {risposta}")
else:
    print(f"{args.x}^{args.y} == {risposta}")

A questo punto il nostro programma è più semplice e abbiamo perso alcune funzionalità per il sake della dimostrazione. Comunque ecco l'output:

$ python3 prog.py 4 2
4^2 == 16
$ python3 prog.py 4 2 -q
16
$ python3 prog.py 4 2 -v
4 alla potenza di 2 è uguale a 16
$ python3 prog.py 4 2 -vq
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose
$ python3 prog.py 4 2 -v --quiet
usage: prog.py [-h] [-v | -q] x y
prog.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

Questo dovrebbe essere semplice da comprendere. Abbiamo aggiunto l'ultimo output così puoi vedere la flessibilità che abbiamo ottenuto, in altre parole mischiare le opzioni in forma lunga con quelle in forma breve.

Prima di concludere in caso i tuoi utenti non sappiano la funziona del tuo programma gliela potrai dire in questo modo:

import argparse

parser = argparse.ArgumentParser(description="calculate X to the power of Y")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true")
group.add_argument("-q", "--quiet", action="store_true")
parser.add_argument("x", type=int, help="la base")
parser.add_argument("y", type=int, help="l'esponente")
args = parser.parse_args()
risposta = args.x**args.y

if args.quiet:
    print(risposta)
elif args.verbose:
    print("{} la potenza di {} è uguale a {}".format(args.x, args.y, risposta))
else:
    print("{}^{} == {}".format(args.x, args.y, risposta))

Nota che c'è una minima differenza nel testo che mostra l'uso dello script. In particoalre [-v | -q], ci dicono che possiamo o usare -v oppure -q, ma non entrambi contemporaneamente:

$ python3 prog.py --help
usage: prog.py [-h] [-v | -q] x y

calcola X alla potenza di Y

positional arguments:
  x              la base
  y              l'esponente

optional arguments:
  -h, --help     mostro il messaggio di help ed esci
  -v, --verbose
  -q, --quiet

Conclusioni

Il modulo argparse offre molto di più di quello che abbiamo visto fino a qui e la sua documentazione è piuttosto dettagliata e completa oltre che piena di esempi. Passando in rassegna questo tutorial dovresti essere in grado di digerire la documentazione senza sentirti sopraffatto.