Python argparse 教學:比 sys.argv 更好用,讓命令列引數整潔又有序

by 好豪
Published: Last Updated on

當你在命令列呼叫 Python 程式、又同時需要修改變數內容,你可以用命令列引數(command-line argument)的方式將變數資訊傳入執行程式中,初學 Python 常會使用 sys.argv,例如:

## test.py
import sys

print(f"sys.argv 是個陣列,長度為:{len(sys.argv)}")
print("第 1 個 sys.argv 會是 py 檔案名稱:")
print(sys.argv[0])
print("第 2 個 sys.argv 的內容:")
print(sys.argv[1])
print("第 3 個 sys.argv 的內容:")
print(sys.argv[2])
print("第 4 個 sys.argv 的內容:")
print(sys.argv[3])
$ python3 test.py abc 9527
sys.argv 是個陣列,長度為:3
第 1 個 sys.argv 會是 py 檔案名稱:
test.py
第 2 個 sys.argv 的內容:
abc
第 3 個 sys.argv 的內容:
9527
第 4 個 sys.argv 的內容:
Traceback (most recent call last):
  File "test.py", line 11, in <module>
    print(sys.argv[3])
IndexError: list index out of range

如範例所見,sys.argv 只能將程式引數一個個以陽春的陣列傳遞,當你需要解析更複雜的引數,包括讓使用者輸入引數內容有彈性、又能讓程式整潔有序地管理引數,你就需要 argparse 函式庫

你可以用 argparse 做到的複雜命令列引數處理,就以用於下載 YouTube 影片的 youtube-dl 專案 為例:

$ youtube-dl XqZsoesa55w -x --audio-format mp3 -o '%(title)s.%(ext)s'

這行呼叫 youtube-dl Python 程式的指令中,引數可以單純用位置決定、可以是布林變數的開關功能、也可以明確寫出控制變數內容。在這篇筆記裡,我將用幾個範例帶你學會 argparse 函式庫如何做到這些引數管理功能、並且與你分享我使用 argparse 時認為實用的幾項小技巧。



位置引數

使用 argparse 的三個基本步驟包括:

  1. 先創造 argparse.ArgumentParser() 物件,它是我們管理引數需要的 “parser”
  2. add_argument() 告訴 parser 我們需要的命令列引數有哪些
  3. 使用 parse_args() 來從 parser 取得引數傳來的 Data

我們從位置引數開始學習這些基本步驟。

位置引數(Positional Argument)會把使用者輸入的引數依照輸入順序放進你宣告的引數變數中,在下方範例中,add_argument() 最前面的參數就是你的命令列引數名稱:

## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("arg1")
parser.add_argument("arg2",
                    help="這是第 2 個引數,請輸入整數")
parser.add_argument("arg3",
                    help="這是第 3 個引數,「要求」輸入整數",
                    type=int)
args = parser.parse_args()

print(f"第 1 個引數:{args.arg1:^10},type={type(args.arg1)}")
print(f"第 2 個引數:{args.arg2:^10},type={type(args.arg2)}")
print(f"第 3 個引數:{args.arg3:^10},type={type(args.arg3)}")

設置位置引數後,(在你進一步設定 nargs 參數以前)如果在命令列呼叫這支 Python 程式沒有引數,就會出錯:

## 沒有引數會出錯
$ python3 test.py
usage: test.py [-h] arg1 arg2 arg3
test.py: error: the following arguments are required: arg1, arg2, arg3

## 輸入太多引數也不行
$ python3 test.py a b c d e f
usage: test.py [-h] arg1 arg2 arg3
test.py: error: argument arg3: invalid int value: 'c'

正確依序輸入引數的結果如下:

$ python3 test.py hello world 3
第 1 個引數:  hello   ,type=<class 'str'>
第 2 個引數:  world   ,type=<class 'str'>
第 3 個引數:    3     ,type=<class 'int'>

從以上結果可以看到第 3 個引數在 add_argument() 設定 type=int,會要求使用者輸入引數「必須」是 int 型別,若不是則會出錯;相較之下,第 2 個引數雖然在 help 參數提示使用者要輸入整數,但是實際上即使不輸入整數,也不會產生 error。

## 第 3 個引數設定 type=int
## 若使用者不是輸入整數就會回報錯誤
$ python3 test.py hello world three
usage: test.py [-h] arg1 arg2 arg3
test.py: error: argument arg3: invalid int value: 'three'

## 第 2 個引數沒有設定 type 參數
## 只會在 -h 或 --help 產生要求使用者輸入整數的提示
$ python3 test.py -h
usage: test.py [-h] arg1 arg2 arg3

