Skip to content

Latest commit

 

History

History
1201 lines (767 loc) · 59 KB

File metadata and controls

1201 lines (767 loc) · 59 KB

五、用户输入和输出

在本章中,我们将介绍以下配方:

  • 使用 print()函数的功能
  • 使用 input()和 getpass()进行用户输入
  • 使用“format”进行调试。format_ 映射(vars())
  • 使用 argparse 获取命令行输入
  • 使用 cmd 创建命令行应用程序
  • 使用操作系统环境设置

导言

软件的核心价值是产生有用的输出。一种简单的输出类型是一些有用结果的文本显示。Python 通过print()函数支持这一点。

input()功能与print()功能具有明显的平行性。input()函数从控制台读取文本,允许我们为程序提供不同的值。

提供输入还有许多其他常用方法。解析命令行也有助于许多应用程序。我们有时需要使用配置文件来提供有用的输入。数据文件和网络连接是提供输入的更多方式。其中每一项都是不同的,需要单独研究。在本章中,我们将重点介绍input()print()的基本原理。

使用 print()函数的功能

在许多情况下,print()函数是我们学习的第一个函数。第一个脚本通常是以下脚本的变体:

print("Hello world.")

我们很快了解到,print()函数可以显示多个值,包括项目之间的有用空格。

当我们写这篇文章时:

>>> count = 9973 
>>> print("Final count", count) 
Final count 9973

我们看到包含一个空格来分隔这两个值。此外,通常由\n字符表示的换行符将在函数中提供的值之后打印。

我们能控制这种格式吗?我们可以更改提供的额外字符吗?

事实证明,我们可以用print()做更多的事情。

准备好了吗

我们有一个电子表格,用于记录大型帆船的燃油消耗量。它包含如下所示的行:

| **日期** | **10/25/13** | **10/26/13** | **10/28/13** | | **发动机在**上 | 08:24:00 | 09:12:00 | 13:21:00 | | **在**上的燃油高度 | 29 | 27 | 22 | | **发动机关闭** | 13:15:00 | 18:25:00 | 06:25:00 | | **燃油高度关** | 27 | 22 | 14 |

有关此数据的更多信息,请参阅第 4 章内置数据结构–列表、集合、指令中的从集合中移除项目–移除()、弹出()和差异切片和切割列表配方。油箱里没有液位计。燃油深度必须通过油箱侧面的观察窗读取,这就是为什么燃油量被称为深度。油箱的最大深度约为 31 英寸,容积约为 72 加仑;可以将深度转换为体积。

下面是一个使用 CSV 数据的示例。此函数读取文件并返回从每行生成的字段列表:

>>> from pathlib import Path 
>>> import csv 
>>> from collections import OrderedDict 
>>> def get_fuel_use(source_path): 
...     with source_path.open() as source_file: 
...         rdr= csv.DictReader(source_file) 
...         od = (OrderedDict( 
...             [(column, row[column]) for column in rdr.fieldnames]) 
...             for row in rdr) 
...         data = list(od) 
...     return data 
>>> source_path = Path("code/fuel2.csv") 
>>> fuel_use= get_fuel_use(source_path) 
>>> fuel_use  
[OrderedDict([('date', '10/25/13'), ('engine on', '08:24:00'), 
    ('fuel height on', '29'), ('engine off', '13:15:00'), 
    ('fuel height off', '27')]), 
OrderedDict([('date', '10/26/13'), ('engine on', '09:12:00'), 
    ('fuel height on', '27'), ('engine off', '18:25:00'), 
    ('fuel height off', '22')]), 
OrderedDict([('date', '10/28/13'), ('engine on', '13:21:00'), 
    ('fuel height on', '22'), ('engine off', '06:25:00'), 
    ('fuel height off', '14')])]

我们使用一个pathlib.Path对象来定义原始数据的位置。我们定义了一个函数get_fuel_use(),它将以给定的路径打开并读取文件。此函数用于从源电子表格创建行列表。每行数据都表示为一个OrderedDict对象。

函数首先创建一个csv.DictReader对象来解析原始数据。读取器通常返回一个内置的dict对象,该对象不会强制对键进行特定排序。为了强制执行特定的键顺序,此函数使用生成器表达式为每行创建一个OrderedDict对象。阅读器的fieldnames属性rdr用于强制列进入特定顺序。生成器表达式使用一对嵌套的循环:一个循环处理行中的每个字段,而外部循环处理数据中的每一行。

结果是一个包含OrderedDict对象的列表对象。这是我们可以用于打印的一致数据源。根据第一行中的列名,每行有五个字段。

怎么做。。。

我们有两种方法来控制print()格式:

  • 设置字段间分隔符sep,默认值为空格
  • 设置行尾字符end,其默认值为\n字符

我们将展示几个更改sepend的示例。每一种都是一种一步配方。

默认情况如下所示。本例对sepend没有变化:

>>> for leg in fuel_use: 
...    start = float(leg['fuel height on']) 
...    finish = float(leg['fuel height off']) 
...    print("On", leg['date'], 
...    'from', leg['engine on'], 
...    'to', leg['engine off'], 
...    'change', start-finish, 'in.') 
On 10/25/13 from 08:24:00 to 13:15:00 change 2.0 in. 
On 10/26/13 from 09:12:00 to 18:25:00 change 5.0 in. 
On 10/28/13 from 13:21:00 to 06:25:00 change 8.0 in. 

当我们查看输出时,可以看到在每个项之间插入了空格。每个数据项集合末尾的\n字符表示每个print()函数产生一个单独的行。

在准备数据时,我们可能希望使用类似于逗号分隔值的格式,可能使用的列分隔符不是简单的逗号。下面是一个使用|的示例:

>>> print("date", "start", "end", "depth", sep=" | ") 
date | start | end | depth 
>>> for leg in fuel_use: 
...    start = float(leg['fuel height on']) 
...    finish = float(leg['fuel height off']) 
...    print(leg['date'], leg['engine on'], 
...    leg['engine off'], start-finish, sep=" | ") 
10/25/13 | 08:24:00 | 13:15:00 | 2.0 
10/26/13 | 09:12:00 | 18:25:00 | 5.0 
10/28/13 | 13:21:00 | 06:25:00 | 8.0

在本例中,我们可以看到每个列都有给定的分隔符字符串。由于end设置没有变化,每个print()功能都会产生一行不同的输出。

最常见的情况似乎是我们想要完全抑制分离器。这使我们能够很好地控制输出。

下面是我们如何更改默认标点以强调字段名和值。在本例中,我们更改了end设置:

