尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
作者 | Yannick Wolff
譯者 | 劉旭坤
整理 | Jane
出品 | Python大本營
對 Python 程序來說,完備的命令行界面可以提升團隊的工作效率,減少調用時可能碰到的困擾。今天,我們就來教大家如何設計功能完整的 Python 命令行界面。
對 Python 開發者來說用的最多的界面恐怕還是命令行。就拿我參與的機器學習項目來說,訓練模型和評估算法的精確度都是通過在命令行界面運行腳本來完成的。
所以調用一個 Python 腳本的時候我們希望這段腳本有一個盡量簡潔方便調用的接口。尤其是團隊中有多名開發者的時候這一點對提升團隊的工作效率很重要。
要讓一段腳本方便調用總的來說有四個原則需要遵守:
-
提供默認參數值
-
處理調用出錯的情況,比如缺少參數、參數類型錯誤或者找不到文件等
-
在文檔中說明各個參數和選項的用法
-
如果執行時間較長應該提供進度條
一個簡單的例子
下面我們先通過一個簡單的例子來談談這四個原則的具體應用。例子中給出的腳本的功能是使用凱撒碼變換對文本進行加密和解密。
Caesar cipher:一種簡單的消息編碼方式。在密碼學中,凱撒密碼,移位密碼是最簡單和最廣為人知的加密技術之一。
比如說我們想讓用戶通過命令行參數來選擇調用的方式是加密還是解密文本,而且用戶要從命令行傳入下面 encrypt 函數中的密匙參數 key。
1defencrypt(plaintext,key): 2cyphertext='' 3forcharacterinplaintext: 4ifcharacter.isalpha(): 5number=ord(character) 6number+=key 7ifcharacter.isupper(): 8ifnumber>ord('Z'): 9number-=2610elifnumber<ord('A'):11number+=2612elifcharacter.islower():13ifnumber>ord('z'):14number-=2615elifnumber<ord('a'):16number+=2617character=chr(number)18cyphertext+=character1920returncyphertext
首先我們得在程序中拿到命令行參數。我在網上搜「 python 命令行參數」出來的第一個結果說讓我用 sys.argv ,那我們就來試試看它好不好用。
初級:笨辦法
其實 sys.argv 只是一個 list ,這個 list 的內容是用戶調用腳本時所輸入的所有參數(其中也包括腳本的文件名)。
如果我像下面這樣調用加解密的腳本 caesar_script.py 的話:
1>pythoncaesar_script.py--key23--decryptmysecretmessage2pbvhfuhwphvvdjh
sys.argv 這個 list 的值就是:
1['caesar_script.py','--key','23','--decrypt','my','secret','message']
所以我們現在要遍歷這個 list 來找其中是否包括了「 –key 」或者「 -k 」,這樣我們就能找到密匙「 23 」。再找到「 –decrypt 」就能知道用戶是想要解密一段文本了(其實解密就是用密匙的相反數再加密一次)。
完成後的代碼如下:
1importsys 2 3fromcaesar_encryptionimportencrypt 4 5 6defcaesar(): 7key=1 8is_error=False 910forindex,arginenumerate(sys.argv):11ifargin['--key','-k']andlen(sys.argv)>index+1:12key=int(sys.argv[index+1])13delsys.argv[index]14delsys.argv[index]15break1617forindex,arginenumerate(sys.argv):18ifargin['--encrypt','-e']:19delsys.argv[index]20break21ifargin['--decrypt','-d']:22key=-key23delsys.argv[index]24break2526iflen(sys.argv)==1:27is_error=True28else:29forarginsys.argv:30ifarg.startswith('-'):31is_error=True3233ifis_error:34print(f'Usage:python{sys.argv[0]}[--key<key>][--encrypt|decrypt]<text>')35else:36print(encrypt(''.join(sys.argv[1:]),key))3738if__name__=='__main__':39caesar()
這段代碼基本上遵守了我們提到的四個原則:
-
key 和 加密模式都設置了缺省參數
-
腳本可以處理像沒有文本或者缺少參數這樣比較基本的錯誤
-
用戶沒有給參數或者有錯的話會顯示使用幫助
1>pythoncaesar_script_using_sys_argv.py2Usage:pythoncaesar.py[--key<key>][--encrypt|decrypt]<text>
然而不算加密函數光處理參數我們就已經寫了 39 行而且寫得一點也不優雅。我有膽說肯定還有更好的辦法來讀命令行參數。
中級:argparse
Python 標準庫里面提供了一個讀取命令行參數的庫——argparse 。我們來看看如果用 argparse 代碼怎麼寫:
1importargparse 2 3fromcaesar_encryptionimportencrypt 4 5 6defcaesar(): 7parser=argparse.ArgumentParser() 8group=parser.add_mutually_exclusive_group() 9group.add_argument('-e','--encrypt',action='store_true')10group.add_argument('-d','--decrypt',action='store_true')11parser.add_argument('text',nargs='*')12parser.add_argument('-k','--key',type=int,default=1)13args=parser.parse_args()1415text_string=''.join(args.text)16key=args.key17ifargs.decrypt:18key=-key19cyphertext=encrypt(text_string,key)20print(cyphertext)2122if__name__=='__main__':23caesar()24viewraw
這樣寫也符合四項指導原則,而且對參數的說明和錯誤處理都優於使用 sys.argv 的笨辦法:
1>pythoncaesar_script_using_argparse.py--encodeMymessage 2 3usage:caesar_script_using_argparse.py[-h][-e|-d][-kKEY][text[text...]] 4caesar_script_using_argparse.py:error:unrecognizedarguments:--encode 5>pythoncaesar_script_using_argparse.py--help 6 7usage:caesar_script_using_argparse.py[-h][-e|-d][-kKEY][text[text...]] 8positionalarguments: 9text10optionalarguments:11-h,--helpshowthishelpmessageandexit12-e,--encrypt13-d,--decrypt14-kKEY,--keyKEY
不過我個人還是覺得代碼里第 7 行到第 13 行定義參數的部分寫得很囉嗦,而且我覺得參數應該使用聲明式的方法來定義。
高級: click
還有一個叫 click 的庫能做到我們想要的這些。它的基本功能和 argparse 是一樣的,但寫出來的代碼更優雅。
使用 click 改寫我們的加解密腳本之後是這樣的:
1importclick 2 3fromcaesar_encryptionimportencrypt 4 [email protected]() [email protected]('text',nargs=-1) [email protected]('--decrypt/--encrypt','-d/-e') [email protected]('--key','-k',default=1) 9defcaesar(text,decrypt,key):10text_string=''.join(text)11ifdecrypt:12key=-key13cyphertext=encrypt(text_string,key)14click.echo(cyphertext)1516if__name__=='__main__':17caesar()18viewraw
我們需要的參數和選項都用裝飾器來聲明,這樣就可以在 caesar 函數里直接使用了。
上面的代碼里有幾點需要說明:
-
nargs 參數是說這個參數的長度是幾個詞。默認值是 1 不過用引號引起來的句子也只算一個詞。這里我們設為 -1 是指不限制長度。
-
–decrypt/–encrypt 這樣加一個斜杠的寫法用來指明互斥的選項,它的功能和 argparse 中的 add_mutually_exclusive_group 函數類似。
-
click.echo 是 click 提供的一個 print 功能,與 Python 2 和 3 都兼容,而且有顏色高亮功能。
添加隱私功能
我們寫的是一個對文本加解密的腳本,但用戶卻直接把要加密的文本打出來了,這樣有別人用這個命令行的話按幾下上方向鍵就能看到我們的用戶加密了什麼東西,這是在是有點荒唐。
我們可以選擇把用戶要加密的文本隱藏起來,或者是從文件里讀文本。這兩種方法都能解決我們的問題,但選擇權應該留給用戶。
同理對於加解密的結果我們也讓用戶選擇是直接在命令行輸出還是保存成一個文件:
1importclick 2 3fromcaesar_encryptionimportencrypt 4 [email protected]() [email protected]( 7'--input_file', 8type=click.File('r'), 9help='Fileinwhichthereisthetextyouwanttoencrypt/decrypt.'10'Ifnotprovided,apromptwillallowyoutotypetheinputtext.',11)[email protected](13'--output_file',14type=click.File('w'),15help='Fileinwhichtheencrypted/decryptedtextwillbewritten.'16'Ifnotprovided,theoutputtextwilljustbeprinted.',17)[email protected](19'--decrypt/--encrypt',20'-d/-e',21help='Whetheryouwanttoencrypttheinputtextordecryptit.'22)[email protected](24'--key',25'-k',26default=1,27help='Thenumerickeytouseforthecaesarencryption/decryption.'28)29defcaesar(input_file,output_file,decrypt,key):30ifinput_file:31text=input_file.read()32else:33text=click.prompt('Enteratext',hide_input=notdecrypt)34ifdecrypt:35key=-key36cyphertext=encrypt(text,key)37ifoutput_file:38output_file.write(cyphertext)39else:40click.echo(cyphertext)4142if__name__=='__main__':43caesar()44viewraw
這里我給每個參數和選項都加上了一小段說明,這樣我們的文檔能更清楚一點因為我們現在參數有點多了。現在的文檔是這樣的:
1>pythoncaesar_script_v2.py--help2Usage:caesar_script_v2.py[OPTIONS]3Options:4--input_fileFILENAMEFileinwhichthereisthetextyouwanttoencrypt/decrypt.Ifnotprovided,apromptwillallowyoutotypetheinputtext.5--output_fileFILENAMEFileinwhichtheencrypted/decryptedtextwillbewritten.Ifnotprovided,theoutputtextwilljustbeprinted.6-d,--decrypt/-e,--encryptWhetheryouwanttoencrypttheinputtextordecryptit.7-k,--keyINTEGERThenumerickeytouseforthecaesarencryption/decryption.8--helpShowthismessageandexit.
兩個新的參數 input_file 和 output_file 都是 click.File 類型,而且 click 幫我們處理了文件打開的讀寫方式和可能出現的錯誤,比如這樣:
1>pythoncaesar_script_v2.py--decrypt--input_filewrong_file.txt2Usage:caesar_script_v2.py[OPTIONS]3Error:Invalidvaluefor"--input_file":Couldnotopenfile:wrong_file.txt:Nosuchfileordirectory
如果用戶沒有提供 input_file 的話,如說明文檔中所寫,則會讓用戶在命令行進行輸入,而且用戶輸入不再是明文了:
1>pythoncaesar_script_v2.py--encrypt--key22Enteratext:**************3yyy.ukectc.eqo
破譯密碼
假設我們現在是黑客,想解密但是不知道密匙該怎麼辦呢?對凱撒加密的英文來說很容易,只要調用解密函數 25 次然後看看那個結果不是亂碼就行了。
要調用 25 次還要一個一個看還是太麻煩,其實只要數數哪個結果里正確的英文詞最多就行了。下面我們就用 PyEnchant 來做到自動破譯密碼:
1importclick 2importenchant 3 4fromcaesar_encryptionimportencrypt 5 [email protected]() [email protected]( 8'--input_file', 9type=click.File('r'),10required=True,11)[email protected](13'--output_file',14type=click.File('w'),15required=True,16)17defcaesar_breaker(input_file,output_file):18cyphertext=input_file.read()19english_dictionnary=enchant.Dict("en_US")20max_number_of_english_words=021forkeyinrange(26):22plaintext=encrypt(cyphertext,-key)23number_of_english_words=024forwordinplaintext.split(''):25ifwordandenglish_dictionnary.check(word):26number_of_english_words+=127ifnumber_of_english_words>max_number_of_english_words:28max_number_of_english_words=number_of_english_words29best_plaintext=plaintext30best_key=key31click.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext:\n\n{best_plaintext[:1000]}...')32output_file.write(best_plaintext)3334if__name__=='__main__':35caesar_breaker()36viewraw
一氣呵成!
不過我們好像還沒有提到四項原則的最後一點:
4.如果執行時間較長應該提供進度條
上面的腳本破譯 104 個詞的文本大約需要 5 秒。考慮到要遍歷 25 個密匙還要數英文詞的個數這個時間並不算慢。
不過文本再長的話,比如 105 個詞的文本,就要花 50 秒。這就有點長了,用戶可能沒有耐心等到程序運行完就強退了。
所以我建議如果執行時間長的話最好加上進度條,關鍵是寫起來非常簡單:
1importclick 2importenchant 3 4fromtqdmimporttqdm 5 6fromcaesar_encryptionimportencrypt 7 [email protected]() [email protected](10'--input_file',11type=click.File('r'),12required=True,13)[email protected](15'--output_file',16type=click.File('w'),17required=True,18)19defcaesar_breaker(input_file,output_file):20cyphertext=input_file.read()21english_dictionnary=enchant.Dict("en_US")22best_number_of_english_words=023forkeyintqdm(range(26)):24plaintext=encrypt(cyphertext,-key)25number_of_english_words=026forwordinplaintext.split(''):27ifwordandenglish_dictionnary.check(word):28number_of_english_words+=129ifnumber_of_english_words>best_number_of_english_words:30best_number_of_english_words=number_of_english_words31best_plaintext=plaintext32best_key=key33click.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext:\n\n{best_plaintext[:1000]}...')34output_file.write(best_plaintext)3536if__name__=='__main__':37caesar_breaker()38viewraw
不仔細看的話可能都看不出有什麼區別,因為區別只有四個字母 tqdm ,阿拉伯語中 tqdm 是進度的意思。
tqdm 庫的用法非常簡單,只要把代碼中的迭代器用 tqdm 括起來就行了:
1forkeyintqdm(range(26)):
這樣就會在命令行輸出一個進度條,簡單得讓人不敢相信。
其實 click 的 click.progress_bar 也有類似的功能,但我覺得 click 的進度條不好看而且寫法比tqdm 稍微麻煩一點。
總結一下希望大家讀完這篇文章能把設計 Python 命令行的這幾個原則用到實踐中去寫出更好用的 Python 命令行。
原文鏈接:
https://blog.sicara.com/perfect-python-command-line-interfaces-7d5d4efad6a2
(*本文由Python大本營整理,轉載請聯繫微信1092722531)
福利
掃碼添加小助手微信,回復:1,入群獲取Python電子書(附代碼~~)
推薦閱讀: