Python 自动化指南(繁琐工作自动化)第二版:十一调试

Posted 布客飞龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 自动化指南(繁琐工作自动化)第二版:十一调试相关的知识,希望对你有一定的参考价值。

原文:https://automatetheboringstuff.com/2e/chapter11/

既然你已经知道了足够多的知识来编写更复杂的程序,你可能会开始发现其中不那么简单的错误。这一章介绍了一些工具和技术,用于查找程序中错误的根本原因,帮助您更快、更省力地修复错误。

套用程序员之间的一个老笑话,写代码占编程的 90%。调试代码占其余的 90%。

你的电脑只会做你让它做的事情;它不会读取你的想法,做你想让它做的事情。即使是专业的程序员也会一直制造 bug,所以如果你的程序有问题也不要气馁。

幸运的是,有一些工具和技术可以确定您的代码到底在做什么以及哪里出错了。首先,您将看到日志记录和断言,这两个特性可以帮助您尽早发现 bug。一般来说,越早发现错误,就越容易修复。

其次,您将了解如何使用调试器。调试器是 Mu 的一个特性,它一次执行一条程序指令,让您有机会在代码运行时检查变量的值,并跟踪这些值在程序过程中是如何变化的。这比全速运行程序要慢得多,但它有助于在程序运行时看到程序中的实际值,而不是从源代码中推断出可能的值。

引发异常

每当 Python 试图执行无效代码时,都会引发异常。在第 3 章的中,你读到了如何用tryexcept语句处理 Python 的异常,这样你的程序就可以从你预期的异常中恢复。但是您也可以在代码中引发自己的异常。引发异常是一种说法,“停止运行这个函数中的代码,将程序执行移到except语句中”。

异常由一个raise语句引发。在代码中,raise语句由以下内容组成:

  • raise关键字
  • Exception()函数的调用
  • 传递给Exception()函数的带有有用错误消息的字符串

例如,在交互式 Shell 中输入以下内容:

>>> raise Exception('This is the error message.')
Traceback (most recent call last):
  File "<pyshell#191>", line 1, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

如果没有包含引发异常的raise语句的tryexcept语句,程序就会崩溃并显示异常的错误信息。

通常,知道如何处理异常的是调用函数的代码,而不是函数本身。这意味着你通常会在函数中看到一个raise语句,在调用函数的代码中看到tryexcept语句。例如,打开一个新的文件编辑器选项卡,输入以下代码,并将程序保存为boxPrint.py :

def boxPrint(symbol, width, height):
    if len(symbol) != 1:
         raise Exception('Symbol must be a single character string.') # ➊
    if width <= 2:
         raise Exception('Width must be greater than 2.') # ➋
    if height <= 2:
         raise Exception('Height must be greater than 2.') # ➌
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        boxPrint(sym, w, h)
     except Exception as err: # ➍
         print('An exception happened: ' + str(err)) # ➎

您可以在autbor.com/boxprint查看该程序的执行情况。这里我们定义了一个boxPrint()函数,它接受一个字符、一个宽度和一个高度,并使用该字符制作一个具有该宽度和高度的小盒子的图片。这个盒子形状被打印到屏幕上。

假设我们希望字符是单个字符,宽度和高度大于 2。如果这些需求没有得到满足,我们添加if语句来引发异常。后来,当我们用各种参数调用boxPrint()时,我们的try/except会处理无效的参数。

这个程序使用了except语句 ➍ 的except Exception as err形式。如果从boxPrint()➊➋➌返回一个Exception对象,这个except语句会将它存储在一个名为err的变量中。然后,我们可以通过将Exception对象传递给str()来将其转换为一个字符串,从而产生一个用户友好的错误消息 ➎。当您运行这个boxPrint.py时,输出将如下所示:

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

使用tryexcept语句,您可以更优雅地处理错误,而不是让整个程序崩溃。

获取字符串形式的回溯

当 Python 遇到错误时,它会产生一个称为回溯的错误信息宝库。回溯包括错误消息、导致错误的行的行号以及导致错误的函数调用序列。这个调用序列被称为调用栈

在 Mu 中打开一个新的文件编辑器页签,进入如下程序,保存为errorExample.py :

def spam():
    bacon()
def bacon():
    raise Exception('This is the error message.')
spam()

当您运行errorExample.py时,输出将如下所示:

Traceback (most recent call last):
  File "errorExample.py", line 7, in <module>
    spam()
  File "errorExample.py", line 2, in spam
    bacon()
  File "errorExample.py", line 5, in bacon
    raise Exception('This is the error message.')
Exception: This is the error message.

从回溯中,您可以看到错误发生在第 5 行的bacon()函数中。这个对bacon()的调用来自第 2 行的spam()函数,该函数在第 7 行被调用。在可以从多个地方调用函数的程序中,调用栈可以帮助您确定哪个调用导致了错误。