>>> for leg in fuel_use: 
...    start = float(leg['fuel height on']) 
...    finish = float(leg['fuel height off']) 
...    print('date', leg['date'], sep='=', end=', ') 
...    print('on', leg['engine on'], sep='=', end=', ') 
...    print('off', leg['engine off'], sep='=', end=', ') 
...    print('change', start-finish, sep="=") 
date=10/25/13, on=08:24:00, off=13:15:00, change=2.0 
date=10/26/13, on=09:12:00, off=18:25:00, change=5.0 
date=10/28/13, on=13:21:00, off=06:25:00, change=8.0

由于结束字符串更改为,,因此每次使用print()函数都不会产生单独的行。直到最后的print()函数,它有end的默认值,我们才得到正确的行尾。

显然,对于任何比这些简单示例更复杂的东西,这种技术都会变得非常复杂。简单地说,我们可以调整分隔符或结尾。对于更复杂的内容,我们需要使用字符串的format()方法。

它是如何工作的。。。

在一般情况下,print()函数是stdout.write()的一个方便的包装器。这种关系是可以改变的,我们将在下面看到。

我们可以想象print()有这样一个定义:

    def print(*args, *, sep=None, end=None, file=sys.stdout): 
        if sep is None: sep = ' ' 
        if end is None: end = '\n' 
        arg_iter= iter(args) 
        first = next(arg_iter) 
        sys.stdout.write(repr(first)) 
        for value in arg_iter: 
            sys.stdout.write(sep) 
            sys.stdout.write(repr(value()) 
        sys.stdout.write(end) 

这就提示了分隔符字符串和结束字符串如何包含在print()函数的输出中。如果未提供值,则默认值为空格和新行。函数遍历参数值,将第一个值视为特殊值,因为它没有分隔符。这种方法确保分隔符字符串sep出现在值之间。

行尾字符串end出现在所有值之后。它总是被写下来的。我们可以通过将其设置为零长度字符串来有效地关闭它。

还有更多。。。

sys模块定义始终可用的两个标准输出文件:sys.stdoutsys.stderr

除了标准输出文件外,我们还可以使用file=关键字参数写入标准错误文件:

    import sys 
    print("Red Alert!", file=sys.stderr) 

我们已经导入了sys模块,以便访问标准错误文件。我们使用它来编写一条不属于标准输出流的消息。

通常,我们需要谨慎,不要在一个程序中打开太多的输出文件。操作系统的限制通常足以打开许多文件。但是,当程序创建大量文件时,它可能会变得混乱。

使用 OS 文件重定向技术通常效果很好。一个程序的主输出可以写入sys.stdout;这很容易在操作系统级别重定向。用户可以输入如下命令行:

python3 myapp.py <input.dat >output.dat

这将提供input.dat文件作为sys.stdin上的输入。当 Python 程序写入sys.stdout时,操作系统会将输出重定向到output.dat对象。

在某些情况下,我们需要打开其他文件。在这种情况下,我们可能会看到这样的编程:

    from pathlib import Path 
    target_path = Path("somefile.dat") 
    with target_path.open('w', encoding='utf-8') as target_file: 
        print("Some output", file=target_file) 
        print("Ordinary log") 

在本例中,我们已经为输出打开了一个特定路径,并使用with语句将打开的文件分配给target_file。然后,我们可以将其用作print()函数中的file=值来写入此文件。因为文件是一个上下文管理器,所以保留with语句意味着文件将被正确关闭,所有操作系统资源将从应用程序中释放。所有文件操作都应该包装在一个with语句上下文中,以确保资源被正确释放。

另见

  • 参考*调试中的“format”。format_map(vars())*配方
  • 有关本例中输入数据的更多信息,请参阅第 4 章内置数据结构【列表、集合、指令】中的从集合中移除项目–移除()、pop()和差异切片和切割列表配方
  • 有关一般文件操作的更多信息,请参阅第 9 章输入/输出、物理格式、逻辑布局

使用 input()和 getpass()进行用户输入

一些 Python 脚本依赖于从用户收集输入。有几种方法可以做到这一点。一种流行的技术是使用控制台提示用户输入。

有两种相对常见的情况:

  • 普通输入:我们使用input()功能。这将提供输入字符的有用回显。
  • 无回声输入:这通常用于密码。输入的字符不显示,提供一定程度的隐私。为此,我们使用getpass模块中的getpass()函数。

input()getpass()函数只是从控制台读取的两个实现选项。事实证明,获取字符串只是处理的第一步。我们实际上有不同层次的考虑:

  1. 与控制台的初始交互。这是编写提示和读取输入的基础。这必须正确处理数据和键盘事件,如编辑退格。这也可能意味着适当地处理文件末尾。
  2. 正在验证输入,以查看它是否属于预期的值域。我们可能要查找数字、是/否值或一周中的天数。在大多数情况下,验证层有两个部分:
    • 我们检查输入是否符合某些通用域,例如数字。
    • 我们检查输入是否适合某个更具体的子域。例如,这可能包括检查数字是否大于或等于零。
  3. 在更大的上下文中验证输入,以确保它与其他输入一致。例如,我们可以检查用户的出生日期是否早于今天。

除了这些技术之外,我们还将研究中使用 argparse 获取命令行输入配方的一些其他方法。

准备好了吗

我们将研究一种从人身上读取复杂结构的技术。在本例中,我们将使用年、月和日作为单独的项目来创建完整的日期。

下面是一个省略所有验证问题的快速示例:

    from datetime import date 

    def get_date(): 
        year = int(input("year: ")) 
        month = int(input("month [1-12]: ")) 
        day = int(input("day [1-31]: ")) 
        result = date(year, month, day) 
        return result 

这说明使用input()功能是多么容易。我们通常需要在附加处理中对其进行包装,以使其更有用。日历很复杂,我们不愿意接受 2 月 32 日而不警告用户这不是一个合适的日期。

怎么做。。。

  1. Check whether the input is a password or something equally subject to redaction. If so, then use the getpass.getpass() function. This means we need to import the following function:

            from getpass import getpass 

    否则,如果不需要编辑输入,则使用input()功能。

  2. Determine which prompt will be used. This might be as simple as >>> or something more complex. In some cases, we might provide a great deal of contextual information.

    在我们的示例中,我们提供了一个字段名和一个关于预期作为提示字符串的数据类型的提示。提示字符串是input()getpass()函数的参数:

            year = int(input("year: ")) 
  3. 确定如何单独验证每个项目。最简单的情况是一个包含所有内容的单一规则的单一值。在这种更复杂的情况下,每个元素都是一个带有范围约束的数字。在后面的步骤中,我们将查看如何验证复合项。

  4. 我们可能希望将我们的输入重组为如下所示:

            month = None 
            while month is None: 
                month_text = input("month [1-12]: ") 
                try: 
                    month = int(month_text) 
                    if 1 <= month <= 12: 
                        pass 
                    else: 
                        raise ValueError("Month of range 1-12") 
                except ValueError as ex: 
                    print(ex) 
                    month = None 

这将对输入应用两个验证规则:

  • 使用int()函数检查月份是否为有效整数
  • 它使用引发ValueError异常的if语句检查整数是否在范围[1,12]内

为错误输入引发异常通常是最简单的方法。它给我们最大的灵活性。我们还可以使用其他异常类,包括定义自定义数据验证异常。

由于我们将对复杂对象的每个字段使用几乎相同的循环,因此我们需要重新构造此输入并将序列验证为单独的函数。我们称之为get_integer()。我们将在此处查看详细信息:

  1. Validate the composite object. In this case, it also means that our overall input needs to be restructured to allow for a retry in the event of bad input:

            input_date = None 
            while input_date is None: 
                year = get_integer("year: ", 1900, 2100) 
                month = get_integer("month [1-12]: ", 1, 12) 
                day = get_integer("day [1-31]: ", 1, 31) 
                try: 
                    result = date(year, month, day) 
                except ValueError as ex: 
                    print(ex) 
                    input_date = None 
            # assert input_date is the valid date entered by the user 

    这个整体循环实现了复合日期对象的更高级别验证。

    给定一年一个月,我们实际上可以确定一个稍微窄一点的天数范围。复杂的是,不仅月份的天数不同,从 28 天到 31 天不等,而且 2 月份的天数也随着年份的不同而不同。

  2. Rather than mimicing the rules, it's easier to use the datetime module to compute the first days of two adjacent months, as follows:

            day_1_date = date(year, month, 1) 
            if month == 12: 
                next_year, next_month = year+1, 1 
            else: 
                next_year, next_month = year, month+1 
            day_end_date = date(next_year, next_month, 1) 

    这将正确计算任何给定月份的最后一天。该算法通过计算给定年份和月份的第一天来工作。然后计算下个月的第一天。它适当地改变年份,使year+1的 1 月紧跟year的 12 月。

    这些日期之间的天数是给定月份的天数。我们可以使用表达式(day_end_date - day_1_date).daystimedelta对象中提取天数。

它是如何工作的。。。

我们需要将输入问题分解为几个独立但密切相关的问题。底层是与用户的初始交互。我们确定了两种常见的处理方法:

  • input():提示并简单阅读
  • getpass.getpass():提示并读取密码,无回声

我们希望能够使用退格字符编辑当前输入行。在某些环境中,可以使用更复杂的编辑器。它体现在 Pythonreadline模块中。此模块(如果存在)可以在准备输入行时添加大量编辑。该模块的主要功能是操作系统级输入历史记录,我们可以使用向上箭头键恢复任何以前的输入。

我们已将输入验证分解为几个层次,以反映确认输入有效所需的编程类型:

  • 通用域验证应使用简单的转换函数,如int()float()。这往往会引发无效数据的异常。使用这些转换函数并处理异常要比尝试编写与有效数值匹配的正则表达式简单得多。
  • 我们的子域验证必须使用if语句来确定值是否符合任何附加约束,如施加的范围。为了保持一致性,如果数据无效,这也会引发异常。

可能会对值施加许多潜在的约束。例如,我们可能只需要有效的 OS 进程 ID,称为 PIDs。这需要在 Nanny Linux 系统上检查/proc/<pid>路径。

对于基于 BSD 的系统,如 Mac OS X,/proc文件系统不存在。相反,需要执行以下操作来确定 PID 是否有效:

    import subprocess 
    status = subprocess.check_output( 
        ['ps',PID]) 

对于 Windows,命令如下所示:

    status = subprocess.check_output( 
        ['tasklist', '/fi', '"PID eq {PID}"'.format(PID=PID)]) 

这两个函数中的任何一个都需要作为输入验证的一部分,以确保用户输入正确的 PID 值。只有在确保了整数的主域时,才能应用此方法。

最后,我们的整体输入函数还应该为无效输入引发一个异常。这在复杂性上可能会有很大的不同。我们在示例中创建了一个简单的日期对象。在其他情况下,我们可能需要进行更多的处理来确定复杂输入是否有效。

还有更多。。。

对于用户输入,我们有几种不同的方法。我们将详细介绍这两个主题:

  • 输入字符串解析:这将涉及到简单使用input()和巧妙的解析
  • 通过cmd模块进行交互:这涉及到一个更复杂的类,以及一些更简单的解析

输入字符串解析

简单的日期值需要三个单独的字段。包含 UTC 时区偏移的更复杂的日期时间将涉及七个单独的字段。通过读取和解析字符串而不是单个字段,可以改善用户体验。

对于简单的日期输入,我们可以使用以下内容:

raw_date_str = input("date [yyyy-mm-dd]: ") 
input_date = datetime.strptime(raw_date_str, '%Y-%m-%d').date() 

我们使用了strptime()函数来解析给定格式的时间字符串。我们在input()函数提供的提示中强调了预期的日期格式。

这种类型的输入要求用户输入更复杂的字符串。因为它是一个字符串,包含了约会的所有细节,所以很多人觉得它更简单、更友好。

请注意,收集单个字段和处理复杂字符串的两种技术都依赖于底层的input()函数。

通过 cmd 模块进行交互

cmd模块包括Cmd类,可用于构建交互界面。这对用户交互的概念采取了一种截然不同的方法。它不依赖于显式地使用input()

我们将在使用 cmd 创建命令行应用程序配方中详细介绍这一点。

另见

在目前为 Oracle 所有的 SunOS 操作系统的参考资料中,有一组命令可提示不同类型的用户输入:

https://docs.oracle.com/cd/E19683-01/816-0210/6m6nb7m5d/index.html

具体来说,所有以ck开头的命令都用于收集和验证用户输入。这可用于定义输入验证规则模块:

  • ckdate:提示并验证日期
  • ckgid:提示并验证组 ID
  • ckint:显示提示,验证并返回整数值
  • ckitem:构建菜单,提示并返回菜单项
  • ckkeywd:提示并验证关键字
  • ckpath:显示提示、验证并返回路径名
  • ckrange:提示并验证整数
  • ckstr:显示提示、验证并返回字符串答案
  • cktime:显示提示、验证并返回一天中的时间
  • ckuid:提示并验证用户 ID
  • ckyorn:提示并验证是/否

使用“格式”调试。格式映射(vars())

Python 中最重要的调试和设计工具之一是print()函数。有几种格式选项可用;我们在中使用 print()函数配方的功能查看了这些。

如果我们想要更灵活的输出呢?我们使用"string".format_map()方法具有更大的灵活性。这还不是全部。我们可以将此功能与vars()功能结合起来,创建一些经常导致惊喜的东西!

准备好了吗

让我们来看一个多步骤的过程,它涉及一些中等复杂的计算。我们将计算一些样本数据的平均值和标准偏差。给定这些值,我们将定位高于平均值一个以上标准偏差的所有项目:

>>> import statistics 
>>> size = [2353, 2889, 2195, 3094, 
... 725, 1099, 690, 1207, 926, 
... 758, 615, 521, 1320] 
>>> mean_size = statistics.mean(size) 
>>> std_size = statistics.stdev(size) 
>>> sig1 = round(mean_size + std_size, 1) 
>>> [x for x in size if x > sig1] 
[2353, 2889, 3094]

此计算有几个工作变量。mean_sizestd_sizesig1变量都显示过滤size列表的最终列表理解元素。如果结果令人困惑甚至不正确,了解计算中的中间步骤是很有帮助的。在这种情况下,由于它们是浮点值,我们通常希望对结果进行四舍五入,使其更有意义。

怎么做。。。

  1. vars()函数从各种来源构建字典结构。

  2. 如果没有参数,那么默认情况下,vars()函数将展开所有局部变量。这将创建一个可以与模板字符串的format_map()方法一起使用的映射。

  3. 使用映射允许我们使用变量名将变量注入格式模板。如下所示:

     >>> print( 
          ...     "mean={mean_size:.2f}, std={std_size:.2f}" 
          ...     .format_map(vars()) 
          ... ) 
          mean=1414.77, std=901.10

我们可以将任何局部变量放入格式字符串中。使用format_map(vars()),我们不需要更复杂的方法来选择要显示哪些变量。

它是如何工作的。。。

vars()函数从各种来源构建字典结构:

  • vars()表达式将展开所有局部变量,以创建可与format_map()方法一起使用的映射。
  • vars(object)表达式将展开对象内部__dict__属性中的所有项。这允许我们公开类定义和对象的属性。当我们看第 6 章类和对象基础中的对象时,我们将看到如何利用这一点。

format_map()方法需要一个单独的参数,这是一个映射。格式字符串使用{name}引用映射中的键。我们可以使用{name:format}提供格式规范。我们还可以使用{name!conversion}提供使用repr()str()ascii()功能的转换功能。

有关格式化选项的更多背景信息,请参阅第 1 章中的*使用“模板”构建复杂字符串。format()*配方、数字、字符串和元组

还有更多。。。

format_map(vars())技术是一种显示变量值的简单方法。另一种选择是使用format(**vars())。这个替代方案可以给我们一些额外的灵活性。

例如,我们可以使用这种更灵活的格式来包含不只是局部变量的其他计算:

>>> print( 
...     "mean={mean_size:.2f}, std={std_size:.2f}," 
...     " limit2={sig2:.2f}" 
...     .format(sig2=mean_size+2*std_size, **vars()) 
... ) 
mean=1414.77, std=901.10, limit2=3216.97

我们已经计算了一个新值sig2,它只出现在格式化输出中。

另见

  • 请参阅第 1 章中的*使用“模板”构建复杂字符串。format()*配方、数字、字符串和元组,了解更多使用 format()方法可以完成的事情
  • 有关其他格式选项,请参阅使用 print()函数的功能

使用 argparse 获取命令行输入

在某些情况下,我们希望从操作系统命令行获取用户输入,而无需进行大量交互。我们更愿意解析命令行参数值并执行处理或报告错误。

例如,在操作系统级别,我们可能希望运行如下程序:

slott$ python3 ch05_r04.py -r KM 36.12,-86.67 33.94,-118.40

From (36.12, -86.67) to (33.94, -118.4) in KM = 2887.35

操作系统提示为slott$。我们输入了python3 ch05_r04.py的命令。此命令有一个可选参数-r KM,以及两个位置参数36.12,-86.6733.94,-118.40

程序解析命令行参数并将结果写回控制台。这允许进行非常简单的用户交互。它使程序非常简单。它允许用户编写 shell 脚本来调用该程序,或者将该程序与其他 Python 程序合并以创建更高级别的程序。

如果用户输入的内容不正确,则交互可能如下所示:

slott$ python3 ch05_r04.py -r KM 36.12,-86.67 33.94,-118asd

usage: ch05_r04.py [-h] [-r {NM,MI,KM}] p1 p2

ch05_r04.py: error: argument p2: could not convert string to float: '-118asd'

无效参数值-118asd导致错误消息。程序停止时出现错误状态代码。在大多数情况下,用户可以按向上箭头键返回上一个命令行,进行更改,然后再次运行程序。交互委托给 OS 命令行。

程序名ch05_r04-信息量不大。我们或许可以做得更好。位置参数是两对(纬度、经度)。输出以给定单位显示两者之间的距离。

如何从命令行解析参数值?

准备好了吗

我们需要做的第一件事是重构代码以创建两个单独的函数:

  • 从命令行获取参数的函数。由于argparse模块的工作方式,此函数几乎总是返回argparse.Namespace对象。
  • 一个真正起作用的函数。此函数的设计应确保它不会以任何方式引用命令行选项。这意味着它可以在各种上下文中重用。

这是我们的实际工作功能display()

    from ch03_r05 import haversine, MI, NM, KM 
    def display(lat1, lon1, lat2, lon2, r): 
        r_float = {'NM': NM, 'KM': KM, 'MI': MI}[r] 
        d = haversine( lat1, lon1, lat2, lon2, r_float ) 
        print( "From {lat1},{lon1} to {lat2},{lon2}" 
              "in {r} = {d:.2f}".format_map(vars())) 

我们已经从另一个模块导入了核心计算haversine()。我们为该函数提供了参数值,并使用format()显示最终结果消息。

我们基于第 3 章函数定义基于部分函数配方中的示例计算得出:

Getting ready

基本计算得出两点之间的中心角c,给出为(lat1lon1)和(lat2lon2)。角度以弧度为单位测量。我们将它乘以地球的平均半径(以某些单位表示),将其转换为距离。如果我们将角度c乘以 3959 英里的半径,我们将得到以英里为单位的角度表示的距离。

请注意,我们希望距离转换因子r作为字符串提供。然后,此函数将字符串映射到实际的浮点值。

有关format()方法的详细信息,请注意,我们使用的是*调试中的一个变体,带有“format.format_map(vars())*配方。

下面是函数在 Python 中使用时的外观:

>>> from ch05_r04 import display 
>>> display(36.12, -86.67, 33.94, -118.4, 'NM') 
From 36.12,-86.67 to 33.94,-118.4 in NM = 1558.53

此功能有两个重要的设计特征。第一个特性是它避免了对由参数解析创建的argparse.Namespace对象特性的引用。我们的目标是要有一个功能,我们可以在许多替代上下文中重用。我们需要将用户界面的输入和输出元素分开。

第二个设计特征是,此函数显示由另一个函数计算的值。这是一个有用的特性,因为它可以让我们分解问题。我们已将用户体验与基本计算分离。

怎么做。。。

  1. 定义整体参数解析函数:

            def get_options(): 
  2. 创建parser对象:

            parser = argparse.ArgumentParser() 
  3. Add the various types of arguments to the parser object. Sometimes this is difficult because we're still refining the user experience. It's difficult to imagine all the ways in which people will use a program and all of the questions they might have.

    对于我们的示例,我们有两个强制性的位置参数和一个可选参数:

    • 点 1 经纬度
    • 点 2 经纬度
    • 可选距离

    我们可以将海里数作为一个方便的默认值,以便水手们得到他们需要的答案:

            parser.add_argument('-r', action='store', 
                    choices=('NM', 'MI', 'KM'), default='NM') 
            parser.add_argument('p1', action='store', type=point_type) 
            parser.add_argument('p2', action='store', type=point_type) 

    我们添加了两种论点。第一个是参数-r,它以-开头,将其标记为可选。有时,--与较长的名称一起使用。在某些情况下,我们将提供两种备选方案,如下所示:

            add_argument('--radius', '-r'....)

    操作是在命令行上存储-r后面的值。我们列出了三种可能的选择,并提供了一个默认值。如果输入不是这三个值之一,解析器将验证输入并写入适当的错误。

    提供的强制参数没有-前缀。我们使用了一个动作store;这是默认操作,实际上不需要声明。作为type参数提供的函数用于将源字符串转换为适当的 Python 对象。这也是验证复杂输入值的理想方法。我们将在本节中查看point_type()验证函数。

  4. Evaluate the parse_args() method of the parser object created in step 2:

            options = parser.parse_args() 

    默认情况下,这将使用来自sys.argv的值,这些值是用户输入的命令行参数值。如果需要以某种方式修改用户提供的命令行,则可以提供显式参数。

最后一个功能是:

    def get_options(): 
        parser = argparse.ArgumentParser() 
        parser.add_argument('-r', action='store', 
                choices=('NM', 'MI', 'KM'), default='NM') 
        parser.add_argument('p1', action='store', type=point_type) 
        parser.add_argument('p2', action='store', type=point_type) 
        options = parser.parse_args() 
        return options 

这依赖于point_type()验证功能。这是必需的,因为默认输入类型由str()函数定义。这确保了参数的值将是字符串对象。我们已经提供了type参数,以便可以注入类型转换。我们可以使用type = inttype = float转换为数字。

在我们的示例中,我们使用point_type()将字符串转换为(纬度经度)两个元组:

    def point_type(string): 
        try: 
            lat_str, lon_str = string.split(',') 
            lat = float(lat_str) 
            lon = float(lon_str) 
            return lat, lon 
        except Exception as ex: 
            raise argparse.ArgumentTypeError from ex 

此函数用于解析输入值。首先,它在,字符处分离两个值。它尝试对每个部分进行浮点转换。如果float()函数同时起作用,我们就有一个有效的纬度和经度,可以作为一对浮点值返回。

如果出现任何问题,将引发异常。从这个异常中,我们将引发一个ArgumentTypeError异常。argparse模块使用它向用户报告错误。

下面是结合选项解析器和输出显示功能的主脚本:

    if __name__ == "__main__": 
        options = get_options() 
        lat_1, lon_1 = options.p1 
        lat_2, lon_2 = options.p2 
        r = {'NM': NM, 'KM': KM, "MI": MI}[options.r] 
        display(lat_1, lon_1, lat_2, lon_2, r) 

此主脚本执行以下操作以将用户输入连接到显示的输出:

  1. 解析命令行选项。这些都存在于选项对象中。
  2. p1p2纬度经度两个元组展开为四个独立变量。
  3. 评估display()功能。

它是如何工作的。。。

参数解析器分三个阶段工作:

  1. 通过创建一个解析器对象作为ArgumentParser的实例来定义整个上下文。我们可以提供诸如总体计划说明等信息。我们还可以在这里提供格式化程序和其他选项。
  2. 使用add_argument()方法添加单个参数。这些参数可以包括可选参数和必需参数。每个参数可以有许多特性来提供不同种类的语法。我们将在*中查看一些备选方案,还有更多。。。*节。
  3. 解析实际的命令行输入。解析器的parse()方法将自动使用sys.argv。我们可以提供一个显式值,而不是sys.argv值。提供覆盖值的最常见原因是允许进行更完整的单元测试。

一些简单的程序将有几个可选参数。更复杂的程序可能有许多可选参数。

将文件名作为位置参数是很常见的。当程序读取一个或多个文件时,命令行上会提供文件名,如下所示:

python3 some_program.py *.rst

我们已经使用了 LinuxShell 的globbing功能,*.rst字符串被扩展为所有符合命名规则的文件的列表。可以使用定义如下的参数处理此文件列表:

    parser.add_argument('file', nargs='*') 

命令行上所有不以-字符开头的名称将被收集到解析器构建的对象中的file值中。

然后,我们可以使用以下方法:

    for filename in options.file: 
        process(filename) 

这将处理命令行中给出的每个文件。

对于 Windows 程序,shell 不全局运行,应用程序必须处理具有通配符模式的文件名。Pythonglob模块可以对此提供帮助。此外,pathlib模块还可以创建Path对象,其中包括球形特征。

我们可能需要做出更复杂的参数解析选项。非常复杂的应用程序可能有几十个单独的命令。以git版本控制程序为例;此应用程序使用几十个单独的命令,如git clonegit commitgit push。每个命令都有独特的参数解析要求。我们可以使用argparse创建这些命令及其不同参数集的复杂层次结构。

还有更多。。。

我们可以处理什么样的论点?有很多常用的参数样式。所有这些变体都是使用解析器的add_argument()方法定义的:

  • 简单选项-o--option参数通常用于启用或禁用程序的功能。这些通常通过action='store_true', default=Falseadd_argument()参数来实现。如果应用程序使用action='store_false', default=True,有时实现会更简单。选择默认值和存储值可以简化编程,但不会改变用户体验。
  • 带有非平凡对象的简单选项:用户认为这是简单的-o--option参数。我们可能希望使用一个更复杂的对象来实现这一点,它不是一个简单的布尔常量。我们可以使用action='store_const', const=some_object, default=another_object。由于模块、类和函数也是对象,因此这里提供了大量的复杂性。
  • 带有值的选项:我们将-r unit显示为一个参数,该参数接受要使用的单位的字符串名称。我们通过action='store'赋值来存储提供的字符串值来实现这一点。我们还可以使用type=function选项提供一个函数,用于验证或将输入转换为有用的形式。
  • 增加计数器的选项:一种常见的技术是有一个具有多个详细级别的调试日志。我们可以使用action='count', default=0来计算给定参数出现的次数。用户可以为详细输出提供-v,为非常详细的输出提供-vv。参数解析器将-vv视为-v参数的两个实例,这意味着该值将从初始值0增加到2
  • 累积列表的选项:我们可能有一个选项,用户可能希望为其提供多个值。例如,我们可以使用距离值列表。我们可以用action='append', default=[]来定义参数。这将允许用户说-r NM -r KM以海里和公里显示。当然,这需要对display()函数进行重大更改,以处理集合中的多个单元。
  • 显示帮助文本:如果我们什么也不做,则-h--help将显示帮助消息并退出。这将为用户提供有用的信息。如果需要的话,我们可以禁用它或更改参数字符串。这是一个广泛使用的约定,所以最好什么都不做,这就是我们程序的一个特点。
  • 显示版本号:通常使用--Version作为参数来显示版本号并退出。我们通过add_argument("--Version", action="version", version="v 3.14")实现这一点。我们提供了一个动作version和一个额外的关键字参数,用于设置要显示的版本。

这涵盖了命令行参数处理的大多数常见情况。通常,在编写自己的应用程序时,我们将尝试利用这些常见的参数样式。如果我们努力使用简单、广泛使用的参数样式,我们的用户就更有可能理解我们的应用程序是如何工作的。

有一些 Linux 命令具有更复杂的命令行语法。一些 Linux 程序,如findexpr具有argparse无法轻松处理的参数。对于这些边缘情况,我们需要直接使用sys.argv的值编写自己的解析器。

另见

  • 我们研究了如何在中使用 input()和 getpass()获取交互用户输入配方
  • 我们将在中使用操作系统环境设置的方法来为这一点增加更多的灵活性

使用 cmd 创建命令行应用程序

有几种方法可以创建交互式应用程序。使用 input()和 getpass()进行用户输入配方查看了input()getpass.getpass()等函数。使用**argparse 获取命令行输入配方展示了如何使用argparse创建应用程序,用户可以通过操作系统命令行与之交互。

我们有第三种使用cmd模块创建交互式应用程序的方法。此模块将提示用户输入,然后调用我们提供的类的特定方法。

这与第 7 章更高级的类设计中的材料有关。我们将向类定义中添加特性以创建唯一的子类。

下面是交互的外观,我们将用户输入标记为:“help

Starting with 100 

Roulette> 
 **`help`** 

Documented commands (type help <topic>): 

========================================

bet  help 

Undocumented commands: 

====================== 

done  spin  stake 

Roulette> 

help bet

 Bet <name> <amount> 

 Name is one of even, odd, red, black, high, or low 

Roulette> 
 **`bet black 1`** 

Roulette> 
 **`bet even 1`** 

Roulette> 
 **`spin`** 

Spin ('21', {'red', 'high', 'odd'}) 

Lose even 

Lose black 

... more interaction ... 

Roulette> 
 **`done`** 

Ending with 93

应用程序中有一条介绍性消息。它显示了玩家的起始赌注,即他们必须下注多少。应用程序将显示提示Roulette>。然后,用户可以输入五个可用命令中的任意一个。

当我们输入help作为命令时,我们会看到可用命令的显示。只有两个有任何文件。其他三人没有进一步的详细资料。

当我们进入help bet时,我们会看到bet命令的详细文档。描述告诉我们从可用的六个选项中提供一个下注名称和下注金额。

我们创建了两个赌注,一个赌黑色,一个赌偶数。然后我们输入spin命令来旋转轮子。这将显示结果数字21——红色、高位和奇数。我们的赌注都是输的。

我们也省略了一些不太成功的互动。当我们进入done命令时,显示了最后一根桩。如果模拟更详细,它还可能显示一些有关旋转、赢和输的聚合统计信息。

准备好了吗

cmd.Cmd应用程序的核心功能是读取评估打印循环REPL)。当存在大量单独的状态更改和大量用于进行这些状态更改的命令时,这种应用程序运行良好。

