作者 | Yannick Wolff

译者 | 刘旭坤

整理 | Jane

出品 | Python大本营

对 Python 程序来说,完备的命令行界面可以提升团队的工作效率,减少调用时可能碰到的困扰。今天,我们就来教大家如何设计功能完整的 Python 命令行界面。

对 Python 开发者来说用的最多的界面恐怕还是命令行。就拿我参与的机器学习项目来说,训练模型和评估算法的精确度都是通过在命令行界面运行脚本来完成的。

所以调用一个 Python 脚本的时候我们希望这段脚本有一个尽量简洁方便调用的接口。尤其是团队中有多名开发者的时候这一点对提升团队的工作效率很重要。

要让一段脚本方便调用总的来说有四个原则需要遵守:

  1. 提供默认参数值

  2. 处理调用出错的情况,比如缺少参数、参数类型错误或者找不到文件等

  3. 在文档中说明各个参数和选项的用法

  4. 如果执行时间较长应该提供进度条

一个简单的例子

下面我们先通过一个简单的例子来谈谈这四个原则的具体应用。例子中给出的脚本的功能是使用凯撒码变换对文本进行加密和解密。

Caesar cipher:一种简单的消息编码方式。在密码学中,凯撒密码,移位密码是最简单和最广为人知的加密技术之一。

比如说我们想让用户通过命令行参数来选择调用的方式是加密还是解密文本,而且用户要从命令行传入下面 encrypt 函数中的密匙参数 key。

1def encrypt(plaintext, key):
2    cyphertext = ''
3    for character in plaintext:
4        if character.isalpha():
5            number = ord(character)
6            number += key
7            if character.isupper():
8                if number > ord('Z'):
9                    number -= 26
10                elif number < ord('A'):
11                    number += 26
12            elif character.islower():
13                if number > ord('z'):
14                    number -= 26
15                elif number < ord('a'):
16                    number += 26
17            character = chr(number)
18        cyphertext += character
19
20    return cyphertext

首先我们得在程序中拿到命令行参数。我在网上搜“ python 命令行参数”出来的第一个结果说让我用 sys.argv ,那我们就来试试看它好不好用。

初级:笨办法

其实 sys.argv 只是一个 list ,这个 list 的内容是用户调用脚本时所输入的所有参数(其中也包括脚本的文件名)。

如果我像下面这样调用加解密的脚本 caesar_script.py 的话:

1> python caesar_script.py --key 23 --decrypt my secret message
2pb vhfuhw phvvdjh

sys.argv 这个 list 的值就是:

1['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']

所以我们现在要遍历这个 list 来找其中是否包括了“ –key ”或者“ -k ”,这样我们就能找到密匙“ 23 ”。再找到“ –decrypt ”就能知道用户是想要解密一段文本了(其实解密就是用密匙的相反数再加密一次)。

完成后的代码如下:

1import sys
2
3from caesar_encryption import encrypt
4
5
6def caesar():
7    key = 1
8    is_error = False
9
10    for index, arg in enumerate(sys.argv):
11        if arg in ['--key', '-k'] and len(sys.argv) > index + 1:
12            key = int(sys.argv[index + 1])
13            del sys.argv[index]
14            del sys.argv[index]
15            break
16
17    for index, arg in enumerate(sys.argv):
18        if arg in ['--encrypt', '-e']:
19            del sys.argv[index]
20            break
21        if arg in ['--decrypt', '-d']:
22            key = -key
23            del sys.argv[index]
24            break
25
26    if len(sys.argv) == 1:
27        is_error = True
28    else:
29        for arg in sys.argv:
30            if arg.startswith('-'):
31                is_error = True
32
33    if is_error:
34        print(f'Usage: python {sys.argv[0]} [ --key <key> ] [ --encrypt|decrypt ] <text>')
35    else:
36        print(encrypt(' '.join(sys.argv[1:]), key))
37
38if __name__ == '__main__':
39    caesar()

