APP下载

如何编写完美的 Python 命令列程式?

消息来源:baojiabao.com 作者: 发布时间:2024-04-25

报价宝综合消息如何编写完美的 Python 命令列程式?

这篇文章将教你如何编写完美的 Python 命令列程式,提高团队的生产力,让大家的工作更舒适。

作为 Python 开发者,我们经常要编写命令列程式。比如在我的资料科学专案中,我要从命令列执行指令码来训练模型,以及计算算法的准确率等。

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

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

尽可能提供预设引数值

所有错误情况必须处理(例如,引数缺失,型别错误,找不到档案)

所有引数和选项必须有文件

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

举个简单的例子

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

假设已经有个写好的 encrypt 函式(实现如下),我们需要建立一个简单的指令码,用来加密和解密讯息。我们希望让使用者通过命令列引数选择加密模式(预设)和解密模式,并选择一个秘钥(预设为 1)。

def encrypt(plaintext, key):

cyphertext = ‘‘

for character in plaintext:

if character.isalpha:

number = ord(character)

number += key

if character.isupper:

if number > ord(‘Z‘):

number -= 26

elif number number += 26

elif character.islower:

if number > ord(‘z‘):

26

elif number 26

character = chr(number)

cyphertext += character

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

“初学者”的方法

sys.argv 是个列表,包含使用者在执行指令码时输入的所有引数(包括指令码名自身)。

例如,如果我输入:

> python caesar_script.py --key 23 --decrypt my secret message

pb vhfuhw phvvdjh该列表将包含:

[‘caesar_script.py‘, ‘--key‘, ‘23‘, ‘--decrypt‘, ‘my‘, ‘secret‘, ‘message‘] 因此只需遍历该引数列表,找到‘--key‘(或‘-k‘)以得到秘钥值,找到‘--decrypt‘以设定解密模式(实际上只需要使用秘钥的反转作为秘钥即可)。

最后我们的指令码大致如下:

import sys

from caesar_encryption import encrypt

def caesar:

key = 1

is_error = False

for index, arg in enumerate(sys.argv):

if arg in [‘--key‘, ‘-k‘] and len(sys.argv) > index + 1:

key = int(sys.argv[index + 1])

del sys.argv[index]

del sys.argv[index]

break

for index, arg in

if arg in [‘--encrypt‘, ‘-e‘]:

del sys.argv[index]

break

if arg in [‘--decrypt‘, ‘-d‘]:

key = -key

del sys.argv[index]

break

if len(sys.argv) == 1:

is_error = True

else:

for arg in sys.argv:

if arg.startswith(‘-‘):

is_error = True

if is_error:

print(f‘Usage: python {sys.argv[0]} [ --key ] [ --encrypt|decrypt ] ‘)

else:

print(encrypt(‘ ‘.join(sys.argv[1:]), key))

if __name__ == ‘__main__‘:

caesar支援预设秘钥和预设模式

基本的错误处理(没有提供输入文字的情况,以及提供了无法识别的引数的情况)

出错时或者不带任何引数呼叫指令码时会显示文件:

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

解析命令列引数应该还有更好的办法……

试试 argparse?

argparse 是 Python 用来解析命令列引数的标准库。

我们来看看用 argparse 怎样编写凯撒加密的指令码:

import argparse

from caesar_encryption import encrypt

def caesar:

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=‘*‘)

‘-k‘, ‘--key‘, type=int, default=1)

args = parser.parse_args

text_string = ‘ ‘.join(args.text)

key = args.key

if args.decrypt:

key = -key

cyphertext = encrypt(text_string, key)

print(cyphertext)

if __name__ == ‘__main__‘:

caesar这段程式码也遵循了上述规则,而且与前面的手工编写的指令码相比,可以提供更准确的文件,以及更具有互动性的错误处理:

> python caesar_script_using_argparse.py --encode My message

usage: .py [-h] [-e | -d] [-k KEY] [text [text ...]]

.py: error: unrecognized arguments: --encode

> python .py --help

positional arguments:

text

optional arguments:

-h, --help show this help message and exit

-e, --encrypt

-d, --decrypt

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

click 能做得更好!

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

这里是凯撒加密指令码的第三版,使用了 click:

import click

from caesar_encryption import encrypt

@click.command

@click.argument(‘text‘, nargs=-1)

@click.option(‘--decrypt/--encrypt‘, ‘-d/-e‘)

@click.option(‘--key‘, ‘-k‘, default=1)

def caesar(text, decrypt, key):

text_string = ‘ ‘.join(text)

if decrypt:

key = -key

cyphertext = encrypt(text_string, key)

click.echo(cyphertext)

if __name__ == ‘__main__‘:

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

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

指令码引数定义中的nargs引数指定了该引数期待的单词的数目(一个用引号括起来的字串算一个单词)。预设值是1。这里nargs=-1允许接收任意数目的单词。

--encrypt/--decrypt这种写法可以定义完全互斥的选项(类似于argparse中的add_mutually_exclusive_group函式),它将产生一个布林型引数。