我们将使用轮盘赌中赌注子集的简单模拟作为示例。其想法是允许用户创建一个或多个赌注,然后旋转模拟的轮盘赌轮。正当赌场轮盘赌有一系列令人眼花缭乱的可能赌注时,我们将只关注六种:

  • 红色,黑色
  • 偶数,奇数
  • 高,低

一个美国轮盘赌轮子有 38 个箱子。数字 1 到 36 分别为红色和黑色。还有另外两个箱子,零和双零,它们是绿色的。这两个额外的箱子被定义为既不偶数也不奇数,既不高也不低。在零上下注的方法只有几种,但在数字上下注的方法有很多。

我们将使用一些辅助函数来表示轮盘赌轮,这些辅助函数用于构建一个箱子集合。每个箱子都有一个显示号码的字符串和一组中奖者的赌名。

我们可以使用一些简单的规则定义一个通用 bin,以确定哪些赌注在中奖集中:

    red_bins = (1, 3, 5, 7, 9, 12, 14, 16, 18, 
        21, 23, 25, 27, 28, 30, 32, 34, 36) 

    def roulette_bin(i): 
        return str(i), { 
            'even' if i%2 == 0 else 'odd', 
            'low'  if 1 <= i < 19 else 'high', 
            'red'  if i in red_bins else 'black' 
        } 