每当出现未处理的异常时,Python 都会显示回溯。但是您也可以通过调用traceback.format_exc()以字符串的形式获取它。如果您想从异常的回溯中获得信息,但又想让一个except语句优雅地处理异常,那么这个函数非常有用。在调用这个函数之前,你需要导入 Python 的traceback模块。

例如,您可以将回溯信息写入一个文本文件并保持程序运行,而不是在发生异常时立即使程序崩溃。当您准备调试程序时,可以稍后查看该文本文件。在交互式 Shell 中输入以下内容:

>>> import traceback
>>> try:
...          raise Exception('This is the error message.')
except:
...          errorFile = open('errorInfo.txt', 'w')
...          errorFile.write(traceback.format_exc())
...          errorFile.close()
...          print('The traceback info was written to errorInfo.txt.')
111
The traceback info was written to errorInfo.txt.

111是来自write()方法的返回值,因为有 111 个字符被写入文件。追溯文本被写入errorInfo.txt

Traceback (most recent call last):
  File "<pyshell#28>", line 2, in <module>
Exception: This is the error message.

在第 255 页的的日志中,您将学习如何使用logging模块,这比简单地将错误信息写入文本文件更有效。

断言

断言是一个健全检查,以确保你的代码没有做一些明显错误的事情。这些健全性检查是由assert语句执行的。如果健全性检查失败,则引发一个AssertionError异常。在代码中,assert语句由以下内容组成:

  • assert关键字
  • 条件(即计算结果为TrueFalse的表达式)
  • 逗号
  • 条件为False时显示的字符串

用简单的英语来说,一个assert语句说,“我断言条件成立,如果不成立,那么某个地方有一个 bug,所以立即停止程序”。例如,在交互式 Shell 中输入以下内容:

>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.sort()
>>> ages
[15, 17, 22, 26, 47, 54, 57, 73, 80, 92]
>>> assert
ages[0] <= ages[-1] # Assert that the first age is <= the last age.

这里的assert语句断言ages中的第一项应该小于或等于最后一项。这是一个健全性检查;如果sort()中的代码没有 bug,并且完成了它的工作,那么这个断言就是真的。

因为ages[0] <= ages[-1]表达式的计算结果是True,所以assert语句什么也不做。

然而,让我们假设我们的代码中有一个 bug。假设我们不小心调用了reverse()列表方法,而不是sort()列表方法。当我们在交互式 Shell 中输入以下内容时,assert语句会引发一个AssertionError:

>>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73]
>>> ages.reverse()
>>> ages
[73, 47, 80, 17, 15, 22, 54, 92, 57, 26]
>>> assert ages[0] <= ages[-1] # Assert that the first age is <= the last age.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

与异常不同,你的代码不应该tryexcept处理assert语句;如果一个assert失败,你的程序应该崩溃。通过像这样的“快速失败”,您缩短了从错误的最初原因到您第一次注意到错误之间的时间。这将减少您在找到错误原因之前必须检查的代码量。

断言是针对程序员的错误,而不是用户的错误。只有当程序正在开发时,断言才应该失败;用户永远不会在完成的程序中看到断言错误。对于程序在正常操作中可能遇到的错误(比如找不到文件或者用户输入了无效数据),抛出一个异常,而不是用assert语句检测它。您不应该使用assert语句来代替引发异常,因为用户可以选择关闭断言。如果用python -O myscript.py而不是python myscript.py运行 Python 脚本,Python 会跳过assert语句。当用户开发一个程序并需要在一个要求最高性能的生产环境中运行它时,他们可能会禁用断言。(尽管,在许多情况下,他们会让断言保持启用状态。)

断言也不能代替全面的测试。例如,如果前面的ages例子被设置为[10, 3, 2, 1, 20],那么assert ages[0] <= ages[-1]断言不会注意到列表是未排序的,因为它的第一个年龄恰好小于或等于最后一个年龄,这是断言检查的唯一内容。

在交通灯模拟中使用断言

假设您正在构建一个交通灯模拟程序。表示十字路口停车灯的数据结构是一个字典,带有关键字'ns''ew',分别表示面向南北和东西的停车灯。这些键的值将是字符串'green''yellow''red'中的一个。代码看起来会像这样:

market_2nd = 'ns': 'green', 'ew': 'red'
mission_16th = 'ns': 'red', 'ew': 'green'

这两个变量将用于市场街和第二街以及教会街和第十六街的交叉口。要启动该项目,您需要编写一个switchLights()函数,该函数将以一个交集字典作为参数,并切换灯光。

起初,你可能认为switchLights()应该简单地将每种光切换到序列中的下一种颜色:任何'green'值都应该更改为'yellow' , 'yellow'值应该更改为'red','red'值应该更改为'green'。实现这一想法的代码可能如下所示:

def switchLights(stoplight):
    for key in stoplight.keys():
        if stoplight[key] == 'green':
            stoplight[key] = 'yellow'
        elif stoplight[key] == 'yellow':
            stoplight[key] = 'red'
        elif stoplight[key] == 'red':
            stoplight[key] = 'green'
