查看原文
其他

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

iPython Python大本营 2019-02-15


作者 | 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)



福利

扫码添加小助手微信,回复:1,入群获取Python电子书(附代码~~)




推荐阅读:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存