roulette_bin()函数返回一个两元组,其中包含 bin 编号的字符串表示形式和一组三个获胜命题。

对于000,我们需要一些不同的东西:

    def zero_bin(): 
        return '0', set() 

    def zerozero_bin(): 
        return '00', set() 

zero_bin()函数返回一个字符串箱号和一个空集。zerozero_bin()函数返回一个特殊字符串,表示它是00,加上空集,表示定义的赌注都不是赢家。

我们可以结合这三个函数的结果来创建一个完整的轮盘赌轮。整个轮子将被建模为一个 bin 元组列表:

    def wheel(): 
        b0 = [zero_bin()] 
        b00 = [zerozero_bin()] 
        b1_36 = [ 
            roulette_bin(i) for i in range(1,37) 
        ] 
        return b0+b00+b1_36 

我们已经建立了一个简单的列表,其中包含一整套垃圾箱:一个零,一个双零,以及数字 1 到 36。我们现在可以使用random.choice()功能随机选择一个箱子。这将告诉我们哪些赌注赢了,哪些赌注输了。

怎么做。。。

  1. 导入 cmd 模块:

            import cmd 
  2. 定义对cmd.Cmd

            class Roulette(cmd.Cmd): 

    的扩展

  3. Define any initialization required in the preloop() method:

                def preloop(self): 
                    self.bets = {} 
                    self.stake = 100 
                    self.wheel = wheel() 

    preloop()方法在处理开始时仅评估一次。我们已经使用它初始化了一个字典,用于下注和玩家的赌注。我们还创建了 wheel 集合的一个实例。自参数是类中方法的一个要求。现在,这只是一个简单的语法要求。在第 6 章类和对象的基础中,我们将更仔细地了解这一点。

    请注意,这是在class语句中缩进的。

    也可以通过__init__()方法进行初始化。不过,这有点复杂,因为我们必须使用super()来确保首先完成Cmd类的初始化。

  4. 对于每个命令,创建一个do_command()方法。方法的名称将是命令,前缀为do_。命令后的用户输入文本将作为参数值提供给方法。下面是两个关于bet命令和spin命令的示例:

                def do_bet(self, arg_string): 
                    pass 
                def do_spin(self, arg_string): 
                    pass 
  5. Parse and validate the arguments to each command. The user's input after the command will be provided as the value of the first positional argument to the method.

    如果参数无效,该方法应打印一条消息并返回。如果参数有效,则该方法可以继续执行验证步骤。

    对于我们的示例,spin命令不需要任何输入。我们可以忽略参数字符串。更完整地说,如果字符串非空,我们可能希望显示一个错误。

    然而,bet命令确实有一个下注,它必须是六个有效下注名称之一。我们可能需要检查重复下注。我们可能还想检查缩写的赌注名称。六个赌注中的每一个都有一个唯一的第一个字母。

    作为扩展,下注也可以有一个金额。我们在第 1 章数字、字符串和元组中的字符串解析正则表达式配方中研究了字符串解析。对于本例,我们只需处理下注的名称:

                def do_spin(self, arg_string): 
                    if len(self.bets) == 0: 
                        print("No bets have been placed") 
                        return 
                    # Happy path: more goes here. 
    
                BET_NAMES = set(['even', 'odd', 'high', 'low', 'red', 'black']) 
    
                def do_bet(self, arg_string): 
                    if arg_string not in BET_NAMES: 
                        print("{0} is not a valid bet".format(arg_string)) 
                        return 
                    # Happy path: more goes here. 
  6. Write the happy path processing for each command. For our example, the spin command will resolve the bets. The bet command will accumulate another bet. Here's the do_bet() happy path:

            self.bets[arg_string] = 1 

    我们已将用户的赌注添加到带有金额的self.bets映射中。在本例中,我们将所有赌注视为具有相同的最小金额。

  7. Here's the do_spin() happy path that resolves all of the bets:

            self.spin = random.choice(self.wheel) 
            print("Spin", self.spin) 
            label, winners = self.spin 
            for b in self.bets: 
                if b in winners: 
                    self.stake += self.bets[b] 
                    print("Win", b) 
                else: 
                    self.stake -= self.bets[b] 
                    print("Lose", b) 
            self.bets= {} 

    首先,我们旋转方向盘以赢得赌注。然后,我们检查了每个玩家的赌注,看哪一个与中奖赌注匹配。如果玩家的赌注b在中奖赌注中,我们将增加他们的赌注。否则,我们将减少他们的股份。

    本例中的所有赌注支付 1:1。如果我们想将这个例子推广到其他类型的赌注,我们必须为各种赌注提供适当的赔率。

  8. Write the main script. This will create an instance of this class and execute the cmdloop() method:

            if __name__ == "__main__": 
                r = Roulette() 
                r.cmdloop() 

    我们已经创建了CmdRoulette子类的一个实例。当我们执行cmdloop()方法时,该类将编写已提供的任何介绍性消息,编写提示符,并读取命令。

