美文网首页Python学习大数据 爬虫Python AI Sql
如何编写完美的Python命令行程序?

如何编写完美的Python命令行程序?

作者: 1a076099f916 | 来源:发表于2019-01-27 10:37 被阅读6次
    如何编写完美的Python命令行程序?

    作为 Python 开发者,我们经常要编写命令行程序。比如在我的数据科学项目中,我要从命令行运行脚本来训练模型,以及计算算法的准确率等。

    加群:700341555获取Python入门学习资料!

    如何编写完美的Python命令行程序?

    因此,更方便更易用的脚本能够很好地提高生产力,特别是在有多个开发者从事同一个项目的场合下。

    因此,我建议你遵循以下四条规则:

    1. 尽可能提供默认参数值
    2. 所有错误情况必须处理(例如,参数缺失,类型错误,找不到文件)
    3. 所有参数和选项必须有文档
    4. 不是立即完成的任务应当显示进度条

    举个简单的例子

    我们把这些规则应用到一个具体的例子上。这个脚本可以使用凯撒加密法加密和解密消息。

    假设已经有个写好的 encrypt 函数(实现如下),我们需要创建一个简单的脚本,用来加密和解密消息。我们希望让用户通过命令行参数选择加密模式(默认)和解密模式,并选择一个秘钥(默认为 1)。

    如何编写完美的Python命令行程序?

    我们的脚本需要做的第一件事就是获取命令行参数的值。当我搜索“python command line arguments”时,出现的第一个结果是关于sys.argv的,所以我们来试试这个方法……

    “初学者”的方法

    sys.argv 是个列表,包含用户在运行脚本时输入的所有参数(包括脚本名自身)。

    例如,如果我输入:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">> python caesar_script.py --key 23 --decrypt my secret message
    pb vhfuhw phvvdjh
    </pre>

    该列表将包含:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']
    </pre>

    因此只需遍历该参数列表,找到'--key'(或'-k')以得到秘钥值,找到'--decrypt'以设置解密模式(实际上只需要使用秘钥的反转作为秘钥即可)。

    最后我们的脚本大致如下:

    如何编写完美的Python命令行程序?

    这个脚本遵循了一些我们前面推荐的规则:

    1. 支持默认秘钥和默认模式
    2. 基本的错误处理(没有提供输入文本的情况,以及提供了无法识别的参数的情况)
    3. 出错时或者不带任何参数调用脚本时会显示文档:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">> python caesar_script_using_sys_argv.py
    Usage: python caesar.py [ --key <key> ] [ --encrypt|decrypt ] <text>
    </pre>

    但是,这个凯撒加密法脚本太长了(39 行,其中甚至还没包括加密代码本身),而且很难读懂。

    解析命令行参数应该还有更好的办法……

    试试 argparse?

    argparse 是 Python 用来解析命令行参数的标准库。

    我们来看看用 argparse 怎样编写凯撒加密的脚本:

    如何编写完美的Python命令行程序?

    这段代码也遵循了上述规则,而且与前面的手工编写的脚本相比,可以提供更准确的文档,以及更具有交互性的错误处理:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">> python caesar_script_using_argparse.py --encode My message
    usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
    caesar_script_using_argparse.py: error: unrecognized arguments: --encode

    python caesar_script_using_argparse.py --help
    usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]
    </pre>

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">positional arguments:
    text
    optional arguments:
    -h, --help show this help message and exit
    -e, --encrypt
    -d, --decrypt
    -k KEY, --key KEY
    </pre>

    但是,仔细看了这段代码后,我发现(虽然有点主观)函数开头的几行(从7行到13行)定义了参数,但定义方式并不太优雅:它太臃肿了,而且完全是程式化的。应该有更描述性、更简洁的方法。

    click 能做得更好!

    幸运的是,有个 Python 库能提供与 argparse 同样的功能(甚至还能提供更多),它的代码风格更优雅。这个库的名字叫 click。

    这里是凯撒加密脚本的第三版,使用了 click:

    如何编写完美的Python命令行程序?

    注意现在参数和选项都在修饰器里定义,定义好的参数直接作为函数参数提供。

    我来解释一下上面代码中的一些地方:

    • 脚本参数定义中的nargs参数指定了该参数期待的单词的数目(一个用引号括起来的字符串算一个单词)。默认值是1。这里nargs=-1允许接收任意数目的单词。
    • --encrypt/--decrypt这种写法可以定义完全互斥的选项(类似于argparse中的add_mutually_exclusive_group函数),它将产生一个布尔型参数。
    • click.echo是该库提供的一个工具函数,它的功能与print相同,但兼容Python 2和Python 3,还有一些其他功能(如处理颜色等)。

    添加一些隐秘性

    这个脚本的参数(被加密的消息)应当是最高机密。而我们却要求用户直接在终端里输入文本,使得这些文本被记录在命令历史中,这不是很讽刺吗?

    解决方法之一就是使用隐藏的提示。或者可以从输入文件中读取文本,对于较长的文本来说更实际一些。或者可以干脆让用户选择。

    输出也一样:用户可以保存到文件中,也可以输出到终端。这样就得到了凯撒脚本的最后一个版本:

    如何编写完美的Python命令行程序?

    这个版本有什么新东西吗?

    • 首先,注意到我给每个参数选项都加了个help参数。由于脚本变得复杂了,help参数可以给脚本的行为添加一些文档。运行结果如下:
    如何编写完美的Python命令行程序?
    • 两个新的参数:input_file 和 output_file,类型均为 click.File。该库能够用正确的模式打开文件,处理可能的错误,再执行函数。例如:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">> python caesar_script_v2.py --decrypt --input_file wrong_file.txt
    Usage: caesar_script_v2.py [OPTIONS]
    Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory
    </pre>

    • 正像help文本中解释的那样,如果没有提供input_file,就使用click.promp让用户直接在提示符下输入文本,在加密模式下这些文本是隐藏的。如下所示:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">> python caesar_script_v2.py --encrypt --key 2
    Enter a text: **************
    yyy.ukectc.eqo
    </pre>

    破解密文!

    现在设想你是个黑客:你要解密一个用凯撒加密过的密文,但你不知道秘钥是什么。

    最简单的策略就是用所有可能的秘钥调用解密函数 25 次,阅读解密结果,看看哪个是合理的。

    但你很聪明,而且也很懒,所以你想让整个过程自动化。确定解密后的 25 个文本哪个最可能是原始文本的方法之一,就是统计所有这些文本中的英文单词的个数。这可以使用 PyEnchant 模块实现:

    如何编写完美的Python命令行程序? 如何编写完美的Python命令行程序?

    貌似运行得很不错,但别忘了,好的命令行程序还有个规则需要遵守:

    4.A 不是立即完成的任务应当显示进度条。

    示例中的文本包含104个单词,因此该脚本需要大约5秒才能解密。这很正常,因为它需要检查所有25个秘钥,每个秘钥都要检查104个单词是否出现在英文字典中。

    假设你要解密的文本包括10^5个但IC,那么就要花费50秒才能输出结果,用户可能会非常着急。

    因此我建议这种任务一定要显示进度条。特别是,显示进度条还非常容易实现。

    下面是个显示进度条的例子:

    如何编写完美的Python命令行程序?

    你发现区别了吗?可能不太好找,因为区别真的很小,只有四个字母:tqdm。

    tqdm 是 Python 库的名字,也是它包含的类的名字。只需用它包裹一个可迭代的东西,就能显示出进度条:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">for key in tqdm(range(26)):
    </pre>

    这样就能显示出非常漂亮的进度条。我都不敢相信这是真的。

    如何编写完美的Python命令行程序?

    另外,click也提供类似的显示进度条的工具(click.progress_bar),但我觉得它的外观不太容易懂,而且要写的代码也多一些。

    我希望这篇文章能让你在改进开发者的体验上多花点时间。

    相关文章

      网友评论

        本文标题:如何编写完美的Python命令行程序?

        本文链接:https://www.haomeiwen.com/subject/vojgjqtx.html