click.echo是该库提供的一个工具函式,它的功能与print相同,但相容Python 2和Python 3,还有一些其他功能(如处理颜色等)。

新增一些隐秘性

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

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

输出也一样:使用者可以储存到档案中,也可以输出到终端。这样就得到了凯撒指令码的最后一个版本:

import click

from caesar_encryption import encrypt

@click.command

@click.option(

‘--input_file‘,

type=click.File(‘r‘),

help=‘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.‘,

)

@click.option(

‘--output_file‘,

type=click.File(‘w‘),

help=‘File in which the encrypted / decrypted text will be written.‘

‘If not provided, the output text will just be printed.‘,

)

@click.option(

‘--decrypt/--encrypt‘,

‘-d/-e‘,

help=‘Whether you want to encrypt the input text or decrypt it.‘

)

@click.option(

‘--key‘,

‘-k‘,

default=1,

help=‘The numeric key to use for the caesar encryption / decryption.‘

)

def caesar(input_file, output_file, decrypt, key):

if input_file:

text = input_file.read

else:

text = click.prompt(‘Enter a text‘, hide_input=not decrypt)

if decrypt:

key = -key

cyphertext = encrypt(text, key)

if output_file:

output_file.write(cyphertext)

else:

click.echo(cyphertext)

if __name__ == ‘__main__‘:

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

首先,注意到我给每个引数选项都加了个help引数。由于指令码变得复杂了,help引数可以给指令码的行为新增一些文件。执行结果如下:

> python caesar_script_v2.py --help

Usage: caesar_script_v2.py [OPTIONS]

Options:

--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.

--output_file FILENAME File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed.

-d, --decrypt / -e, --encrypt Whether you want to encrypt the input text or decrypt it.

-k, --key INTEGER The numeric key to use for the caesar encryption / decryption.

--help Show this message and exit.两个新的引数:input_file 和 output_file,型别均为 click.File。该库能够用正确的模式开启档案,处理可能的错误,再执行函式。例如:

> 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正像help文字中解释的那样,如果没有提供input_file,就使用click.promp让使用者直接在提示符下输入文字,在加密模式下这些文字是隐藏的。如下所示:

> python caesar_script_v2.py --encrypt --key 2

Enter a text: **************

yyy.ukectc.eqo

破解密文!

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

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

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

import click

import enchant

from caesar_encryption import encrypt

@click.command

@click.option(

‘--input_file‘,

type=click.File(‘r‘),

required=True,

)

@click.option(

‘--output_file‘,

type=click.File(‘w‘),

required=True,

)

def caesar_breaker(input_file, output_file):

cyphertext = input_file.read

english_dictionnary = enchant.Dict("en_US")

max_number_of_english_words = 0

for key in range(26):

plaintext = encrypt(cyphertext, -key)

number_of_english_words = 0

for word in plaintext.split(‘ ‘):

if word and english_dictionnary.check(word):

number_of_english_words += 1

if number_of_english_words > max_number_of_english_words:

max_number_of_english_words = number_of_english_words

best_plaintext = plaintext

best_key = key

click.echo(f‘The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...‘)

output_file.write(best_plaintext)

if __name__ == ‘__main__‘:

caesar_breaker

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

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

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

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

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

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

import click

import enchant

from tqdm import tqdm

from caesar_encryption import encrypt

@click.command

@click.option(

‘--input_file‘,

type=click.File(‘r‘),

required=True,

)

@click.option(

‘--output_file‘,

type=click.File(‘w‘),

required=True,

)

def caesar_breaker(input_file, output_file):

cyphertext = input_file.read

english_dictionnary = enchant.Dict("en_US")

best_number_of_english_words = 0

for key in tqdm(range(26)):

plaintext = encrypt(cyphertext, -key)

number_of_english_words = 0

for word in plaintext.split(‘ ‘):

if word and english_dictionnary.check(word):

number_of_english_words += 1

if number_of_english_words > best_number_of_english_words:

best_number_of_english_words = number_of_english_words

best_plaintext = plaintext

best_key = key

click.echo(f‘The most likely encryption key is {best_key}. It gives the following plaintext:\n\n{best_plaintext[:1000]}...‘)

output_file.write(best_plaintext)

if __name__ == ‘__main__‘:

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

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

for key in tqdm(range(26)):这样就能显示出非常漂亮的进度条。我都不敢相信这是真的。

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

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

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

作者:Yannick Wolff,Sicara 的资料科学家。

本文为 CSDN 翻译,如需转载,请注明来源出处。

热 文 推 荐

print_r(‘点个好看吧!‘);

var_dump(‘点个好看吧!‘);

NSLog(@"点个好看吧!");

System.out.println("点个好看吧!");

console.log("点个好看吧!");

print("点个好看吧!");

printf("点个好看吧!");

cout Console.WriteLine("点个好看吧!");

fmt.Println("点个好看吧!");

Response.Write("点个好看吧!");

alert("点个好看吧!")

echo "点个好看吧!"

2019-01-19 10:40:00

相关文章