它是如何工作的。。。

Cmd模块包含大量内置功能,用于显示提示、读取用户输入,然后根据用户输入确定正确的方法。

例如,当我们输入bet black时,Cmd超类的内置方法将从输入中删除第一个单词bet,并在其前面加上do_,然后评估实现该命令的方法。

如果没有do_bet()方法,则命令处理器写入错误消息。这是自动完成的,我们根本不需要编写任何代码。

因为我们编写了一个do_bet()方法,所以将调用它。在本例中,命令black后的文本将作为位置参数值提供。

一些方法,如do_help()已经是应用程序的一部分。这些方法将总结其他do_*方法。当我们的一个方法有一个 docstring 时,它可以通过内置的帮助功能来显示。

Cmd类依赖 Python 的内省功能。该类的实例可以检查方法名称,以查找所有以do_开头的方法。它们在类级别__dict__属性中可用。内省是一个高级主题,将在第 7 章更高级的类设计中涉及。

还有更多。。。

Cmd类有许多额外的地方,我们可以在这些地方添加交互功能:

  • 我们可以定义help_*()方法,这些方法将成为杂项帮助主题的一部分。
  • 当任何do_*方法返回值时,循环将结束。我们可能想添加一个do_quit()方法,它的主体是return True。这将结束命令处理循环。
  • 我们可以提供一个名为emptyline()的方法来响应空行。一个选择是什么都不做。另一个常见的选择是在用户未输入命令时执行默认操作。
  • 当用户的输入与任何do_*方法不匹配时,对default()方法进行评估。这可能用于更高级的输入解析。
  • postloop()方法可用于在循环完成后进行一些处理。这将是一个写总结的好地方。这还需要一个do_*方法,该方法返回任何非False值的值以结束命令循环。