positional arguments:
  arg1
  arg2        這是第 2 個引數,請輸入整數
  arg3        這是第 3 個引數,「要求」輸入整數

optional arguments:
  -h, --help  show this help message and exit

儲存引數資料的 Namespace 物件

使用 parse_args() 從 parser 取得引數資料後,此函數會回傳 Namespace 物件,這只是一個單純把資料用屬性(attribute)儲存下來的超簡單類別。

## 延續上方傳入三個引數的範例
## $ python3 test.py hello world 3

## args = parser.parse_args()
## args 是 Namespace 物件,只是個極簡單的類別

>>> print(type(args))
<class 'argparse.Namespace'>
>>> print(args)
Namespace(arg1='hello', arg2='world', arg3=3)

## 用一般取得 attribute 的方式來取得引數資料內容
>>> print(args.arg1)
hello

## 你也可以使用 vars(),以 dict 資料結構取得引數資料
>>> print(vars(args))
{'arg1': 'hello', 'arg2': 'world', 'arg3': 3}

指定引數數量

add_argument() 內設定 nargs 參數,可以限制你的引數有幾個:

  • nargs=3:引數只能恰好是 3 個
  • nargs='?':引數只能是 0 個或是 1 個
  • nargs='+':引數至少 1 個(1 個或任意多個)
  • nargs='*':引數可以是任意數量(0 個或任意多個)

當你設定 nargs? 或者 * 時,表示你的程式可以接受使用者不輸入該項引數,此時你可以在 default 設定預設值、在使用者沒有輸入該引數時採用。

## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("arg1",
                    nargs=3,
                    type=int,
                    help="這是第 1 個引數,請輸入三個整數")
parser.add_argument("arg2",
                    nargs='?',
                    help="這是第 2 個引數,請輸入一個值、也可以不輸入",
                    default="NO_VALUE")
parser.add_argument("arg3",
                    nargs='+',
                    help="這是第 3 個引數,請輸入一個或多個值")
args = parser.parse_args()
print(args)
## 請注意第一與第三個引數,可以接受多筆資料
## 並且會以 list 資料結構儲存
$ python3 test.py 10 11 12 hey hello world
Namespace(arg1=[10, 11, 12], arg2='hey', arg3=['hello', 'world'])

## 下方範例有兩個值得注意的地方:
## * 第二個引數可以接受無傳入值、但是第三個引數一定要有值
##   所以 'hey' 這個引數就跳過 arg2、由 arg3 接收
## * nargs='+' 或者 nargs='*' 引數會用 list 儲存資料
##   就算只接受到一個傳入引數資料也一樣
$ python3 test.py 10 11 12 hey
Namespace(arg1=[10, 11, 12], arg2='NO_VALUE', arg3=['hey'])

選項引數

選項引數(Optional Argument)必須放在位置引數之後、可以是任意順序,只要使用特定符號表示(通常用破折號 ---),parser 就知道該引數表示相對應的內容為何。

選項引數的名稱同樣放在 add_argument() 參數最前面的位置,通常會分成長與短兩種型態。短型態是長型態的一個字縮寫、讓選項引數可以更簡短,例如 --arg1 的短型態寫成 -a

## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("first_position_arg",
                    nargs=2,
                    help="這是第 1 個位置引數,請輸入兩個任意值")
parser.add_argument("-a",
                    "--arg1",
                    type=str,
                    help="這是第 1 個選項引數,請輸入一個字串")
parser.add_argument("--arg2",
                    nargs=3,
                    type=int,
                    help="這是第 2 個選項引數,請輸入三個整數")
args = parser.parse_args()
print(args)
## 此範例中,位置引數與選項引數兩者都使用
$ python3 test.py 123 abc -a hey --arg2 11 12 13
Namespace(first_position_arg=['123', 'abc'], arg1='hey', arg2=[11, 12, 13])

選項引數範例:verbose 的四種寫法

verbose 是常見的命令列引數,一般來說,他的主要功能是讓使用者控制「程式執行過程中給予多少提示訊息」,或者是說希望你的程式多「囉唆」。舉例而言,Python 深度學習熱門的 Keras 套件中,模型訓練的 fit() 函式就有三個 verbose 等級、區分三種資訊顯示詳盡程度,雖然此例的 verbose 是寫在函式中,但是區分三個 verbose 等級的功能只要用 argparse 就可以在命令列引數做到!

Keras 的 fit() 函數有三個 verbose 等級(source: stackoverflow

接下來筆者好豪將以 verbose 為例,介紹四種寫法、帶你認識 argparse 選項引數的更多功能。

store_true:簡單開關

add_argument()action 參數設定為 "store_true",只要使用者輸入此命令列引數,parse_args() 就紀錄該引數值為 True,否則為 False,這種簡單開關類型的命令列引數設定一般被稱為 “Flag“。請注意此類選項引數後方不能再輸入其他內容。

## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbose",
                    action="store_true",  # 引數儲存為 boolean
                    help="簡單開關的引數")
args = parser.parse_args()
print(args.verbose)
$ python3 test.py --verbose
args.verbose 的數值為:True
我現在是個囉唆的程式

## 沒輸入 Flag 的話,預設為 False
$ python3 test.py
args.verbose 的數值為:False

## Flag 後面不可以輸入其他值
$ python3 test.py --verbose hey
usage: test.py [-h] [--verbose]
test.py: error: unrecognized arguments: hey

conflict:互斥類型的開關

如果你希望使用者輸入的 Flag 是需要明確表示的,不像上個範例沒輸入 Flag 就預設為 False,例如,在 verbose 你希望使用者用命令列引數直接表示程式該囉唆(verbose)還是安靜(quiet),這時就適合使用互斥引數組合 add_mutually_exclusive_group(),在同個 “mutually_exclusive_group” 內,只允許其中唯一一個命令列引數出現、不能同時使用多個引數。

## test.py
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-v",
                   "--verbose",
                   action="store_true",
                   help="開啟囉唆模式")
group.add_argument("-q",
                   "--quiet",
                   action="store_true",
                   help="開啟安靜模式")
args = parser.parse_args()
if args.verbose:
    print("我  現  在  是  個  囉  唆  的  程  式  !")
elif args.quiet:
    print("安... 靜... 的... 程... 式...")
else:
    print(f"args.verbose 的值為:{args.verbose}")
    print(f"args.quiet 的值為:{args.quiet}")
    print("你沒有告訴我該囉唆還是安靜?")
$ python3 test.py --verbose
我  現  在  是  個  囉  唆  的  程  式  !

$ python3 test.py --quiet
安... 靜... 的... 程... 式...

## 同個 "mutually_exclusive_group" 內的多個引數,不可以同時出現!
$ python3 test.py -v -q
usage: test.py [-h] [-v | -q]
test.py: error: argument -q/--quiet: not allowed with argument -v/--verbose

## 同樣地,action="store_true" 時,如果沒有使用 flag 的話
## 預設都會是 False
$ python3 test.py
args.verbose 的值為:False
args.quiet 的值為:False
你沒有告訴我該囉唆還是安靜?

choices:有範圍的等級

更進一步,當你希望你的程式引數只能是特定幾個值、或者是有範圍的值,就如同前面提到的 Keras 函數 fit() 範例中 verbose 引數分成三個囉唆等級,只要在 add_argument() 設定 choices 參數數值範圍就能達成,使用者如果輸入 choices 範圍以外的引數數值、程式將不允許運作。

## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v",
                    "--verbosity",
                    type=int,
                    choices=[0, 1, 2],
                    help="請輸入囉唆程度")
args = parser.parse_args()
print(f"args.verbosity 的值為:{args.verbosity}")
$ python3 test.py -v 2
args.verbosity 的值為:2

## 不允許脫離 choices 限制的數值範圍
$ python3 test.py -v 3
usage: test.py [-h] [-v {0,1,2}]
test.py: error: argument -v/--verbosity: invalid choice: 3 (choose from 0, 1, 2)

## 使用者可以不輸入此引數,請注意此時引數值預設為 None
## 你可以在 add_argument() 內設定 default 或者 required 參數,做好防呆
$ python3 test.py
args.verbosity 的值為:None

count:用字元數計算等級

add_argument() 可以將 action 設定為 count,用來計算該引數輸入了幾次。

  • 如果是長型態引數,就數整個字串出現次數,例如 --verbose --verbose 計為 2 次
  • 如果是短型態引數,只需要數單破折號 - 後方同個字母出現次數,例如 -vvv 計為 3 次
  • count 也類似於 “flag”,選項引數後方不可以再輸入其他值
## test.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-v",
                    "--verbose",
                    action="count",
                    default=0,
                    help="請輸入囉唆程度")
args = parser.parse_args()
print(f"args.verbose 的值為:{args.verbose}")
$ python3 test.py -v
args.verbose 的值為:1

