這篇文章將教你如何編寫完美的 Python 命令行程序,提高團(tuán)隊(duì)的生產(chǎn)力,讓大家的工作更舒適。
作者 |Yannick Wolff
作為 Python 開發(fā)者,我們經(jīng)常要編寫命令行程序。比如在我的數(shù)據(jù)科學(xué)項(xiàng)目中,我要從命令行運(yùn)行腳本來訓(xùn)練模型,以及計(jì)算算法的準(zhǔn)確率等。
因此,更方便更易用的腳本能夠很好地提高生產(chǎn)力,特別是在有多個(gè)開發(fā)者從事同一個(gè)項(xiàng)目的場(chǎng)合下。
因此,我建議你遵循以下四條規(guī)則:
盡可能提供默認(rèn)參數(shù)值
所有錯(cuò)誤情況必須處理(例如,參數(shù)缺失,類型錯(cuò)誤,找不到文件)
所有參數(shù)和選項(xiàng)必須有文檔
不是立即完成的任務(wù)應(yīng)當(dāng)顯示進(jìn)度條
舉個(gè)簡(jiǎn)單的例子
我們把這些規(guī)則應(yīng)用到一個(gè)具體的例子上。這個(gè)腳本可以使用凱撒加密法加密和解密消息。
假設(shè)已經(jīng)有個(gè)寫好的 encrypt 函數(shù)(實(shí)現(xiàn)如下),我們需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的腳本,用來加密和解密消息。我們希望讓用戶通過命令行參數(shù)選擇加密模式(默認(rèn))和解密模式,并選擇一個(gè)秘鑰(默認(rèn)為 1)。
defencrypt(plaintext,key):cyphertext=''forcharacterinplaintext:ifcharacter.isalpha():number=ord(character)number+=keyifcharacter.isupper():ifnumber>ord('Z'):number-=26elifnumberord('z'):number-=26elifnumber
我們的腳本需要做的第一件事就是獲取命令行參數(shù)的值。當(dāng)我搜索“python command line arguments”時(shí),出現(xiàn)的第一個(gè)結(jié)果是關(guān)于sys.argv的,所以我們來試試這個(gè)方法……
“初學(xué)者”的方法
sys.argv 是個(gè)列表,包含用戶在運(yùn)行腳本時(shí)輸入的所有參數(shù)(包括腳本名自身)。
例如,如果我輸入:
>pythoncaesar_script.py--key23--decryptmysecretmessagepbvhfuhwphvvdjh
該列表將包含:
['caesar_script.py','--key','23','--decrypt','my','secret','message']
因此只需遍歷該參數(shù)列表,找到'--key'(或'-k')以得到秘鑰值,找到'--decrypt'以設(shè)置解密模式(實(shí)際上只需要使用秘鑰的反轉(zhuǎn)作為秘鑰即可)。
最后我們的腳本大致如下:
importsysfromcaesar_encryptionimportencryptdefcaesar():key=1is_error=Falseforindex,arginenumerate(sys.argv):ifargin['--key','-k']andlen(sys.argv)>index+1:key=int(sys.argv[index+1])delsys.argv[index]delsys.argv[index]breakforindex,arginenumerate(sys.argv):ifargin['--encrypt','-e']:delsys.argv[index]breakifargin['--decrypt','-d']:key=-keydelsys.argv[index]breakiflen(sys.argv)==1:is_error=Trueelse:forarginsys.argv:ifarg.startswith('-'):is_error=Trueifis_error:print(f'Usage:python{sys.argv[0]}[--key
這個(gè)腳本遵循了一些我們前面推薦的規(guī)則:
支持默認(rèn)秘鑰和默認(rèn)模式
基本的錯(cuò)誤處理(沒有提供輸入文本的情況,以及提供了無法識(shí)別的參數(shù)的情況)
出錯(cuò)時(shí)或者不帶任何參數(shù)調(diào)用腳本時(shí)會(huì)顯示文檔:
>pythoncaesar_script_using_sys_argv.pyUsage:pythoncaesar.py[--key
但是,這個(gè)凱撒加密法腳本太長(zhǎng)了(39 行,其中甚至還沒包括加密代碼本身),而且很難讀懂。
解析命令行參數(shù)應(yīng)該還有更好的辦法……
試試 argparse?
argparse 是 Python 用來解析命令行參數(shù)的標(biāo)準(zhǔn)庫(kù)。
我們來看看用 argparse 怎樣編寫凱撒加密的腳本:
importargparsefromcaesar_encryptionimportencryptdefcaesar():parser=argparse.ArgumentParser()group=parser.add_mutually_exclusive_group()group.add_argument('-e','--encrypt',action='store_true')group.add_argument('-d','--decrypt',action='store_true')parser.add_argument('text',nargs='*')parser.add_argument('-k','--key',type=int,default=1)args=parser.parse_args()text_string=''.join(args.text)key=args.keyifargs.decrypt:key=-keycyphertext=encrypt(text_string,key)print(cyphertext)if__name__=='__main__':caesar()
這段代碼也遵循了上述規(guī)則,而且與前面的手工編寫的腳本相比,可以提供更準(zhǔn)確的文檔,以及更具有交互性的錯(cuò)誤處理:
>pythoncaesar_script_using_argparse.py--encodeMymessageusage:caesar_script_using_argparse.py[-h][-e|-d][-kKEY][text[text...]]caesar_script_using_argparse.py:error:unrecognizedarguments:--encode>pythoncaesar_script_using_argparse.py--helpusage:caesar_script_using_argparse.py[-h][-e|-d][-kKEY][text[text...]]positionalarguments:textoptionalarguments:-h,--helpshowthishelpmessageandexit-e,--encrypt-d,--decrypt-kKEY,--keyKEY
但是,仔細(xì)看了這段代碼后,我發(fā)現(xiàn)(雖然有點(diǎn)主觀)函數(shù)開頭的幾行(從7行到13行)定義了參數(shù),但定義方式并不太優(yōu)雅:它太臃腫了,而且完全是程式化的。應(yīng)該有更描述性、更簡(jiǎn)潔的方法。
click 能做得更好!
幸運(yùn)的是,有個(gè) Python 庫(kù)能提供與 argparse 同樣的功能(甚至還能提供更多),它的代碼風(fēng)格更優(yōu)雅。這個(gè)庫(kù)的名字叫 click。
這里是凱撒加密腳本的第三版,使用了 click:
importclickfromcaesar_encryptionimportencrypt@click.command()@click.argument('text',nargs=-1)@click.option('--decrypt/--encrypt','-d/-e')@click.option('--key','-k',default=1)defcaesar(text,decrypt,key):text_string=''.join(text)ifdecrypt:key=-keycyphertext=encrypt(text_string,key)click.echo(cyphertext)if__name__=='__main__':caesar()
注意現(xiàn)在參數(shù)和選項(xiàng)都在修飾器里定義,定義好的參數(shù)直接作為函數(shù)參數(shù)提供。
我來解釋一下上面代碼中的一些地方:
腳本參數(shù)定義中的nargs參數(shù)指定了該參數(shù)期待的單詞的數(shù)目(一個(gè)用引號(hào)括起來的字符串算一個(gè)單詞)。默認(rèn)值是1。這里nargs=-1允許接收任意數(shù)目的單詞。
--encrypt/--decrypt這種寫法可以定義完全互斥的選項(xiàng)(類似于argparse中的add_mutually_exclusive_group函數(shù)),它將產(chǎn)生一個(gè)布爾型參數(shù)。
click.echo是該庫(kù)提供的一個(gè)工具函數(shù),它的功能與print相同,但兼容Python 2和Python 3,還有一些其他功能(如處理顏色等)。
添加一些隱秘性
這個(gè)腳本的參數(shù)(被加密的消息)應(yīng)當(dāng)是最高機(jī)密。而我們卻要求用戶直接在終端里輸入文本,使得這些文本被記錄在命令歷史中,這不是很諷刺嗎?
解決方法之一就是使用隱藏的提示?;蛘呖梢詮妮斎胛募凶x取文本,對(duì)于較長(zhǎng)的文本來說更實(shí)際一些?;蛘呖梢愿纱嘧層脩暨x擇。
輸出也一樣:用戶可以保存到文件中,也可以輸出到終端。這樣就得到了凱撒腳本的最后一個(gè)版本:
importclickfromcaesar_encryptionimportencrypt@click.command()@click.option('--input_file',type=click.File('r'),help='Fileinwhichthereisthetextyouwanttoencrypt/decrypt.''Ifnotprovided,apromptwillallowyoutotypetheinputtext.',)@click.option('--output_file',type=click.File('w'),help='Fileinwhichtheencrypted/decryptedtextwillbewritten.''Ifnotprovided,theoutputtextwilljustbeprinted.',)@click.option('--decrypt/--encrypt','-d/-e',help='Whetheryouwanttoencrypttheinputtextordecryptit.')@click.option('--key','-k',default=1,help='Thenumerickeytouseforthecaesarencryption/decryption.')defcaesar(input_file,output_file,decrypt,key):ifinput_file:text=input_file.read()else:text=click.prompt('Enteratext',hide_input=notdecrypt)ifdecrypt:key=-keycyphertext=encrypt(text,key)ifoutput_file:output_file.write(cyphertext)else:click.echo(cyphertext)if__name__=='__main__':caesar()
這個(gè)版本有什么新東西嗎?
首先,注意到我給每個(gè)參數(shù)選項(xiàng)都加了個(gè)help參數(shù)。由于腳本變得復(fù)雜了,help參數(shù)可以給腳本的行為添加一些文檔。運(yùn)行結(jié)果如下:
>pythoncaesar_script_v2.py--helpUsage:caesar_script_v2.py[OPTIONS]Options:--input_fileFILENAMEFileinwhichthereisthetextyouwanttoencrypt/decrypt.Ifnotprovided,apromptwillallowyoutotypetheinputtext.--output_fileFILENAMEFileinwhichtheencrypted/decryptedtextwillbewritten.Ifnotprovided,theoutputtextwilljustbeprinted.-d,--decrypt/-e,--encryptWhetheryouwanttoencrypttheinputtextordecryptit.-k,--keyINTEGERThenumerickeytouseforthecaesarencryption/decryption.--helpShowthismessageandexit.
兩個(gè)新的參數(shù):input_file 和 output_file,類型均為 click.File。該庫(kù)能夠用正確的模式打開文件,處理可能的錯(cuò)誤,再執(zhí)行函數(shù)。例如:
>pythoncaesar_script_v2.py--decrypt--input_filewrong_file.txtUsage:caesar_script_v2.py[OPTIONS]Error:Invalidvaluefor"--input_file":Couldnotopenfile:wrong_file.txt:Nosuchfileordirectory
正像help文本中解釋的那樣,如果沒有提供input_file,就使用click.promp讓用戶直接在提示符下輸入文本,在加密模式下這些文本是隱藏的。如下所示:
>pythoncaesar_script_v2.py--encrypt--key2Enteratext:**************yyy.ukectc.eqo
破解密文!
現(xiàn)在設(shè)想你是個(gè)黑客:你要解密一個(gè)用凱撒加密過的密文,但你不知道秘鑰是什么。
最簡(jiǎn)單的策略就是用所有可能的秘鑰調(diào)用解密函數(shù) 25 次,閱讀解密結(jié)果,看看哪個(gè)是合理的。
但你很聰明,而且也很懶,所以你想讓整個(gè)過程自動(dòng)化。確定解密后的 25 個(gè)文本哪個(gè)最可能是原始文本的方法之一,就是統(tǒng)計(jì)所有這些文本中的英文單詞的個(gè)數(shù)。這可以使用 PyEnchant 模塊實(shí)現(xiàn):
importclickimportenchantfromcaesar_encryptionimportencrypt@click.command()@click.option('--input_file',type=click.File('r'),required=True,)@click.option('--output_file',type=click.File('w'),required=True,)defcaesar_breaker(input_file,output_file):cyphertext=input_file.read()english_dictionnary=enchant.Dict("en_US")max_number_of_english_words=0forkeyinrange(26):plaintext=encrypt(cyphertext,-key)number_of_english_words=0forwordinplaintext.split(''):ifwordandenglish_dictionnary.check(word):number_of_english_words+=1ifnumber_of_english_words>max_number_of_english_words:max_number_of_english_words=number_of_english_wordsbest_plaintext=plaintextbest_key=keyclick.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext: {best_plaintext[:1000]}...')output_file.write(best_plaintext)if__name__=='__main__':caesar_breaker()
貌似運(yùn)行得很不錯(cuò),但別忘了,好的命令行程序還有個(gè)規(guī)則需要遵守:
4.A 不是立即完成的任務(wù)應(yīng)當(dāng)顯示進(jìn)度條。
示例中的文本包含10^4個(gè)單詞,因此該腳本需要大約5秒才能解密。這很正常,因?yàn)樗枰獧z查所有25個(gè)秘鑰,每個(gè)秘鑰都要檢查10^4個(gè)單詞是否出現(xiàn)在英文字典中。
假設(shè)你要解密的文本包括10^5個(gè)但I(xiàn)C,那么就要花費(fèi)50秒才能輸出結(jié)果,用戶可能會(huì)非常著急。
因此我建議這種任務(wù)一定要顯示進(jìn)度條。特別是,顯示進(jìn)度條還非常容易實(shí)現(xiàn)。
下面是個(gè)顯示進(jìn)度條的例子:
importclickimportenchantfromtqdmimporttqdmfromcaesar_encryptionimportencrypt@click.command()@click.option('--input_file',type=click.File('r'),required=True,)@click.option('--output_file',type=click.File('w'),required=True,)defcaesar_breaker(input_file,output_file):cyphertext=input_file.read()english_dictionnary=enchant.Dict("en_US")best_number_of_english_words=0forkeyintqdm(range(26)):plaintext=encrypt(cyphertext,-key)number_of_english_words=0forwordinplaintext.split(''):ifwordandenglish_dictionnary.check(word):number_of_english_words+=1ifnumber_of_english_words>best_number_of_english_words:best_number_of_english_words=number_of_english_wordsbest_plaintext=plaintextbest_key=keyclick.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext: {best_plaintext[:1000]}...')output_file.write(best_plaintext)if__name__=='__main__':caesar_breaker()
你發(fā)現(xiàn)區(qū)別了嗎?可能不太好找,因?yàn)閰^(qū)別真的很小,只有四個(gè)字母:tqdm。
tqdm 是 Python 庫(kù)的名字,也是它包含的類的名字。只需用它包裹一個(gè)可迭代的東西,就能顯示出進(jìn)度條:
forkeyintqdm(range(26)):
這樣就能顯示出非常漂亮的進(jìn)度條。我都不敢相信這是真的。
另外,click也提供類似的顯示進(jìn)度條的工具(click.progress_bar),但我覺得它的外觀不太容易懂,而且要寫的代碼也多一些。
我希望這篇文章能讓你在改進(jìn)開發(fā)者的體驗(yàn)上多花點(diǎn)時(shí)間。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4365瀏覽量
63913 -
python
+關(guān)注
關(guān)注
56文章
4822瀏覽量
85900 -
數(shù)據(jù)科學(xué)
+關(guān)注
關(guān)注
0文章
168瀏覽量
10360
原文標(biāo)題:如何編寫完美的 Python 命令行程序?
文章出處:【微信號(hào):mcuworld,微信公眾號(hào):嵌入式資訊精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
用于分析可執(zhí)行程序和內(nèi)存轉(zhuǎn)儲(chǔ)的命令行工具介紹

caxa命令行中的應(yīng)用
如何在Linux命令行中運(yùn)行Python腳本
Fcoder從命令行批量轉(zhuǎn)換為TIFF

評(píng)論