此外,我们还可以设置许多属性。这些是与方法定义对等的类级变量:

  • prompt属性是要写入的提示字符串。对于我们的示例,我们可以执行以下操作:

            class Roulette(cmd.Cmd): 
                prompt="Roulette> " 
  • intro属性是介绍性消息。

  • 我们可以通过设置doc_headerundoc_headermisc_headerruler属性来定制帮助输出。这些都将改变帮助输出的外观。

目标是能够创建一个整洁的类,以简单灵活的方式处理用户交互。这个类创建了一个应用程序,该应用程序具有许多与 Python 的 REPL 相同的特性。它还具有许多提示用户输入的命令行程序的共同特性。

这些交互式应用程序的一个例子是 Linux 中的命令行 FTP 客户端。它有一个提示ftp>,并解析几十个单独的 FTP 命令。输入help将显示 FTP 交互中的所有内部命令。

另见

  • 我们将在第 6 章类和对象的基础第 7 章更高级的类设计中查看类定义

使用操作系统环境设置

有几种方法可以查看用户输入的时间跨度:

  • 交互数据:这是用户在一种现在的时间跨度内提供的。
  • 程序启动时提供的命令行参数:这些值通常跨越程序的一次完整执行。
  • 在操作系统级别设置的环境变量:可以在命令行中设置这些变量,使它们几乎与启动应用程序的命令一样具有交互性:
    • 它们可以在.bashrc文件或.profile文件中为用户配置。这使得它们比命令行更持久,交互性稍差。
    • 在 Windows 中,有高级设置选项,允许用户设置长期配置。这些通常是程序多次执行的输入。
  • 配置文件设置:这些设置因应用程序而异。其思想是编辑一个文件,并使这些选项或参数长期可用。这些可能适用于多个用户,甚至适用于所有用户。配置文件的时间跨度通常最长。

