個人工商時間:歡迎訂閱我的每週電子報,我將會分享資料科學跟 AI 工具,也寫下我正在看什麼、學什麼,想到什麼就寫,讓我們透過 Email 聊聊吧!
當你在命令列呼叫 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 的三個基本步驟包括:
- 先創造
argparse.ArgumentParser()
物件,它是我們管理引數需要的 “parser” add_argument()
告訴 parser 我們需要的命令列引數有哪些- 使用
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 就可以在命令列引數做到!
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 整潔好讀得多!如果你還想追求更進階的命令列引數設定,就到 官方文件 繼續鑽研吧。
(補充)網友分享 其他值得學習的命令列引數管理套件:
你正在學習 Python 的標準函式庫嗎?我曾寫過 pathlib 函式庫教學文章、對檔案路徑處理相當實用,推薦你閱讀。或者你可以參考我整理的 《Python 神乎其技》免費教學文章,學會更多 Pythonic Code!
如果這篇文章有幫助到你,歡迎追蹤 好豪的粉絲專頁 與 Threads 帳號,我會持續分享 Python 技巧、資料科學等知識;也歡迎點選下方按鈕將本文加入書籤、或者分享給更多正在學 Python 的朋友。