switchLights(market_2nd)

您可能已经看到了这段代码的问题,但是让我们假设您编写了模拟代码的其余部分,长达数千行,而没有注意到它。当你最终运行模拟时,程序不会崩溃,但你的虚拟汽车会崩溃!

因为您已经编写了程序的其余部分,所以您不知道 BUG 可能在哪里。也许是在模拟汽车的代码中,或者是在模拟虚拟司机的代码中。追踪这个错误到switchLights()函数可能需要几个小时。

但是,如果在编写switchLights()时,您添加了一个断言来检查至少有一个灯始终是红色的,您可能会在函数的底部包含以下内容:

assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight)

有了这个断言,您的程序将崩溃,并显示以下错误消息:

   Traceback (most recent call last):
     File "carSim.py", line 14, in <module>
       switchLights(market_2nd)
     File "carSim.py", line 13, in switchLights
       assert 'red' in stoplight.values(), 'Neither light is red! ' +
   str(stoplight)
   AssertionError: Neither light is red! 'ns': 'yellow', 'ew': 'green' # ➊

这里重要的一行是AssertionError➊。虽然您的程序崩溃并不理想,但它会立即指出健全性检查失败:两个方向的流量都没有红灯,这意味着流量可能是双向的。通过在程序执行的早期快速失败,您可以为自己节省很多未来的调试工作。

日志

如果您曾经在程序运行时在代码中放入一个print()语句来输出某个变量的值,那么您已经使用了一种形式的日志来调试您的代码。日志记录是了解程序中发生了什么以及发生的顺序的好方法。Python 的logging模块使得创建您编写的定制消息的记录变得容易。这些日志消息将描述程序执行到达日志函数调用的时间,并列出您在该时间点指定的任何变量。另一方面,缺失的日志消息表明部分代码被跳过并且从未执行过。

使用logging模块