## 短型態選項引數,用重複同一個字母的次數計算
$ python3 test.py -vv
args.verbose 的值為:2

## 長型態選項引數,是計算整個選項引數字串出現次數
$ python3 test.py --verbose --verbose --verbose
args.verbose 的值為:3

## default 參數設定了預設值為 0
$ python3 test.py
args.verbose 的值為:0

verbose 四種寫法整理

簡單複習一下我們在選項引數所學。

如果你的命令列引數,要像 Keras 的 fit() 分成三個 verbose 等級,可以有兩種做法:

  • add_argument()choices$ python3 test.py -v 2
  • add_argument()action="count"$ python3 test.py -vvv

如果只需要開與關兩種選項,也有不同寫法:

  • add_argument()action="store_true"$ python3 test.py --verbose
  • 使用 add_mutually_exclusive_group()$ python3 test.py --verbose v.s. $ python3 test.py --quiet

提醒:預設的 help 選項引數

相信你也注意到,你不需要自己設定 -h--help,就可以直接使用這兩個選項引數,這些是 argparse 預設的選項引數,用來給予使用者關於這支程式與引數要求的提示訊息,例如 $ python3 test.py -h

所以,請記得 add_argument() 設定選項引數名稱不可以-h--help,如果使用它們、衝突到這些預設選項引數,程式會出現 conflict error 喔!

寫好 “help” 讓程式更清晰好讀

創造 ArgumentParser() 物件時,在參數設定寫清楚你的程式說明,會讓你的程式在 --help 頁面更清晰好讀喔!

  • prog:程式名稱或標題
  • description:在 --help 頁面標題與命令列引數說明之間加上敘述,通常用於介紹程式功能
  • epilog:在 --help 頁面最後面加上敘述

此外,在 add_argument() 裡面的 help 參數,也是用來添加顯示在 --help 頁面的資訊,建議好好填寫、讓使用者清楚了解你的命令列引數各自的功能與用法。

## test.py
import argparse
parser = argparse.ArgumentParser(
    prog="我的程式",
    description="這是用來介紹程式功能的地方",
    epilog="這是寫在 help 頁面最後面的句子")
parser.add_argument("-v",
                    "--verbose",
                    action="store_true",
                    help="簡單開關的引數")
args = parser.parse_args()
print(f"args.verbose 的值為:{args.verbose}")
## 加上 --help 頁面的敘述,對程式執行內容沒有影響
$ python3 test.py --verbose
args.verbose 的值為:True

## 現在 --help 頁面更豐富了
$ python3 test.py -h
usage: 我的程式 [-h] [-v]

這是用來介紹程式功能的地方

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose  簡單開關的引數

這是寫在 help 頁面最後面的句子


後記:為何不用 optparse?

如果你曾嘗試處理過命令列引數,或許已經碰過 getopt 或 optparse 函式庫,文章開頭提到的 youtube-dl 專案 事實上也採用 optparse 來處理命令列引數(範例)。那 argparse 到底跟這些函式庫的功能有什麼不一樣呢?PEP 389 解釋了這個問題,差異包括:

  • getopt 與 optparse 只支援選項引數,不支援位置引數;argparse 兩者都支援
  • optparse 不支援必備引數(Required Option);argparse 有支援
  • argparse 可設定「可變」引數數量,例如:narg='+' 代表引數可以是一個或多個

根據 PEP 389 的說明,argparse 包含了 optparse 所有的功能,未來 optparse 也不會繼續開發與維護,所以建議各位開發者越早開始使用 argparse 越好囉!

結語

這則筆記提到的都是筆者好豪自己在 argparse 常用的基本技巧,熟練這些技巧就足以應付大部分命令列引數需要的功能、程式碼也比陽春的 sys.argv 整潔好讀得多!如果你還想追求更進階的命令列引數設定,就到 官方文件 繼續鑽研吧。

(補充)網友分享 其他值得學習的命令列引數管理套件:

  • click
  • 由 FastAPI 作者開發、以 click 為基礎的 typer
  • Google 開發的 fire

你正在學習 Python 的標準函式庫嗎?我曾寫過 pathlib 函式庫教學文章、對檔案路徑處理相當實用,推薦你閱讀。或者你可以參考我整理的 《Python 神乎其技》免費教學文章,學會更多 Pythonic Code!


如果這篇文章有幫助到你,歡迎追蹤 好豪的粉絲專頁,我會持續分享 Python 技巧、資料科學等知識;也歡迎點選下方按鈕將本文加入書籤、或者分享給更多正在學 Python 的朋友。

推薦閱讀