这段代码基本上遵守了我们提到的四个原则:

  1. key 和 加密模式都设置了缺省参数

  2. 脚本可以处理像没有文本或者缺少参数这样比较基本的错误

  3. 用户没有给参数或者有错的话会显示使用帮助

1> python caesar_script_using_sys_argv.py
2Usage: python caesar.py [ --key <key> ] [ --encrypt|decrypt ] <text>

然而不算加密函数光处理参数我们就已经写了 39 行而且写得一点也不优雅。我有胆说肯定还有更好的办法来读命令行参数。

中级:argparse

Python 标准库里面提供了一个读取命令行参数的库——argparse 。我们来看看如果用 argparse 代码怎么写:

1import argparse
2
3from caesar_encryption import encrypt
4
5
6def caesar():
7    parser = argparse.ArgumentParser()
8    group = parser.add_mutually_exclusive_group()
9    group.add_argument('-e', '--encrypt', action='store_true')
10    group.add_argument('-d', '--decrypt', action='store_true')
11    parser.add_argument('text', nargs='*')
12    parser.add_argument('-k', '--key', type=int, default=1)
13    args = parser.parse_args()
14
15    text_string = ' '.join(args.text)
16    key = args.key
17    if args.decrypt:
18        key = -key
19    cyphertext = encrypt(text_string, key)
20    print(cyphertext)
21
22if __name__ == '__main__':
23    caesar()
24view raw

这样写也符合四项指导原则,而且对参数的说明和错误处理都优于使用 sys.argv 的笨办法:

1> python caesar_script_using_argparse.py --encode My message
2
3usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
4caesar_script_using_argparse.py: error: unrecognized arguments: --encode
5> python caesar_script_using_argparse.py --help
6
7usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
8positional arguments:
9  text
10optional arguments:
11  -h, --help         show this help message and exit
12  -e, --encrypt
13  -d, --decrypt
14  -k KEY, --key KEY

不过我个人还是觉得代码里第 7 行到第 13 行定义参数的部分写得很啰嗦,而且我觉得参数应该使用声明式的方法来定义。

高级: click

还有一个叫 click 的库能实现我们想要的这些。它的基本功能和 argparse 是一样的,但写出来的代码更优雅。

使用 click 改写我们的加解密脚本之后是这样的:

1import click
2
3from caesar_encryption import encrypt
4
5@click.command()
6@click.argument('text', nargs=-1)
7@click.option('--decrypt/--encrypt', '-d/-e')
8@click.option('--key', '-k', default=1)
9def caesar(text, decrypt, key):
10    text_string = ' '.join(text)
11    if decrypt:
12        key = -key
13    cyphertext = encrypt(text_string, key)
14    click.echo(cyphertext)
15
16if __name__ == '__main__':
17    caesar()
18view raw

我们需要的参数和选项都用装饰器来声明,这样就可以在 caesar 函数里直接使用了。

上面的代码里有几点需要说明:

  1. nargs 参数是说这个参数的长度是几个词。默认值是 1 不过用引号引起来的句子也只算一个词。这里我们设为 -1 是指不限制长度。

  2. --decrypt/--encrypt 这样加一个斜杠的写法用来指明互斥的选项,它的功能和 argparse 中的  add_mutually_exclusive_group 函数类似。

  3. click.echo  是 click 提供的一个 print 功能,与 Python 2 和 3 都兼容,而且有颜色高亮功能。

添加隐私功能

我们写的是一个对文本加解密的脚本,但用户却直接把要加密的文本打出来了,这样有别人用这个命令行的话按几下上方向键就能看到我们的用户加密了什么东西,这是在是有点荒唐。

我们可以选择把用户要加密的文本隐藏起来,或者是从文件里读文本。这两种方法都能解决我们的问题,但选择权应该留给用户。

同理对于加解密的结果我们也让用户选择是直接在命令行输出还是保存成一个文件:

1import click
2
3from caesar_encryption import encrypt
4
5@click.command()
6@click.option(
7    '--input_file',
8    type=click.File('r'),
9    help='File in which there is the text you want to encrypt/decrypt.'
10         'If not provided, a prompt will allow you to type the input text.',
11)
12@click.option(
13    '--output_file',
14    type=click.File('w'),
15    help='File in which the encrypted / decrypted text will be written.'
16         'If not provided, the output text will just be printed.',
17)
18@click.option(
19    '--decrypt/--encrypt',
20    '-d/-e',
21    help='Whether you want to encrypt the input text or decrypt it.'
22)
23@click.option(
24    '--key',
25    '-k',
26    default=1,
27    help='The numeric key to use for the caesar encryption / decryption.'
28)
29def caesar(input_file, output_file, decrypt, key):
30    if input_file:
31        text = input_file.read()
32    else:
33        text = click.prompt('Enter a text', hide_input=not decrypt)
34    if decrypt:
35        key = -key
36    cyphertext = encrypt(text, key)
37    if output_file:
38        output_file.write(cyphertext)
39    else:
40        click.echo(cyphertext)
41
42if __name__ == '__main__':
43    caesar()
44view raw

这里我给每个参数和选项都加上了一小段说明,这样我们的文档能更清楚一点因为我们现在参数有点多了。现在的文档是这样的:

1> python caesar_script_v2.py --help
2Usage: caesar_script_v2.py [OPTIONS]
3Options:
4  --input_file FILENAME          File in which there is the text you want to encrypt/decrypt. If not provided, a prompt will allow you to type the input text.
5  --output_file FILENAME         File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed.
6  -d, --decrypt / -e, --encrypt  Whether you want to encrypt the input text or decrypt it.
7  -k, --key INTEGER              The numeric key to use for the caesar encryption / decryption.
8  --help                         Show this message and exit.

两个新的参数 input_file 和 output_file 都是 click.File 类型,而且 click 帮我们处理了文件打开的读写方式和可能出现的错误,比如这样:

1> python caesar_script_v2.py --decrypt --input_file wrong_file.txt
2Usage: caesar_script_v2.py [OPTIONS]
3Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory

如果用户没有提供 input_file 的话,如说明文档中所写,则会让用户在命令行进行输入,而且用户输入不再是明文了:

1> python caesar_script_v2.py --encrypt --key 2
2Enter a text: **************
3yyy.ukectc.eqo

破译密码

假设我们现在是黑客,想解密但是不知道密匙该怎么办呢?对凯撒加密的英文来说很容易,只要调用解密函数 25 次然后看看那个结果不是乱码就行了。

要调用 25 次还要一个一个看还是太麻烦,其实只要数数哪个结果里正确的英文词最多就行了。下面我们就用 PyEnchant 来实现自动破译密码:

1import click
2import enchant
3
4from caesar_encryption import encrypt
5
6@click.command()
7@click.option(
8    '--input_file',
9    type=click.File('r'),
10    required=True,
11)
12@click.option(
13    '--output_file',
14    type=click.File('w'),
15    required=True,
16)
17def caesar_breaker(input_file, output_file):
18    cyphertext = input_file.read()
19    english_dictionnary = enchant.Dict("en_US")
20    max_number_of_english_words = 0
21    for key in range(26):
22        plaintext = encrypt(cyphertext, -key)
23        number_of_english_words = 0
24        for word in plaintext.split(' '):
25            if word and english_dictionnary.check(word):
26                number_of_english_words += 1
27        if number_of_english_words > max_number_of_english_words:
28            max_number_of_english_words = number_of_english_words
29            best_plaintext = plaintext
30            best_key = key
31    click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
32    output_file.write(best_plaintext)
33
34if __name__ == '__main__':
35    caesar_breaker()
36view raw

一气呵成!

不过我们好像还没有提到四项原则的最后一点:

4.如果执行时间较长应该提供进度条