要使logging模块在程序运行时在屏幕上显示日志消息,请将以下内容复制到程序顶部(但在#! python shebang 行下):

import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s -  %(levelname)
s -  %(message)s')

您不需要太担心这是如何工作的,但基本上,当 Python 记录一个事件时,它会创建一个保存该事件信息的LogRecord对象。logging模块的basicConfig()函数让您指定想要查看的LogRecord对象的详细信息以及如何显示这些详细信息。

假设你写了一个函数来计算一个数的阶乘。在数学中,阶乘 4 是1 × 2 × 3 × 4,即 24。阶乘 7 是1 × 2 × 3 × 4 × 5 × 6 × 7,即 5040。打开一个新的文件编辑器选项卡,并输入以下代码。它有一个错误,但是您也将输入几个日志消息来帮助您自己找出哪里出错了。将程序另存为factorialLog.py

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s -  %(levelname)s
-  %(message)s')
logging.debug('Start of program')
def factorial(n):
    logging.debug('Start of factorial(%s%%)'  % (n))
    total = 1
    for i in range(n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s%%)'  % (n))
    return total
print(factorial(5))
logging.debug('End of program')

这里,当我们想要打印日志信息时,我们使用logging.debug()函数。这个debug()函数会调用basicConfig(),打印一行信息。该信息将采用我们在basicConfig()中指定的格式,并将包括我们传递给debug()的消息。print(factorial(5))调用是原始程序的一部分,因此即使日志消息被禁用,结果也会显示。

这个程序的输出如下所示:

2019-05-23 16:20:12,664 - DEBUG - Start of program
2019-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2019-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2019-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2019-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2019-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2019-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2019-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2019-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2019-05-23 16:20:12,684 - DEBUG - End of program

factorial()函数返回0作为5的阶乘,这是不对的。for循环应该将total中的值乘以从15的数字。但是logging.debug()显示的日志信息显示i变量开始于0而不是1。因为零乘以任何东西都是零,所以剩余的迭代对于total也具有错误的值。日志消息提供了一系列线索,可以帮助您找出事情开始出错的时间。

for i in range(n + 1):行改为for i in range( 1、n + 1):,再次运行程序。输出将如下所示:

2019-05-23 17:13:40,650 - DEBUG - Start of program
2019-05-23 17:13:40,651 - DEBUG - Start of factorial(5)
2019-05-23 17:13:40,651 - DEBUG - i is 1, total is 1
2019-05-23 17:13:40,654 - DEBUG - i is 2, total is 2
2019-05-23 17:13:40,656 - DEBUG - i is 3, total is 6
2019-05-23 17:13:40,659 - DEBUG - i is 4, total is 24
2019-05-23 17:13:40,661 - DEBUG - i is 5, total is 120
2019-05-23 17:13:40,661 - DEBUG - End of factorial(5)
120
2019-05-23 17:13:40,666 - DEBUG - End of program

factorial(5)调用正确返回120。日志消息显示了循环内部发生了什么,这直接导致了 bug。

您可以看到,logging.debug()调用不仅打印出传递给它们的字符串,还打印出时间戳和单词DEBUG

不用print()函数进行调试

键入import logginglogging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')有些笨拙。你可能想用print()通话来代替,但是不要屈服于这种诱惑!一旦你完成了调试,你将花费大量的时间从你的代码中为每个日志消息删除print()调用。你甚至可能不小心移除了一些用于非日志信息的print()调用。日志消息的好处在于,你可以随意在你的程序中填入你喜欢的数量,并且你可以随时通过添加一个logging.disable(logging.CRITICAL)调用来禁用它们。与print()不同,logging模块使得在显示和隐藏日志消息之间切换变得容易。

日志消息是给程序员看的,不是给用户看的。用户不会关心你需要看到的帮助调试的一些字典值的内容;使用日志消息来做类似的事情。对于用户希望看到的消息,如文件未找到输入无效,请输入一个号码,您应该使用print()调用。禁用日志消息后,您不希望剥夺用户的有用信息。

日志级别

日志级别提供了一种根据重要性对日志消息进行分类的方法。共有五个日志级别,在表 11-1 中从最不重要到最重要进行了描述。可以使用不同的日志记录函数在每个级别记录消息。

表 11-1:Python 中的日志记录级别

级别记录函数描述
调试logging.debug()最低级别。用于小细节。通常您只在诊断问题时才关心这些消息。
信息logging.info()用于记录程序中一般事件的信息,或者确认程序中的事情正在正常进行。
警告logging.warning()用于表示一个潜在的问题,该问题不会阻止程序运行,但将来可能会阻止程序运行。
错误logging.error()用于记录导致程序无法执行某项操作的错误。
严重logging.critical()最高级别。用于指示已经导致或即将导致程序完全停止运行的致命错误。

您的日志消息作为字符串传递给这些函数。日志记录级别只是建议。最终,由您来决定您的日志消息属于哪一类。在交互式 Shell 中输入以下内容:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s -
%(levelname)s -  %(message)s')
>>> logging.debug('Some debugging details.')
2019-05-18 19:04:26,901 - DEBUG - Some debugging details.
>>> logging.info('The logging module is working.')
2019-05-18 19:04:35,569 - INFO - The logging module is working.
>>> logging.warning('An error message is about to be logged.')
2019-05-18 19:04:56,843 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2019-05-18 19:05:07,737 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2019-05-18 19:05:45,794 - CRITICAL - The program is unable to recover!

日志级别的好处是,您可以更改希望看到的日志消息的优先级。将logg

Python 自动化指南(繁琐工作自动化)第二版:八输入验证

原文:https://automatetheboringstuff.com/2e/chapter8/

输入验证代码检查用户输入的值,比如来自input()函数的文本,格式是否正确。例如,如果您希望用户输入他们的年龄,您的代码不应该接受无意义的答案,如负数(在可接受的整数范围之外)或单词(这是错误的数据类型)。输入验证还可以防止错误或安全漏洞。如果您实现了一个withdrawFromAccount()函数,该函数接受一个参数作为要从帐户中减去的金额,那么您需要确保该金额是一个正数。如果withdrawFromAccount()函数从账户中减去一个负数,那么“取款”将会增加钱!

通常,我们通过反复要求用户输入来执行输入验证,直到他们输入有效文本,如下例所示:

while True:
    print('Enter your age:')
    age = input()
    try:
        age = int(age)
    except:
        print('Please use numeric digits.')
        continue
    if age < 1:
        print('Please enter a positive number.')
        continue
    break
print(f'Your age is age.')

当您运行此程序时,输出可能如下所示:

Enter your age:
five
Please use numeric digits.
Enter your age:
-2
Please enter a positive number.
Enter your age:
30
Your age is 30.

当您运行此代码时,系统会提示您输入年龄,直到您输入一个有效的年龄。这确保了当执行离开while循环时,age变量将包含一个不会在以后使程序崩溃的有效值。

然而,为程序中的每个input()调用编写输入验证代码很快就变得乏味了。此外,您可能会错过某些情况,并允许无效的输入通过您的检查。在本章中,您将学习如何使用第三方 PyInputPlus 模块进行输入验证。

PyInputPlus 模块

PyInputPlus 包含类似于input()的函数,用于几种数据:数字、日期、电子邮件地址等等。如果用户输入了无效的输入,比如格式错误的日期或超出预期范围的数字,PyInputPlus 将重新提示用户输入,就像上一节中我们的代码所做的那样。PyInputPlus 还有其他有用的特性,比如限制它重新提示用户的次数,如果要求用户在限定的时间内做出响应,还会超时。

PyInputPlus 不是 Python 标准库的一部分,所以必须使用 PIP 单独安装。要安装 PyInputPlus,请从命令行运行pip install --user pyinputplus。附录 A 有安装第三方模块的完整说明。要检查 PyInputPlus 是否安装正确,请在交互式 Shell 中导入它:

>>> import pyinputplus

如果在导入模块时没有出现错误,则说明该模块已成功安装。

PyInputPlus 有几个用于不同类型输入的函数:

inputStr()类似于内置的input()函数,但是具有 PyInputPlus 的一般特性。您还可以向它传递一个自定义验证函数

inputNum()确保用户输入一个数字并返回一个intfloat,这取决于数字中是否有小数点

inputChoice()确保用户输入所提供的选项之一

inputMenu()类似于inputChoice(),但是提供了一个带有数字或字母选项的菜单

inputDatetime()确保用户输入日期和时间

inputYesNo()确保用户输入“是”或“否”的回答

inputBool()inputYesNo()类似,但是接受“真”或“假”响应并返回一个布尔值

inputEmail()确保用户输入有效的电子邮件地址

inputFilepath()确保用户输入有效的文件路径和文件名,并且可以选择性地检查具有该名称的文件是否存在

inputPassword()类似于内置的input(),但是在用户输入时显示*字符,这样密码或其他敏感信息就不会显示在屏幕上

只要用户输入无效的输入,这些函数就会自动重新提示用户:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum()
five
'five' is not a number.
42
>>> response
42

每次我们想调用 PyInputPlus 函数时,import语句中的as pyip代码可以避免我们键入pyinputplus。相反,我们可以使用更短的pyip名称。如果你看一下这个例子,你会发现不像input(),这些函数返回一个intfloat值:423.14,而不是字符串'42''3.14'

正如您可以将一个字符串传递给input()来提供提示一样,您也可以将一个字符串传递给 PyInputPlus 函数的prompt关键字参数来显示提示:

>>> response = input('Enter a number: ')
Enter a number: 42
>>> response
'42'
>>> import pyinputplus as pyip
>>> response = pyip.inputInt(prompt='Enter a number: ')
Enter a number: cat
'cat' is not an integer.
Enter a number: 42
>>> response
42

使用 Python 的help()函数来了解关于这些函数的更多信息。例如,help(pyip.inputChoice)显示inputChoice()函数的帮助信息。完整的文档可以在pyinputplus.readthedocs.io找到。

与 Python 的内置input()不同,PyInputPlus 函数有几个额外的输入验证特性,如下一节所示。

关键字参数

接受intfloat数的inputNum()inputInt()inputFloat()函数也有用于指定有效值范围的minmaxgreaterThanlessThan关键字参数。例如,在交互式 Shell 中输入以下内容:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum('Enter num: ', min=4)
Enter num:3
Input must be at minimum 4.
Enter num:4
>>> response
4
>>> response = pyip.inputNum('Enter num: ', greaterThan=4)
Enter num: 4
Input must be greater than 4.
Enter num: 5
>>> response
5
>>> response = pyip.inputNum('>', min=4, lessThan=6)
Enter num: 6
Input must be less than 6.
Enter num: 3
Input must be at minimum 4.
Enter num: 4
>>> response
4

这些关键字参数是可选的,但是如果提供的话,输入不能小于min参数或大于max参数(尽管输入可以等于它们)。同样,输入必须大于greaterThan并且小于lessThan参数(也就是说,输入不能等于它们)。

blank关键字参数

默认情况下,不允许空白输入,除非blank关键字参数设置为True:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum('Enter num: ')
Enter num:(blank input entered here)
Blank values are not allowed.
Enter num: 42
>>> response
42
>>> response = pyip.inputNum(blank=True)
(blank input entered here)
>>> response
''

如果你想让输入可选,用户不需要输入任何东西,使用blank=True

limittimeoutdefault关键字参数

默认情况下,PyInputPlus 函数将永远(或者只要程序运行)继续要求用户输入有效的数据。如果你想让一个函数在一定次数的尝试或一定时间后停止要求用户输入,你可以使用关键字参数limittimeout。为limit关键字参数传递一个整数,以确定 PyInputPlus 函数在放弃之前尝试接收有效输入的次数,为timeout关键字参数传递一个整数,以确定在 PyInputPlus 函数放弃之前用户必须输入有效输入的秒数。

如果用户未能输入有效的输入,这些关键字参数将导致函数分别引发一个RetryLimitExceptionTimeoutException。例如,在交互式 Shell 中输入以下内容:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum(limit=2)
blah
'blah' is not a number.
Enter num: number
'number' is not a number.
Traceback (most recent call last):
    --snip--
pyinputplus.RetryLimitException
>>> response = pyip.inputNum(timeout=10)
42 (entered after 10 seconds of waiting)
Traceback (most recent call last):
    --snip--
pyinputplus.TimeoutException

当您使用这些关键字参数并传递一个default关键字参数时,该函数将返回默认值,而不是引发异常。在交互式 Shell 中输入以下内容:

>>> response = pyip.inputNum(limit=2, default='N/A')
hello
'hello' is not a number.
world
'world' is not a number.
>>> response
'N/A'

inputNum()函数只是返回字符串'N/A',而不是引发RetryLimitException

allowRegexesblockRegexes关键字参数

您还可以使用正则表达式来指定是否允许输入。allowRegexesblockRegexes关键字参数采用正则表达式字符串列表来确定 PyInputPlus 函数将接受或拒绝哪些有效输入。例如,在交互式 Shell 中输入以下代码,以便inputNum()除了接受常用数字之外,还接受罗马数字:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum(allowRegexes=[r'(I|V|X|L|C|D|M)+', r'zero'])
XLII
>>> response
'XLII'
>>> response = pyip.inputNum(allowRegexes=[r'(i|v|x|l|c|d|m)+', r'zero'])
xlii
>>> response
'xlii'

当然,这个正则表达式只影响inputNum()函数从用户那里接受的字母;该函数仍将接受带有无效排序的罗马数字,如'XVX''MILLI',因为r'(I|V|X|L|C|D|M)+'正则表达式接受这些字符串。

还可以通过使用blockRegexes关键字参数来指定 PyInputPlus 函数不接受的正则表达式字符串列表。在交互式 Shell 中输入以下内容,以便inputNum()不接受偶数:

>>> import pyinputplus as pyip
>>> response = pyip.inputNum(blockRegexes=[r'[02468]

如果同时指定了`allowRegexes`和`blockRegexes`参数,允许列表将覆盖阻止列表。例如,在交互式 Shell 中输入以下内容,它允许使用`'caterpillar'`和`'category'`,但阻止任何包含`'cat'`的内容:

```py
>>> import pyinputplus as pyip
>>> response = pyip.inputStr(allowRegexes=[r'caterpillar', 'category'],
blockRegexes=[r'cat'])
cat
This response is invalid.
catastrophe
This response is invalid.
category
>>> response
'category'

PyInputPlus 模块的函数可以让您不必自己编写繁琐的输入验证代码。但是 PyInputPlus 模块有比这里详细描述的更多的内容。你可以在pyinputplus.readthedocs.io的在线查看它的完整文档。

inputCustom()传递自定义验证函数

通过将函数传递给inputCustom(),您可以编写一个函数来执行您自己的定制验证逻辑。例如,假设您希望用户输入一系列数字,其总和为 10。没有pyinputplus.inputAddsUpToTen()函数,但是您可以创建自己的函数:

  • 接受用户输入内容的单个字符串参数
  • 如果字符串验证失败,将引发异常
  • 如果inputCustom()应该返回不变的字符串,则返回None(或者没有return语句)
  • 如果inputCustom()应该返回一个不同于用户输入的字符串,则返回一个非None
  • 作为第一个参数传递给inputCustom()

比如我们可以创建自己的addsUpToTen()函数,然后传递给inputCustom()。注意,函数调用看起来像inputCustom(addsUpToTen)而不是inputCustom(addsUpToTen()),因为我们将addsUpToTen()函数本身传递给inputCustom(),而不是调用addsUpToTen()并传递它的返回值。

>>> import pyinputplus as pyip
>>> def addsUpToTen(numbers):
...   numbersList = list(numbers)
...   for i, digit in enumerate(numbersList):
...     numbersList[i] = int(digit)
...   if sum(numbersList) != 10:
...     raise Exception('The digits must add up to 10, not %s.' %
(sum(numbersList)))
...   return int(numbers) # Return an int form of numbers.
...
>>> response = pyip.inputCustom(addsUpToTen) # No parentheses after
addsUpToTen here.
123
The digits must add up to 10, not 6.
1235
The digits must add up to 10, not 11.
1234
>>> response # inputStr() returned an int, not a string.
1234
>>> response = pyip.inputCustom(addsUpToTen)
hello
invalid literal for int() with base 10: 'h'
55
>>> response

inputCustom()函数还支持一般的 PyInputPlus 特性,例如blanklimittimeoutdefaultallowRegexesblockRegexes关键字参数。当很难或不可能为有效输入编写正则表达式时,编写自己的自定义验证函数是有用的,如在“加起来等于 10”的例子中。

项目:如何让一个白痴忙上好几个小时

让我们使用 PyInputPlus 来创建一个简单的程序,它执行以下操作:

  1. 问用户是否想知道如何让一个白痴忙上几个小时。
  2. 如果用户回答否,退出。
  3. 如果用户回答是,请转到第一步。

当然,我们不知道用户是否会输入除“是”或“否”之外的内容,所以我们需要执行输入验证。对于用户来说,能够输入yn而不是完整的单词也是很方便的。PyInputPlus 的inputYesNo()函数将为我们处理这个问题,并且无论用户输入的是哪种情况,都会返回一个小写的'yes''no'字符串值。

当您运行这个程序时,它看起来应该如下所示:

Want to know how to keep an idiot busy for hours?
sure
'sure' is not a valid yes/no response.
Want to know how to keep an idiot busy for hours?
yes
Want to know how to keep an idiot busy for hours?
y
Want to know how to keep an idiot busy for hours?
Yes
Want to know how to keep an idiot busy for hours?
YES
Want to know how to keep an idiot busy for hours?
YES!!!!!!
'YES!!!!!!' is not a valid yes/no response.
Want to know how to keep an idiot busy for hours?
TELL ME HOW TO KEEP AN IDIOT BUSY FOR HOURS.
'TELL ME HOW TO KEEP AN IDIOT BUSY FOR HOURS.' is not a valid yes/no response.
Want to know how to keep an idiot busy for hours?
no
Thank you. Have a nice day.

打开一个新的文件编辑器标签,保存为idiot.py。然后输入以下代码:

import pyinputplus as pyip

这将导入 PyInputPlus 模块。由于输入pyinputplus有点麻烦,我们将简称为pyip

while True:
    prompt = 'Want to know how to keep an idiot busy for hours?\\n'
    response = pyip.inputYesNo(prompt)

接下来,while True:创建一个无限循环,该循环将继续运行,直到遇到一个break语句。在这个循环中,我们调用pyip.inputYesNo()来确保这个函数调用不会返回,直到用户输入一个有效的答案。

    if response == 'no':
        break

保证调用pyip.inputYesNo()只返回字符串yes或字符串no。如果它返回了no,那么我们的程序就跳出了无限循环,继续执行最后一行,感谢用户:

print('Thank you. Have a nice day.')

否则,循环再次迭代。

你也可以通过传递关键字参数yesValnoVal在非英语语言中使用inputYesNo()函数。例如,这个程序的西班牙语版本会有这两行:

    prompt = '¿Quieres saber cómo mantener ocupado a un idiota durante horas?\\n'
    response = pyip.inputYesNo(prompt, yesVal='sí', noVal='no')
    if response == 'sí':

现在用户可以输入s(小写或大写)来代替yesy来得到肯定的答案。

项目:乘法竞猜

PyInputPlus 的特性对于创建一个定时乘法测验很有用。通过将allowRegexesblockRegexestimeoutlimit关键字参数设置为pyip.inputStr(),可以将大部分实现留给 PyInputPlus。你需要写的代码越少,你写程序的速度就越快。让我们创建一个程序,向用户提出 10 个乘法问题,其中有效输入是问题的正确答案。打开一个新的文件编辑器选项卡,将文件保存为multiplicationQuiz.py

首先,我们将导入pyinputplusrandomtime。我们将使用变量numberOfQuestionscorrectAnswers来跟踪程序问了多少问题以及用户给出了多少正确答案。一个for循环将重复提出 10 次随机乘法问题:

import pyinputplus as pyip
import random, time
numberOfQuestions = 10
correctAnswers = 0
for questionNumber in range(numberOfQuestions):

for循环中,程序将选择两个一位数相乘。我们将使用这些数字为用户创建一个#Q: N × N =提示,其中Q是问题编号(1 到 10)N是要相乘的两个数字。

    # Pick two random numbers:
    num1 = random.randint(0, 9)
    num2 = random.randint(0, 9)
    prompt = '#%s: %s x %s = ' % (questionNumber, num1, num2)

pyip.inputStr()函数将处理这个测验程序的大部分功能。我们传递给allowRegexes的参数是一个包含正则表达式字符串'^%s$'的列表,其中%s被正确的答案替换。^%字符确保答案以正确的数字开始和结束,尽管 PyInputPlus 会首先删除用户回答开头和结尾的任何空格,以防他们在回答之前或之后无意中按了空格键。我们传递给blocklistRegexes的参数是一个带有('.*', 'Incorrect!')的列表。元组中的第一个字符串是匹配所有可能字符串的正则表达式。因此,如果用户的回答与正确答案不匹配,程序将拒绝他们提供的任何其他答案。在这种情况下,将显示'Incorrect!'字符串,并提示用户再次回答。此外,通过timeout8limit3将确保用户只有 8 秒和 3 次尝试来提供正确答案:

    try:
        # Right answers are handled by allowRegexes.
        # Wrong answers are handled by blockRegexes, with a custom message.
        pyip.inputStr(prompt, allowRegexes=['^%s$' % (num1 * num2)],
                              blockRegexes=[('.*', 'Incorrect!')],
                              timeout=8, limit=3)

如果用户在 8 秒超时后回答,即使他们回答正确,pyip.inputStr()也会引发TimeoutException异常。如果用户错误回答超过 3 次,就会引发一个RetryLimitException异常。这两种异常类型都在 PyInputPlus 模块中,所以pyip.需要预先考虑它们:

    except pyip.TimeoutException:
        print('Out of time!')
    except pyip.RetryLimitException:
        print('Out of tries!')

记住,就像else块可以跟随一个ifelif块一样,它们可以选择跟随最后一个except块。如果在try块中没有出现异常,下面的else块中的代码将会运行。在我们的例子中,这意味着如果用户输入了正确的答案,代码就会运行:

    else:
        # This block runs if no exceptions were raised in the try block.
        print('Correct!')
        correctAnswers += 1

不管是三条信息中的哪一条,“超时!”、“超出尝试次数!”,或者“正确!”,显示,让我们在for循环结束时暂停 1 秒钟,让用户有时间阅读。在程序问了 10 个问题并且for循环继续之后,让我们向用户展示他们做出了多少个正确答案:

    time.sleep(1) # Brief pause to let user see the result.
print('Score: %s / %s' % (correctAnswers, numberOfQuestions))

PyInputPlus 非常灵活,您可以在各种各样接受用户键盘输入的程序中使用它,如本章中的程序所示。

总结

很容易忘记编写输入验证代码,但是没有它,您的程序几乎肯定会有 bug。您期望用户输入的值和他们实际输入的值可能完全不同,您的程序需要足够健壮来处理这些异常情况。您可以使用正则表达式来创建自己的输入验证代码,但是对于一般情况,使用现有的模块更容易,比如 PyInputPlus。您可以使用import pyinputplus as pyip导入模块,以便在调用模块函数时输入一个较短的名称。

PyInputPlus 具有用于输入各种输入的函数,包括字符串、数字、日期、是/否、True / False、电子邮件和文件。虽然input()总是返回一个字符串,但是这些函数以适当的数据类型返回值。inputChoice()函数允许您从几个预先选择的选项中选择一个,而inputMenu()还添加了数字或字母以便快速选择。

所有这些函数都有以下标准特性:去掉两边的空白,用timeoutlimit关键字参数设置超时和重试限制,将正则表达式字符串列表传递给allowRegexesblockRegexes以包含或排除特定响应。您将不再需要编写自己繁琐的while循环来检查有效输入并重新提示用户。

如果 PyInputPlus 模块的函数都不符合您的需要,但是您仍然喜欢 PyInputPlus 提供的其他特性,您可以调用inputCustom()并传递您自己的自定义验证函数供 PyInputPlus 使用。pyinputplus.readthedocs.io/en/latest的文档中有 PyInputPlus 函数和附加特性的完整列表。PyInputPlus 在线文档中的内容比本章中描述的要多得多。重新发明轮子是没有用的,学会使用这个模块将使你不必自己编写和调试代码。*

现在您已经掌握了处理和验证文本的专业知识,是时候学习如何读写计算机硬盘上的文件了。

练习题

  1. PyInputPlus 是 Python 标准库自带的吗?

  2. 为什么 PyInputPlus 一般用import pyinputplus as pyip导入?

  3. inputInt()inputFloat()有什么区别?

  4. 如何确保用户使用 PyInputPlus 输入一个介于099之间的整数?

  5. 传递给allowRegexesblockRegexes关键字参数的是什么?

  6. 空白输入三次inputStr(limit=3)做什么?

  7. 空白输入三次inputStr(limit=3, default='hello')做什么?

实践项目

为了练习,编写程序来完成以下任务。

三明治制作器

编写一个程序,询问用户对三明治的偏好。程序应该使用 PyInputPlus 来确保他们输入有效的输入,例如:

  • 使用inputMenu()表示面包类型:小麦、白面包或酸面团。
  • 使用inputMenu()表示蛋白质类型:鸡肉、火鸡、火腿或豆腐。
  • inputYesNo()询问他们是否想要奶酪。
  • 如果是这样,用inputMenu()询问奶酪的种类:切达奶酪、瑞士奶酪或马苏里拉奶酪。
  • inputYesNo()询问他们想要蛋黄酱、芥末、生菜还是西红柿。
  • inputInt()询问他们想要多少三明治。请确保该数字等于或大于 1。

为这些选项中的每一个提供价格,并在用户输入他们的选择后,让您的程序显示总成本。

自己编写乘法小测验

要了解 PyInputPlus 为您做了多少工作,请尝试自己重新创建乘法测验项目,而不要导入它。这个程序会提示用户 10 道乘法题,范围从0 × 09 × 9。您需要实现以下特性:

  • 如果用户输入正确的答案,程序显示“正确!”1 秒钟,然后继续下一个问题。
  • 在程序进入下一个问题之前,用户有三次输入正确答案的机会。
  • 第一次显示问题八秒后,即使用户在八秒限制后输入了正确答案,该问题也会被标记为不正确。

将您的代码与第 196 页的“项目:乘法测验”中使用 PyInputPlus 的代码进行比较。

以上是关于Python 自动化指南(繁琐工作自动化)第二版:十一调试的主要内容,如果未能解决你的问题,请参考以下文章

Python 自动化指南(繁琐工作自动化)第二版:二流程控制

Python 自动化指南(繁琐工作自动化)第二版:五字典和结构化数据

Python 自动化指南(繁琐工作自动化)第二版:十八发送电子邮件和短信

Python 自动化指南(繁琐工作自动化)第二版:七使用正则表达式的模式匹配

Python编程快速上手-让繁琐工作自动化-第二章习题及其答案

Python学习的必备书籍