使用 input()和 getpass()进行用户输入使用 cmd 创建命令行应用程序配方中,我们考察了与用户的交互。在使用**argparse 获取命令行输入配方中,我们了解了如何处理命令行参数。我们来看看第 13 章应用集成中的配置文件。

环境变量可通过os模块获得。我们如何根据这些操作系统级别的设置来配置应用程序?

准备好了吗

我们可能希望通过操作系统设置向程序提供各种类型的信息。这里有一个深刻的限制:操作系统设置只能是字符串值。这意味着许多类型的设置都需要一些代码来解析值并从字符串中创建适当的 Python 对象。

当我们使用argparse解析命令行参数时,这个模块可以为我们做一些数据转换。当我们使用os处理环境变量时;我们必须自己进行转换。

使用 argparse 获取命令行输入配方中,我们将haversine()函数封装在一个简单的应用程序中,该应用程序解析命令行参数。

在操作系统级别,我们创建了一个如下工作的程序:

slott$ python3 ch05_r04.py -r KM 36.12,-86.67 33.94,-118.40

From (36.12, -86.67) to (33.94, -118.4) in KM = 2887.35

在使用了一段时间后,我们发现我们经常使用海里来计算我们的船锚定的距离。我们希望其中一个输入点和-r参数都有默认值。