上面的脚本破译 104 个词的文本大约需要 5 秒。考虑到要遍历 25 个密匙还要数英文词的个数这个时间并不算慢。

不过文本再长的话,比如 105 个词的文本,就要花 50 秒。这就有点长了,用户可能没有耐心等到程序运行完就强退了。

所以我建议如果执行时间长的话最好加上进度条,关键是写起来非常简单:

1import click
2import enchant
3
4from tqdm import tqdm
5
6from caesar_encryption import encrypt
7
8@click.command()
9@click.option(
10    '--input_file',
11    type=click.File('r'),
12    required=True,
13)
14@click.option(
15    '--output_file',
16    type=click.File('w'),
17    required=True,
18)
19def caesar_breaker(input_file, output_file):
20    cyphertext = input_file.read()
21    english_dictionnary = enchant.Dict("en_US")
22    best_number_of_english_words = 0
23    for key in tqdm(range(26)):
24        plaintext = encrypt(cyphertext, -key)
25        number_of_english_words = 0
26        for word in plaintext.split(' '):
27            if word and english_dictionnary.check(word):
28                number_of_english_words += 1
29        if number_of_english_words > best_number_of_english_words:
30            best_number_of_english_words = number_of_english_words
31            best_plaintext = plaintext
32            best_key = key
33    click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...')
34    output_file.write(best_plaintext)
35
36if __name__ == '__main__':
37    caesar_breaker()
38view raw

不仔细看的话可能都看不出有什么区别,因为区别只有四个字母 tqdm ,阿拉伯语中 tqdm 是进度的意思。

tqdm 库的用法非常简单,只要把代码中的迭代器用 tqdm 括起来就行了:

1for key in tqdm(range(26)):

这样就会在命令行输出一个进度条,简单得让人不敢相信。

其实 click 的 click.progress_bar 也有类似的功能,但我觉得 click 的进度条不好看而且写法比tqdm 稍微麻烦一点。

总结一下希望大家读完这篇文章能把设计 Python 命令行的这几个原则用到实践中去写出更好用的 Python 命令行。

原文链接:

https://blog.sicara.com/perfect-python-command-line-interfaces-7d5d4efad6a2


(*本文由Python大本营整理,转载请联系微信1092722531)

推荐

推荐阅读:

  • 如何写出符合Python审美的代码风格?

  • 年度重磅:《AI聚变:2018年优秀AI应用案例TOP 20》正式发布

  • MIT新福利,2019人工智能公开课上线啦!

  • 漫画:如何实现大整数相乘?(下)

  • 力压今日头条成 App Store 榜第一,个税 App 惊爆 62 例木马病毒!

  • 狼性文化遭质疑,那我们当个佛系程序员可好?

  • 19位专家年末论深冬:区块链究竟还能否回暖?

  • 老程序员肺腑忠告:千万别一辈子靠技术生存!

  • 微软彻底拥抱Python!

