作者 | 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-=26
10elifnumber
11number+=26
12elifcharacter.islower():
13ifnumber>ord('z'):
14number-=26
15elifnumber
16number+=26
17character=chr(number)
18cyphertext+=character
19
20returncyphertext
首先我们得在程序中拿到命令行参数。我在网上搜“ python 命令行参数”出来的第一个结果说让我用sys.argv,那我们就来试试看它好不好用。
初级:笨办法
其实sys.argv只是一个 list ,这个 list 的内容是用户调用脚本时所输入的所有参数(其中也包括脚本的文件名)。
如果我像下面这样调用加解密的脚本 caesar_script.py 的话:
1>pythoncaesar_script.py--key23--decryptmysecretmessage
2pbvhfuhwphvvdjh
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
9
10forindex,arginenumerate(sys.argv):
11ifargin['--key','-k']andlen(sys.argv)>index+1:
12key=int(sys.argv[index+1])
13delsys.argv[index]
14delsys.argv[index]
15break
16
17forindex,arginenumerate(sys.argv):
18ifargin['--encrypt','-e']:
19delsys.argv[index]
20break
21ifargin['--decrypt','-d']:
22key=-key
23delsys.argv[index]
24break
25
26iflen(sys.argv)==1:
27is_error=True
28else:
29forarginsys.argv:
30ifarg.startswith('-'):
31is_error=True
32
33ifis_error:
34print(f'Usage:python{sys.argv[0]}[--key][--encrypt|decrypt]')
35else:
36print(encrypt(''.join(sys.argv[1:]),key))
37
38if__name__=='__main__':
39caesar()
这段代码基本上遵守了我们提到的四个原则:
key 和 加密模式都设置了缺省参数
脚本可以处理像没有文本或者缺少参数这样比较基本的错误
用户没有给参数或者有错的话会显示使用帮助
1>pythoncaesar_script_using_sys_argv.py
2Usage:pythoncaesar.py[--key][--encrypt|decrypt]
然而不算加密函数光处理参数我们就已经写了 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()
14
15text_string=''.join(args.text)
16key=args.key
17ifargs.decrypt:
18key=-key
19cyphertext=encrypt(text_string,key)
20print(cyphertext)
21
22if__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:
9text
10optionalarguments:
11-h,--helpshowthishelpmessageandexit
12-e,--encrypt
13-d,--decrypt
14-kKEY,--keyKEY
不过我个人还是觉得代码里第 7 行到第 13 行定义参数的部分写得很啰嗦,而且我觉得参数应该使用声明式的方法来定义。
高级: click
还有一个叫 click 的库能实现我们想要的这些。它的基本功能和 argparse 是一样的,但写出来的代码更优雅。
使用 click 改写我们的加解密脚本之后是这样的:
1importclick
2
3fromcaesar_encryptionimportencrypt
4
5@click.command()
6@click.argument('text',nargs=-1)
7@click.option('--decrypt/--encrypt','-d/-e')
8@click.option('--key','-k',default=1)
9defcaesar(text,decrypt,key):
10text_string=''.join(text)
11ifdecrypt:
12key=-key
13cyphertext=encrypt(text_string,key)
14click.echo(cyphertext)
15
16if__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
5@click.command()
6@click.option(
7'--input_file',
8type=click.File('r'),
9help='Fileinwhichthereisthetextyouwanttoencrypt/decrypt.'
10'Ifnotprovided,apromptwillallowyoutotypetheinputtext.',
11)
12@click.option(
13'--output_file',
14type=click.File('w'),
15help='Fileinwhichtheencrypted/decryptedtextwillbewritten.'
16'Ifnotprovided,theoutputtextwilljustbeprinted.',
17)
18@click.option(
19'--decrypt/--encrypt',
20'-d/-e',
21help='Whetheryouwanttoencrypttheinputtextordecryptit.'
22)
23@click.option(
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=-key
36cyphertext=encrypt(text,key)
37ifoutput_file:
38output_file.write(cyphertext)
39else:
40click.echo(cyphertext)
41
42if__name__=='__main__':
43caesar()
44viewraw
这里我给每个参数和选项都加上了一小段说明,这样我们的文档能更清楚一点因为我们现在参数有点多了。现在的文档是这样的:
1>pythoncaesar_script_v2.py--help
2Usage: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.txt
2Usage:caesar_script_v2.py[OPTIONS]
3Error:Invalidvaluefor"--input_file":Couldnotopenfile:wrong_file.txt:Nosuchfileordirectory
如果用户没有提供 input_file 的话,如说明文档中所写,则会让用户在命令行进行输入,而且用户输入不再是明文了:
1>pythoncaesar_script_v2.py--encrypt--key2
2Enteratext:**************
3yyy.ukectc.eqo
破译密码
假设我们现在是黑客,想解密但是不知道密匙该怎么办呢?对凯撒加密的英文来说很容易,只要调用解密函数 25 次然后看看那个结果不是乱码就行了。
要调用 25 次还要一个一个看还是太麻烦,其实只要数数哪个结果里正确的英文词最多就行了。下面我们就用 PyEnchant 来实现自动破译密码:
1importclick
2importenchant
3
4fromcaesar_encryptionimportencrypt
5
6@click.command()
7@click.option(
8'--input_file',
9type=click.File('r'),
10required=True,
11)
12@click.option(
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=0
21forkeyinrange(26):
22plaintext=encrypt(cyphertext,-key)
23number_of_english_words=0
24forwordinplaintext.split(''):
25ifwordandenglish_dictionnary.check(word):
26number_of_english_words+=1
27ifnumber_of_english_words>max_number_of_english_words:
28max_number_of_english_words=number_of_english_words
29best_plaintext=plaintext
30best_key=key
31click.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext:\n\n{best_plaintext[:1000]}...')
32output_file.write(best_plaintext)
33
34if__name__=='__main__':
35caesar_breaker()
36viewraw
一气呵成!
不过我们好像还没有提到四项原则的最后一点:
4.如果执行时间较长应该提供进度条
上面的脚本破译 104 个词的文本大约需要 5 秒。考虑到要遍历 25 个密匙还要数英文词的个数这个时间并不算慢。
不过文本再长的话,比如 105 个词的文本,就要花 50 秒。这就有点长了,用户可能没有耐心等到程序运行完就强退了。
所以我建议如果执行时间长的话最好加上进度条,关键是写起来非常简单:
1importclick
2importenchant
3
4fromtqdmimporttqdm
5
6fromcaesar_encryptionimportencrypt
7
8@click.command()
9@click.option(
10'--input_file',
11type=click.File('r'),
12required=True,
13)
14@click.option(
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=0
23forkeyintqdm(range(26)):
24plaintext=encrypt(cyphertext,-key)
25number_of_english_words=0
26forwordinplaintext.split(''):
27ifwordandenglish_dictionnary.check(word):
28number_of_english_words+=1
29ifnumber_of_english_words>best_number_of_english_words:
30best_number_of_english_words=number_of_english_words
31best_plaintext=plaintext
32best_key=key
33click.echo(f'Themostlikelyencryptionkeyis{best_key}.Itgivesthefollowingplaintext:\n\n{best_plaintext[:1000]}...')
34output_file.write(best_plaintext)
35
36if__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)
热门跟贴