由于船可以锚定在不同的位置,我们需要更改默认值,而无需调整实际代码。

我们将使用距离单位设置 OS 环境变量UNITS。我们可以用原点设置另一个变量HOME_PORT。我们希望能够做到以下几点:

slott$ UNITS=NM

slott$ HOME_PORT=36.842952,-76.300171

slott$ python3 ch05_r06.py 36.12,-86.67

From 36.12,-86.67 to 36.842952,-76.300171 in NM = 502.23

单位和原点值通过操作系统环境提供给应用程序。这可以在配置文件中设置,以便我们可以轻松地进行更改。也可以手动设置,如示例所示。

怎么做。。。

  1. 导入os模块。操作系统环境可通过此模块访问:

            import os 
  2. 导入应用程序所需的任何其他类或对象:

            from ch03_r05 import haversine, MI, NM, KM 
  3. 定义一个函数,该函数将使用环境值作为可选命令行参数的默认值。要解析的默认参数集来自sys.argv,因此导入sys模块

            def get_options(argv=sys.argv): 

    也很重要

  4. Gather default values from the OS environment settings. This includes any validation required:

            default_units = os.environ.get('UNITS', 'KM') 
            if default_units not in ('KM', 'NM', 'MI'): 
                sys.exit("Invalid value for UNITS, not KM, NM, or MI") 
            default_home_port = os.environ.get('HOME_PORT') 

    sys.exit()函数可以很好地处理错误处理。它将打印消息并以非零状态代码退出。

  5. 创建parser属性。为相关参数提供任何默认值。这取决于argparse模块,该模块也必须导入:

                      parser = argparse.ArgumentParser() 
            parser.add_argument('-r', action='store', 
                choices=('NM', 'MI', 'KM'), default=default_units) 
            parser.add_argument('p1', action='store', type=point_type) 
            parser.add_argument('p2', nargs='?', action='store', type=point_type, 
                default=default_home_port) 
            options = parser.parse_args(argv[1:]) 
  6. 执行任何附加验证以确保参数设置正确。在本例中,HOME_PORT可能没有值,第二个命令行参数也可能没有值。这需要一个if语句并调用sys.exit()

                    if options.p2 is None: 
                    sys.exit("Neither HOME_PORT nor p2 argument provided.") 
  7. 返回带有一组有效参数的options对象:

            return options 

这将允许-r参数和第二点是完全可选的。如果命令行中省略了缺省值,参数解析器将使用配置信息来提供缺省值。

使用Using argparse 获取命令行输入方法,以处理get_options()函数创建的选项。

它是如何工作的。。。

我们已经使用 OS 环境变量来创建可以被命令行参数覆盖的默认值。如果设置了环境变量,则该字符串将作为参数定义的默认值提供。如果未设置环境变量,则使用应用程序级默认值。

UNITS变量的实例中,应用程序使用 km 作为默认值,如果不是,则设置 OS 环境变量。

这为我们提供了三层交互:

  • 我们可以在.bashrc文件中定义一个设置。或者,我们可以使用 Windows高级设置选项进行持久性更改。每次登录或创建新命令窗口时都将使用此值。
  • 我们可以在命令行以交互方式设置操作系统环境。这将持续我们会议的时间。当我们注销或关闭命令窗口时,该值将丢失。
  • 每次程序运行时,我们都可以通过命令行参数提供唯一的值。

请注意,从环境变量检索的值没有内置或自动验证。我们需要验证这些字符串以确保它们是有意义的。

还要注意,我们在几个地方重复了有效单位的列表。这违反了不要重复自己干燥原则。具有此列表的全局变量是一个很好的改进。

还有更多。。。

使用 argparse 获取命令行输入的方法显示了一种稍微不同的方法来处理sys.argv中提供的默认命令行参数。第一个参数是正在执行的 Python 应用程序的名称,通常与参数解析无关。

sys.argv的值将是字符串列表,如下所示:

    ['ch05_r06.py', '-r', 'NM', '36.12,-86.67'] 

我们必须在处理过程中的某个时刻跳过sys.argv[0]中的初始值。我们有两个选择:

  • 在这个配方中,我们在解析过程中尽可能晚地丢弃多余的项。向解析器提供sys.argv[1:]时跳过第一项。
  • 在上一个示例中,我们在处理过程中放弃了该值。main()函数使用options = get_options(sys.argv[1:])向解析器提供较短的列表。

通常,这两种方法之间唯一相关的区别取决于单元测试的数量和复杂性。此配方将需要一个包含初始参数字符串的单元测试,该字符串将在解析过程中被丢弃。

另见

  • 我们将在第 13 章应用程序集成中介绍处理配置文件的多种方法