技术流 | 手把手教你用Python设计一个命令行界面相关推荐

  1. python界面设计-手把手教你用Python设计一个简单的命令行界面

    原标题:手把手教你用Python设计一个简单的命令行界面 对 Python 程序来说,完备的命令行界面可以提升团队的工作效率,减少调用时可能碰到的困扰.今天,我们就来教大家如何设计功能完整的 Pyth ...

  2. 手把手教你用Python打造一个语音合成系统

    击上方"Python爬虫与数据挖掘",进行关注 回复"书籍"即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 大弦嘈嘈如急雨,小弦切切如私语. / ...

  3. 如何用python开发游戏_手把手教你用Python完成一个控制台小游戏-阿里云开发者社区...

    很多人想学Python程序设计或者已经了解过一点Python程序设计基础,却没办法开发出一个项目. 今天,通过演示一个简单的控制台小游戏制作,手把手教你如何用Python编写一个游戏程序,即便你是个新 ...

  4. 手把手教你用 wxPython 设计一个可以弹琴的计算器

    文章目录 1. 前言 2. 桌面程序设计的通用框架 3. 了解事件驱动,探索鼠标事件及其绑定 4. 最原始的计算器 5. 更漂亮的计算器 6. 给漂亮的计算器加上声音 7. 打包成.exe文件 8. ...

  5. 黑客很酷?手把手教你用Python做一个

    前言 随着信息化时代的到来,人们对互联网接触越来越广泛,这样就使得很多人对于黑客充满向往,企图通过网络虚拟世界来做一些有趣的事情. 所以今天我们要说的工具就是:怎样利用Python做一个黑客软件. 众 ...

  6. 元旦到了,手把手教你用 Python 制作一个炫酷烟花秀

    大家好,我是小张, 今天是2021 的最后一天,到了这个时间点,部分小伙伴已经开始复盘这一年的得与失.比如今年增加了多少技能点,看了多少本书,写了多少篇文章或者年前的小目标实现进度大概多少等等:做一个 ...

  7. 手把手教你用Python打造一个语音合成系统(已生成软件)

    /前言/ 平时我们聊天的时候,也许会想着录制一些自己的声音,而且还想有点特色,也就是所谓的变声,今天我们要说的就是这个变声器的制作,说的高大上点就是语音合成系统. 这个语音合成系统,能实现个性化语音的 ...

  8. 手把手教你用Python做一个哄女友神器,小白可上手

    大数据文摘出品 作者:诗风悠存.蒋宝尚 哄女朋友最高的境界是什么? 除了用心之外,每天不重复的甜言蜜语必然是少不了的.虽然语文老师上学的时候也教了一些东西,但是日子长了必然"江郎才尽&quo ...

  9. 【Python】手把手教你用Python做一个图像融合demo,小白可上手!

    创作背景 说到融合,一下子会让我们这些95后想起来童年的动漫游戏王了! 发动魔法卡--融合! 哈哈,今天说得当然不是游戏王里的魔法了,但是我们使用的是Python魔法,今天我们将使用Python编程语 ...

最新文章

  1. Photon Server伺服务器在LoadBalancing的基础上扩展登陆服务
  2. 按键防抖_单片机用一个IO口采集多个按键信号
  3. 《以前工作中的三大痛点,只因他们没学Python》
  4. hdu 4533(树状数组区间更新+单点查询)
  5. 由手机上网带来病毒引发的三大疑问?
  6. 我理解的invoke和begininvoke
  7. matlab幂次变换代码,常用的一些图像处理Matlab源代码
  8. Uzi宣布退役:身体条件不允许再继续战斗了!
  9. windows下sublime2 clojure环境配置
  10. CAD自带图案填充代表意思
  11. 小程序解析富文本(支持视频,支持微信编辑器,支持135编辑器富文本样式)
  12. CT图像重建中的伪影
  13. xiaoxin juju needs help - 组合公式
  14. 实例:输入一个时间值s,它是距离当日午夜的秒值,计算目前的时间,时间按00:00:00格式输出
  15. JS与jQuery获取任意事件的子元素下标(获取当前类数组的某一子元素下标)
  16. 华为用户的福利!1分钱就可以坐公交车,操作方法教程
  17. win10+cuda10.0+pytorch安装
  18. Mysql-如何建表更符合业务
  19. Windows下如何启动Redis服务?
  20. 开发板搭建简单的Web服务器

热门文章

  1. ipsec ***野蛮模式应用
  2. 编程小问题系列(2)——为什么WPF里MediaElement等视频控件不起作用
  3. MS sql server和mysql中update多条数据的例子
  4. 新一代宽带路由器—Vigor防火墙路由器
  5. 使用HttpClient实现跨服务图片下载
  6. 空缺的2018-3-11《祖宗十九代》《缝纫机乐队》
  7. Ubuntu使用QCustomPlot简介
  8. cap理论与分布式事务的解决方案
  9. [HNOI 2010]Bounce 弹飞绵羊
  10. 【转载】邻接表表示法