Python-自动化指南-繁琐工作自动化-第三版-三-

Python 自动化指南(繁琐工作自动化)第三版(三)

原文:automatetheboringstuff.com/

译者:飞龙

协议:CC BY-NC-SA 4.0

5 调试

原文:automatetheboringstuff.com/3e/chapter5.html

图片

现在你已经足够编写基本的程序了,你可能会开始发现程序中不那么简单的 bug。本章介绍了一些工具和技术,用于查找程序中 bug 的根本原因,帮助你更快、更轻松地修复它们。用程序员之间的一句老笑话来说,编写代码占编程的 90%,调试代码占剩下的 90%。

你的电脑只会做你告诉它做的事情;它不会读你的心思,去做你打算让它做的事情。即使是专业的程序员也会经常遇到 bug,所以如果你的程序有问题,不要感到气馁。

幸运的是,有一些工具和技术可以准确地识别你的代码正在做什么以及它在哪里出错。你将使用调试器,这是 Mu 的一个功能,它一次执行一条指令,让你有机会在代码运行时检查变量的值,并跟踪值在程序运行过程中的变化。这个过程比以全速运行程序慢得多,但它允许你在程序运行时看到实际的值,而不是必须从源代码中推断值可能是什么。

你也会让你的程序抛出自定义异常来指示错误,你还将了解日志记录和断言这两个功能,它们可以帮助你早期检测 bug。一般来说,你越早发现 bug,修复起来就越容易。

抛出异常

当 Python 尝试执行无效代码时,会抛出一个异常。在第四章中,你使用tryexcept语句处理 Python 的异常,以便你的程序能够从你预期的异常中恢复。但你也可以在代码中抛出你自己的异常。抛出异常是一种说“停止运行这段代码,并将程序执行移动到except语句”的方式。

我们使用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. 

如果没有tryexcept语句覆盖抛出异常的raise语句,程序将直接崩溃并显示异常的错误信息。

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

def box_print(symbol, width, height):
    if len(symbol) != 1:
        raise Exception('Symbol must be a single character string.') # ❶
    if width <= 2:
        raise Exception('Symbol must be a single character string.') # ❶
    if height <= 2:
        raise Exception('Symbol must be a single character string.') # ❶

 print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

try:
    box_print('*', 4, 4)
    box_print('O', 20, 5)
    box_print('x', 1, 3)
    box_print('ZZ', 3, 3)
except Exception as err: # ❹
    raise Exception('Symbol must be a single character string.') # ❶
try:
    box_print('ZZ', 3, 3)
except Exception as err:
    print('An exception happened: ' + str(err)) 

在这里,我们定义了一个box_print()函数,它接受一个字符、一个宽度和一个高度,并使用该字符来制作一个具有该宽度和高度的盒子形状的小图片。这个盒子形状被打印到屏幕上。

假设我们想要函数只接受一个字符,并且我们期望宽度和高度大于 2。我们添加if语句来在这些要求不满足时引发异常。稍后,当我们用各种参数调用box_print()时,我们的try-except将处理无效参数。

此程序使用except Exception as err形式的except语句 ❹。如果box_print() ❶ ❷ ❸返回一个Exception对象,这个except语句将把它存储在一个名为err的变量中。然后我们可以通过传递给str()来将Exception对象转换为字符串,从而生成一个用户友好的错误信息 ❺。当你运行这个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语句,你可以优雅地处理错误,而不是让整个程序崩溃。

断言

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

  • assert关键字

  • 一个条件(即,一个评估结果为TrueFalse的表达式)

  • 一个逗号

  • 当条件为False时显示的字符串

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

>>> 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()中的代码没有错误并且完成了其工作,那么断言将是真实的。因为ages[0] <= ages[-1]表达式评估为True,所以assert语句什么也不做。

然而,让我们假装我们的代码中有一个错误。比如说,我们不小心调用了reverse()列表方法而不是sort()列表方法。当我们把以下内容输入到交互式外壳中时,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 "<python-input-0>", line 1, in <module>
AssertionError 

与异常不同,你的代码不应该用tryexcept来处理assert语句;如果assert失败,你的程序应该崩溃。通过这种方式“快速失败”,你缩短了从原始错误原因到第一次注意到错误的时间,减少了在找到错误原因之前需要检查的代码量。

断言(Assertions)用于程序员错误,而不是用户错误。断言应该在程序开发期间失败;用户永远不应该在完成的程序中看到断言错误。对于程序在正常操作过程中可能遇到的错误(例如找不到文件或用户输入无效数据),应抛出异常而不是用assert语句检测它。

记录日志

如果你曾经在代码中放置一个print()函数来输出程序运行时某个变量的值,你就已经使用了日志记录来调试你的代码。日志记录是了解程序中发生的事情及其顺序的绝佳方式。Python 的logging模块使得创建你编写的自定义消息记录变得容易。这些日志消息将描述程序执行何时达到日志函数调用,并列出在那个时间点指定的任何变量,提供一条线索,帮助你找出事情开始出错的时间。另一方面,缺失的日志消息表明代码的一部分被跳过且从未执行。

日志记录模块

要启用logging模块在程序运行时在屏幕上显示日志消息,将以下内容复制到程序顶部:

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

logging模块的basicConfig()函数允许你指定你想要看到哪些细节以及如何显示这些细节。

假设你编写了一个函数来计算一个数字的阶乘。在数学中,4 的阶乘是 1 × 2 × 3 × 4,或 24。7 的阶乘是 1 × 2 × 3 × 4 × 5 × 6 × 7,或 5,040。打开一个新的文件编辑标签,并输入以下代码。它有一个错误,但你将生成几个日志消息来帮助找出问题所在。将程序保存为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(' + str(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(' + str(n) + ')')
    return total

print(factorial(5))
logging.debug('End of program') 

我们使用logging.debug()函数来打印日志信息。这个debug()函数调用basicConfig(),它以我们在函数调用中指定的格式打印一行信息,包括我们传递给debug()的消息。print(factorial(5))调用是原始程序的一部分,因此即使禁用日志消息,代码也会显示结果。

这个程序的输出看起来像这样:

2035-05-23 16:20:12,664 - DEBUG - Start of program
2035-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2035-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2035-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2035-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2035-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2035-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2035-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2035-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2035-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):,然后再次运行程序。输出将看起来像这样:

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

factorial(5)的调用正确地返回了120。日志消息显示了循环内部发生的情况,这直接指向了错误。

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

日志文件

你可以将日志消息写入文本文件而不是显示在屏幕上。logging.basicConfig()函数接受一个名为filename的参数,如下所示:

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

此代码将日志消息保存到myProgramLog.txt

虽然日志消息很有帮助,但它们可能会使屏幕变得杂乱,难以阅读程序的输出。将日志消息写入文件将保持屏幕清晰,并在程序运行后允许你阅读这些消息。你可以使用任何文本编辑器打开此文本文件,例如记事本或 TextEdit。

一种不良做法:使用 print()进行调试

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

日志消息是针对程序员而不是用户的。用户不会关心你需要看到以帮助调试的一些字典值的内容;对于用户应该看到的错误消息,如文件未找到无效输入,请输入一个数字,请使用print()调用。你不想剥夺用户使用这些信息解决问题的能力。|

日志级别

日志级别提供了一种按重要性对日志消息进行分类的方法,这样你就可以在测试程序时过滤掉不太重要的消息。共有五个日志级别,从最不重要到最重要,如表 5-1 所示。你的程序可以使用不同的日志功能在每个级别记录消息。

表 5-1:Python 中的日志级别

级别 日志功能 描述
调试 logging.debug() 最低级别,用于小细节。通常,你只会在诊断问题时关注这些消息。
信息 logging.info() 用于记录程序中一般事件的详细信息,或确认程序在各个点的运行情况。
警告 logging.warning() 用于指示可能的问题,这些问题不会阻止程序工作,但将来可能会这样做。
错误 logging.error() 用于记录导致程序无法执行某项操作的错误。
CRITICAL logging.critical() 最高级别,用于指示已导致或即将导致程序完全停止运行的致命错误。

最终,决定你的日志消息属于哪个类别的是你。你可以将这些函数作为字符串传递日志消息。通过在交互式外壳中输入以下内容来尝试自己:

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

日志级别的优点是你可以改变你想要看到的日志消息的优先级。将logging.DEBUG传递给basicConfig()函数的level命名参数将显示所有日志级别的消息(DEBUG 是最低级别)。但经过更多的发展,你可能只对错误感兴趣。在这种情况下,你可以将basicConfig()level参数设置为logging.ERROR。这将只显示 ERROR 和 CRITICAL 消息,并跳过 DEBUG、INFO 和 WARNING 消息。

禁用日志记录

在你调试完程序后,你可能不希望所有这些日志消息使屏幕变得杂乱。logging.disable()函数禁用了这些,这样你就不必手动删除日志调用。只需将日志级别传递给logging.disable()以抑制该级别或更低级别的所有日志消息。要完全禁用日志记录,将logging.disable(logging.CRITICAL)添加到你的程序中。例如,在交互式外壳中输入以下内容:

>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s - 
%(levelname)s -  %(message)s')
>>> logging.critical('Critical error! Critical error!')
2035-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical('Critical error! Critical error!')
>>> logging.error('Error! Error!') 

因为logging.disable()将在其后禁用所有消息,所以你可能想在程序中靠近import logging代码行的位置添加它。这样,你可以轻松找到它来注释或取消注释调用,以根据需要启用或禁用日志消息。 ### Mu 的调试器

调试器是 Mu 编辑器、IDLE 和其他编辑软件的一个功能,允许你逐行执行你的程序。调试器将运行一行代码,然后等待你告诉它继续。通过以这种方式在“调试器”下运行你的程序,你可以在程序的任何给定点花费任意多的时间来检查变量的值。这是一个追踪错误的宝贵工具。

要在 Mu 的调试器下运行程序,请点击按钮行顶部的调试按钮,位于运行按钮旁边。调试检查器面板应打开在窗口的右侧。此面板列出了程序中当前变量的值。在图 5-1 中,调试器在程序即将运行第一行代码之前暂停了程序的执行。你可以在文件编辑器中看到这条被高亮的行。

Mu 界面的截图

图 5-1:Mu 在调试器下运行程序描述

调试模式还在编辑器的顶部添加了以下新按钮:继续、单步执行、进入和退出。通常的停止按钮也是可用的。

点击“继续”按钮将导致程序正常执行,直到终止或达到断点。(我将在本章后面描述断点。)如果你完成调试并希望程序继续正常执行,请点击“继续”按钮。

点击“进入”按钮将导致调试器执行下一行代码然后再次暂停。如果下一行代码是一个函数调用,调试器将进入该函数,跳转到函数的第一行代码。

点击“单步执行”按钮将执行下一行代码,类似于“进入”按钮。然而,如果下一行代码是一个函数调用,则“单步执行”按钮将跳过,或快速通过,该函数中的代码。函数的代码将以全速执行,并在函数调用返回时调试器将暂停。例如,如果下一行代码调用了一个spam()函数,但你并不关心这个函数内部的代码,你可以点击“单步执行”来以正常速度执行函数中的代码,然后在函数返回时暂停。因此,使用“单步执行”按钮比使用“进入”按钮更常见。

点击“跳出”按钮将导致调试器以全速执行代码行,直到它从当前函数返回。如果你使用“进入”按钮进入了一个函数调用,现在只想继续执行指令直到离开它,请点击“跳出”按钮以跳出当前函数调用。

如果你想要完全停止调试并且不想继续执行程序的其余部分,请点击“停止”按钮。停止按钮将立即终止程序。

调试加法程序

要练习使用 Mu 调试器,打开一个新的文件编辑标签并输入以下代码:

print('Enter the first number to add:')
first = input()
print('Enter the second number to add:')
second = input()
print('Enter the third number to add:')
third = input()
print('The sum is ' + first + second + third) 

将其保存为 buggyAddingProgram.py 并首先在没有启用调试器的情况下运行它。程序将输出类似以下内容:

Enter the first number to add:
5
Enter the second number to add:
3
Enter the third number to add:
42
The sum is 5342 

程序没有崩溃,但总和显然是错误的。

再次运行程序,这次在调试器下运行。点击“调试”按钮,程序应该暂停在第 1 行,这是它即将执行的代码。

点击“单步执行”按钮一次以执行第一个print()调用。在这里你应该使用“单步执行”而不是“进入”,因为你不想进入print()函数的代码(尽管 Mu 应该阻止调试器进入 Python 的内置函数)。调试器将跳转到第 2 行,并在文件编辑器中突出显示第 2 行,如图 5-2 所示。这显示了程序当前执行的当前位置。

Mu 界面的截图

图 5-2:点击“单步执行”后的 Mu 编辑器窗口描述

再次点击Step Over以执行input()函数调用。在 Mu 等待你在输出面板中为input()调用输入内容时,高亮会消失。输入 5 并按回车键。高亮会重新出现。

持续点击Step Over,并将 3 和 42 作为接下来的两个数字输入。当调试器到达第 7 行,程序中的最后一个print()调用时,Mu 编辑器窗口应该看起来像图 5-3。

Mu 界面的截图

图 5-3:位于 Mu 编辑器窗口右侧的调试检查器面板显示,变量被设置为字符串而不是整数,导致错误。描述

在调试检查器面板中,你应该看到firstsecondthird变量被设置为字符串值'5''3''42',而不是整数值 5、3 和 42。当执行最后一行时,Python 将这些字符串连接起来,而不是将数字相加,从而导致错误。

使用调试器逐步执行程序很有帮助,但也可能很慢。通常,你希望程序正常运行直到到达特定的代码行。你可以通过设置断点来配置调试器实现这一点。

设置断点

在特定代码行上设置断点会强制调试器在程序执行到达该行时暂停。打开一个新的文件编辑标签,并输入以下程序,该程序模拟掷硬币 1000 次。将其保存为coinFlip.py

import random
heads = 0
for i in range(1, 1001):
    raise Exception('Symbol must be a single character string.') # ❶
        heads = heads + 1
    if i == 500:
        raise Exception('Symbol must be a single character string.') # ❶
print('Heads came up ' + str(heads) + ' times.') 

random.randint(0, 1)调用❶有一半的时间会返回0,另一半时间返回1,模拟了 50/50 的掷硬币,其中1代表正面。当你没有调试器运行这个程序时,它会快速输出类似以下的内容:

Halfway done!
Heads came up 490 times. 

如果你在这个程序下运行调试器,你将不得不点击数千次Step Over按钮,程序才会终止。如果你对程序执行到一半时的heads值感兴趣,即当 1000 次掷硬币中的 500 次完成时,你可以在print('Halfway done!')上设置一个断点。要设置断点,请点击文件编辑器中的行号。这应该会在行号旁边出现一个红色圆点,标记断点;参见图 5-4。

Mu 界面中一个 Python 程序放大截图,第 7 行旁边有一个红色圆点

图 5-4:设置断点会在行号旁边出现一个红色圆点(圈出)。

注意,你不会想在if语句行上设置断点,因为if语句会在循环的每次迭代中执行。当你将断点设置在if语句中的代码上时,调试器只有在执行进入if子句时才会中断。

现在当你以调试模式运行程序时,它应该像往常一样在第一行处暂停,但如果你点击继续,程序应该以全速运行,直到它到达设置断点的行。然后你可以点击继续、Step Over、Step In 或 Step Out 来继续正常操作。

如果你想要移除一个断点,请再次点击行号。红色圆点将消失,调试器在未来的程序中不会在该行中断。

摘要

断言、异常、日志记录和调试器都是查找和防止程序中错误的有价值工具。使用 Python 的assert语句进行断言是实现“健全性检查”的好方法,当必要条件不成立时,它会给你一个早期警告。断言仅用于程序不应该尝试恢复的错误,并且它们应该快速失败。否则,你应该抛出异常。

异常可以通过tryexcept语句来捕获和处理。logging模块是在程序运行时查看代码的好方法,它比print()函数更方便使用,因为它有不同的日志级别,并且能够将日志记录到文本文件中。

调试器允许你逐行遍历你的程序。或者,你可以以正常速度运行程序,并且当调试器遇到设置断点的行时,它会暂停执行。使用调试器,你可以在程序生命周期的任何时刻查看任何变量的值。

不小心将错误引入代码是生活中不可避免的事实,无论你有多少年编码经验。这些调试工具和技术将帮助你编写出能正常工作的程序。

练习问题

  1. 编写一个assert语句,如果变量spam是小于10的整数,则触发AssertionError

  2. 编写一个assert语句,如果变量eggsbacon包含相同的字符串(即使它们的字母大小写不同),则触发AssertionError。(即'hello''hello'被认为是相同的,同样'goodbye''GOODbye'也是相同的。)

  3. 编写一个总是触发AssertionErrorassert语句。

  4. 你的程序必须包含哪两行代码才能调用logging.debug()

  5. 你的程序必须包含哪两行代码才能使logging.debug()将日志消息发送到名为programLog.txt的文件?

  6. 五种日志级别是什么?

  7. 你可以在程序中添加哪一行代码来禁用所有日志消息?

  8. 为什么使用日志消息比使用print()显示相同的消息更好?

  9. 调试器中的“Step Over”、“Step In”和“Step Out”按钮有什么区别?

  10. 点击继续后,调试器何时停止?

  11. 断点是什么?

  12. 在 Mu 中如何在代码行上设置断点?

练习程序:调试抛硬币

以下程序旨在是一个简单的抛硬币猜测游戏。玩家有两个猜测的机会。(这是一个简单的游戏。)然而,程序中存在多个错误。运行几次程序以找到导致程序无法正确工作的错误。

import random
guess = ''
while guess not in ('heads', 'tails'):
    print('Guess the coin toss! Enter heads or tails:')
    guess = input()
toss = random.randint(0, 1)  # 0 is tails, 1 is heads
if toss == guess:
    print('You got it!')
else:
    print('Nope! Guess again!')
    guess = input()
    if toss == guess:
        print('You got it!')
    else:
        print('Nope. You are really bad at this game.') 

引发异常

当 Python 尝试执行无效代码时,它会引发异常。在第四章中,你使用tryexcept语句处理了 Python 的异常,以便你的程序可以从你预期的异常中恢复。但你也可以在你的代码中引发自己的异常。引发异常是一种说“停止运行此代码,并将程序执行移动到except语句”的方式。

我们使用raise语句引发异常,该语句由以下内容组成:

  • raise关键字

  • Exception()函数的调用

  • 传递给Exception()函数的有用错误消息字符串

例如,将以下内容输入到交互式外壳中:

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

如果没有tryexcept语句覆盖引发异常的raise语句,程序将简单地崩溃并显示异常的错误消息。

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

def box_print(symbol, width, height):
    if len(symbol) != 1:
        raise Exception('Symbol must be a single character string.') # ❶
    if width <= 2:
        raise Exception('Symbol must be a single character string.') # ❶
    if height <= 2:
        raise Exception('Symbol must be a single character string.') # ❶

 print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

try:
    box_print('*', 4, 4)
    box_print('O', 20, 5)
    box_print('x', 1, 3)
    box_print('ZZ', 3, 3)
except Exception as err: # ❹
    raise Exception('Symbol must be a single character string.') # ❶
try:
    box_print('ZZ', 3, 3)
except Exception as err:
    print('An exception happened: ' + str(err)) 

在这里,我们定义了一个box_print()函数,它接受一个字符、一个宽度和一个高度,并使用该字符制作一个具有该宽度和高度的盒子形状的小图片。这个盒子形状被打印到屏幕上。

假设我们希望函数只接受一个字符,并且我们期望宽度和高度都大于 2。我们添加if语句来引发异常,如果这些要求没有得到满足。稍后,当我们用各种参数调用box_print()时,我们的try-except将处理无效参数。

此程序使用except Exception as err形式的except语句❹。如果从box_print()❶❷❸返回一个Exception对象,则此except语句将把它存储在一个名为err的变量中。然后我们可以通过传递给str()来将Exception对象转换为字符串,以生成一个用户友好的错误消息❺。当你运行此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语句,你可以优雅地处理错误,而不是让整个程序崩溃。

断言

一个断言是一个理智检查,以确保你的代码没有做明显错误的事情。我们使用assert语句执行这些理智检查。如果理智检查失败,代码将引发AssertionError异常。一个assert语句由以下内容组成:

  • assert关键字

  • 一个条件(即,一个评估结果为TrueFalse的表达式)

  • 一个逗号

  • 当条件为False时显示的字符串

用简单的英语来说,一个assert语句表示,“我断言这个条件是正确的,如果不是,那么某个地方有错误,所以立即停止程序。”例如,在交互式 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()中的代码没有错误并且完成了它的任务,那么断言将是正确的。因为ages[0] <= ages[-1]表达式评估为True,所以assert语句不会做任何事情。

然而,让我们假设我们的代码中有一个错误。比如说我们不小心调用了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 "<python-input-0>", line 1, in <module>
AssertionError 

与异常不同,你的代码不应该用tryexcept来处理assert语句;如果assert失败,你的程序应该崩溃。通过“快速失败”的方式,你缩短了从错误原始原因到第一次注意到错误的时间,减少了在找到错误原因之前需要检查的代码量。

断言用于程序员错误,而不是用户错误。断言应该在程序开发期间失败;用户永远不应该在完成的程序中看到断言错误。对于程序在正常操作过程中可能遇到的错误(例如找不到文件或用户输入无效数据),应该抛出一个异常,而不是用assert语句来检测它。

日志记录

如果你曾经在代码中放置一个print()函数来在程序运行时输出某个变量的值,那么你已经使用了日志记录来调试你的代码。日志记录是一种了解程序中发生的事情及其顺序的好方法。Python 的logging模块使得创建你编写的自定义消息记录变得容易。这些日志消息将描述程序执行何时达到日志函数调用,并将列出在那个时间点指定的任何变量,提供一条可以帮你找出事情开始出错的时间线索。另一方面,缺失的日志消息表明代码的一部分被跳过并且从未执行。

日志模块

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

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

logging模块的basicConfig()函数允许你指定你想要看到哪些细节以及如何显示这些细节。

假设你编写了一个函数来计算一个数字的阶乘。在数学中,4 的阶乘是 1 × 2 × 3 × 4,或者 24。7 的阶乘是 1 × 2 × 3 × 4 × 5 × 6 × 7,或者 5,040。打开一个新的文件编辑标签页,并输入以下代码。它里面有一个错误,但你将生成几个日志消息来帮助你找出问题所在。将程序保存为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(' + str(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(' + str(n) + ')')
    return total

print(factorial(5))
logging.debug('End of program') 

我们使用 logging.debug() 函数来打印日志信息。这个 debug() 函数调用 basicConfig(),它以我们在函数调用中指定的格式打印一行信息,包括我们传递给 debug() 的消息。print(factorial(5)) 调用是原始程序的一部分,所以即使禁用了日志消息,代码也会显示结果。

此程序的输出如下所示:

2035-05-23 16:20:12,664 - DEBUG - Start of program
2035-05-23 16:20:12,664 - DEBUG - Start of factorial(5)
2035-05-23 16:20:12,665 - DEBUG - i is 0, total is 0
2035-05-23 16:20:12,668 - DEBUG - i is 1, total is 0
2035-05-23 16:20:12,670 - DEBUG - i is 2, total is 0
2035-05-23 16:20:12,673 - DEBUG - i is 3, total is 0
2035-05-23 16:20:12,675 - DEBUG - i is 4, total is 0
2035-05-23 16:20:12,678 - DEBUG - i is 5, total is 0
2035-05-23 16:20:12,680 - DEBUG - End of factorial(5)
0
2035-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):,然后再次运行程序。输出将如下所示:

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

factorial(5) 调用正确地返回 120。日志消息显示了循环内部发生的情况,这直接指向了错误。

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

日志文件

你可以将日志消息写入文本文件,而不是在屏幕上显示。logging.basicConfig() 函数接受一个名为 filename 的参数,如下所示:

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

此代码将日志消息保存到 myProgramLog.txt

虽然日志消息很有帮助,但它们可能会使屏幕变得杂乱,难以阅读程序的输出。将日志消息写入文件将保持屏幕清晰,并在程序运行后允许你阅读这些消息。你可以使用任何文本编辑器打开这个文本文件,例如记事本或 TextEdit。

一个糟糕的做法:使用 print() 调试

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

日志消息是针对程序员,而不是用户的。用户不会关心你需要查看以帮助调试的一些字典值的内容;对于用户应该看到的错误消息,如“文件未找到”或“无效输入,请输入一个数字”,请使用print()调用。你不想剥夺用户使用这些信息解决问题的能力。

日志级别

日志级别提供了一种按重要性对日志消息进行分类的方法,这样你就可以在测试程序时过滤掉不太重要的消息。Python 中有五个日志级别,从最不重要到最重要,如表 5-1 所示。你的程序可以使用不同的日志函数在每个级别记录消息。

表 5-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 minor code and debugging details.')
2035-05-18 19:04:26,901 - DEBUG - Some debugging details.
>>> logging.info('An event happened.')
2035-05-18 19:04:35,569 - INFO - The logging module is working.
>>> logging.warning('Something could go wrong.')
2035-05-18 19:04:56,843 - WARNING - An error message is about to be logged.
>>> logging.error('An error has occurred.')
2035-05-18 19:05:07,737 - ERROR - An error has occurred.
>>> logging.critical('The program is unable to recover!')
2035-05-18 19:05:45,794 - CRITICAL - The program is unable to recover! 

日志级别的优点是你可以改变你想要看到的日志消息的优先级。将logging.DEBUG传递给basicConfig()函数的level命名参数将显示所有日志级别的消息(DEBUG 是最低级别)。但是,在进一步开发你的程序之后,你可能只对错误感兴趣。在这种情况下,你可以将basicConfig()level参数设置为logging.ERROR。这将只显示 ERROR 和 CRITICAL 消息,并跳过 DEBUG、INFO 和 WARNING 消息。

禁用日志

在调试完你的程序后,你可能不希望所有这些日志消息使屏幕变得杂乱。logging.disable()函数可以禁用这些消息,这样你就不必手动删除日志调用。只需将日志级别传递给logging.disable(),就可以抑制该级别或更低级别的所有日志消息。要完全禁用日志,请将logging.disable(logging.CRITICAL)添加到你的程序中。例如,在交互式 shell 中输入以下内容:

>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s - 
%(levelname)s -  %(message)s')
>>> logging.critical('Critical error! Critical error!')
2035-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical('Critical error! Critical error!')
>>> logging.error('Error! Error!') 

由于 logging.disable() 将在之后禁用所有消息,你可能想在程序中 import logging 代码行附近添加它。这样,你可以轻松找到它来注释或取消注释该调用,以便根据需要启用或禁用日志消息。

日志模块

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

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

logging 模块的 basicConfig() 函数允许你指定你想要看到哪些详细信息以及如何显示这些详细信息。

假设你编写了一个函数来计算一个数的阶乘。在数学中,4 的阶乘是 1 × 2 × 3 × 4,即 24。7 的阶乘是 1 × 2 × 3 × 4 × 5 × 6 × 7,即 5,040。打开一个新的文件编辑标签,并输入以下代码。它有一个错误,但你将生成几个日志消息来帮助找出问题所在。将程序保存为 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(' + str(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(' + str(n) + ')')
    return total

print(factorial(5))
logging.debug('End of program') 

我们使用 logging.debug() 函数来打印日志信息。这个 debug() 函数调用 basicConfig(),它以我们在函数调用中指定的格式打印一行信息,包括我们传递给 debug() 的消息。print(factorial(5)) 调用是原始程序的一部分,因此即使禁用日志消息,代码也会显示结果。

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

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

factorial() 函数返回 5 的阶乘为 0,这是不正确的。for 循环应该将 total 中的值乘以从 15 的数字,但 logging.debug() 显示的日志消息表明 i 变量从 0 开始而不是 1。由于零乘以任何数都是零,其余迭代的 total 值都是错误的。

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

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

factorial(5) 调用正确地返回 120。日志消息显示了循环内部发生的情况,这直接指向了错误。

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

日志文件

你可以将日志消息写入文本文件而不是在屏幕上显示。logging.basicConfig() 函数接受一个名为 filename 的参数,如下所示:

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

此代码将日志消息保存到 myProgramLog.txt

虽然日志消息很有帮助,但它们可能会使屏幕变得杂乱,难以阅读程序的输出。将日志消息写入文件将保持屏幕清晰,并在程序运行后允许你阅读这些消息。你可以在任何文本编辑器中打开此文本文件,例如记事本或 TextEdit。

一个不好的做法:使用 print() 调试

输入import logginglogging.basicConfig(level=logging.DEBUG, format= '%(asctime)s - %(levelname)s - %(message)s')有些繁琐。你可能想使用print()调用,但不要屈服于这种诱惑!一旦你完成调试,你将花费大量时间从代码中的每个日志消息中移除print()调用。你甚至可能会意外地移除一些用于非日志消息的print()调用。日志消息的好处是你可以自由地在程序中添加尽可能多的日志,并且可以通过添加单个logging.disable(logging.CRITICAL)调用随时禁用它们。与print()不同,logging模块使得在显示和隐藏日志消息之间切换变得容易。

日志消息是针对程序员而不是用户的。用户不会关心你为了帮助调试而需要查看的某些字典值的内容;对于用户应该看到的错误消息,如文件未找到无效输入,请输入一个数字,请使用print()调用。你不想剥夺用户使用这些信息解决问题的能力。

日志级别

日志级别提供了一种按重要性对日志消息进行分类的方法,这样你就可以在测试程序时过滤掉不太重要的消息。共有五个日志级别,从最不重要到最重要,如表 5-1 所示。你的程序可以使用不同的日志函数在每个级别记录消息。

表 5-1:Python 中的日志级别

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

最终,决定你的日志消息属于哪个类别的是你。你可以将这些函数作为字符串传递给日志消息。通过在交互式外壳中输入以下内容来亲自尝试:

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

日志级别的优点是你可以更改你想要看到的日志消息的优先级。将logging.DEBUG传递给basicConfig()函数的level命名参数将显示所有日志级别的消息(DEBUG 是最低级别)。但在进一步开发你的程序后,你可能只对错误感兴趣。在这种情况下,你可以将basicConfig()level参数设置为logging.ERROR。这将只显示 ERROR 和 CRITICAL 消息,并跳过 DEBUG、INFO 和 WARNING 消息。

禁用日志记录

在你调试完程序后,你可能不想所有这些日志消息都杂乱无章地显示在屏幕上。logging.disable()函数可以禁用这些消息,这样你就不必手动删除日志调用。只需将日志级别传递给logging.disable(),就可以抑制该级别或更低级别的所有日志消息。要完全禁用日志记录,将logging.disable(logging.CRITICAL)添加到你的程序中。例如,在交互式 shell 中输入以下内容:

>>> import logging
>>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s - 
%(levelname)s -  %(message)s')
>>> logging.critical('Critical error! Critical error!')
2035-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error!
>>> logging.disable(logging.CRITICAL)
>>> logging.critical('Critical error! Critical error!')
>>> logging.error('Error! Error!') 

因为logging.disable()会在其后禁用所有消息,你可能想在程序中的import logging代码行附近添加它。这样,你可以轻松找到并注释掉或取消注释该调用,以便根据需要启用或禁用日志消息。

Mu 的调试器

调试器是 Mu 编辑器、IDLE 和其他编辑软件的一个功能,允许你逐行执行程序。调试器将运行一行代码,然后等待你告诉它继续。通过以这种方式在“调试器下”运行程序,你可以花尽可能多的时间检查程序生命周期中任何给定点的变量值。这是一个追踪错误的宝贵工具。

要在 Mu 的调试器下运行程序,请点击按钮行顶部的调试按钮,位于运行按钮旁边。调试检查器面板应打开在窗口的右侧。此面板列出了程序中变量的当前值。在图 5-1 中,调试器在程序即将运行第一行代码之前暂停了执行。你可以看到文件编辑器中突出显示的这一行。

Mu 界面的截图

图 5-1:Mu 在调试器下运行程序描述

调试模式还会在编辑器的顶部添加以下新按钮:继续、单步跳过、进入和退出。通常的停止按钮也可用。

点击继续按钮将使程序正常执行,直到终止或达到断点。(我将在本章后面描述断点。)如果你完成调试并希望程序继续正常执行,请点击继续按钮。

点击进入按钮将导致调试器执行下一行代码并再次暂停。如果下一行代码是一个函数调用,调试器将进入该函数,跳转到函数的第一行代码。

点击单步执行按钮将执行下一行代码,类似于进入按钮。然而,如果下一行代码是一个函数调用,单步执行按钮将跳过,或快速通过,该函数中的代码。函数的代码将以全速执行,调试器将在函数调用返回时立即暂停。例如,如果下一行代码调用了一个spam()函数,但你并不关心这个函数内部的代码,你可以点击单步执行来以正常速度执行函数中的代码,然后在函数返回时暂停。因此,使用单步执行按钮比使用进入按钮更常见。

点击跳出按钮将导致调试器以全速执行代码行,直到从当前函数返回。如果你已经使用进入按钮进入了一个函数调用,现在只想继续执行指令直到离开它,请点击跳出按钮以跳出当前函数调用。

如果你想要完全停止调试并且不想继续执行程序的其余部分,请点击停止按钮。停止按钮将立即终止程序。

调试加法程序

为了练习使用 Mu 调试器,打开一个新的文件编辑器标签并输入以下代码:

print('Enter the first number to add:')
first = input()
print('Enter the second number to add:')
second = input()
print('Enter the third number to add:')
third = input()
print('The sum is ' + first + second + third) 

将其保存为buggyAddingProgram.py,并首先在没有启用调试器的情况下运行它。程序将输出类似以下内容:

Enter the first number to add:
5
Enter the second number to add:
3
Enter the third number to add:
42
The sum is 5342 

程序没有崩溃,但总和显然是错误的。

再次运行程序,这次在调试器下进行。点击调试按钮,程序应该暂停在第 1 行,这是它即将执行的代码。

点击单步执行按钮一次来执行第一个print()调用。在这里你应该使用单步执行而不是进入,因为你不想进入print()函数的代码(尽管 Mu 应该阻止调试器进入 Python 的内置函数)。调试器将移动到第 2 行,并在文件编辑器中高亮显示第 2 行,如图 5-2 所示。这显示了程序当前执行的当前位置。

Mu 界面的截图

图 5-2:点击单步执行后的 Mu 编辑器窗口描述

再次点击单步执行来执行input()函数调用。当 Mu 等待你在输出面板中为input()调用输入一些内容时,高亮会消失。输入 5 并按回车键。高亮会重新出现。

继续点击单步执行,输入下一个两个数字 3 和 42。当调试器到达第 7 行时,程序中的最后一个print()调用,Mu 编辑器窗口应该看起来像图 5-3。

Mu 界面的截图

图 5-3:位于 Mu 编辑器窗口右侧的调试检查器面板显示,变量被设置为字符串而不是整数,导致错误。描述

在调试检查器面板中,你应该看到firstsecondthird变量被设置为字符串值'5''3''42',而不是整数值5342。当执行最后一行时,Python 将这些字符串连接起来而不是将数字相加,导致错误。

使用调试器逐步执行程序很有帮助,但也可能很慢。通常,你希望程序正常运行直到达到特定的代码行。你可以通过设置断点来配置调试器实现这一点。

设置断点

在特定代码行上设置断点会强制调试器在程序执行到达该行时暂停。打开一个新的文件编辑标签,并输入以下程序,该程序模拟掷硬币 1000 次。将其保存为coinFlip.py

import random
heads = 0
for i in range(1, 1001):
    raise Exception('Symbol must be a single character string.') # ❶
        heads = heads + 1
    if i == 500:
        raise Exception('Symbol must be a single character string.') # ❶
print('Heads came up ' + str(heads) + ' times.') 

random.randint(0, 1)调用 ❶ 将在半数时间内返回0,在另一半时间内返回1,模拟 50/50 的掷硬币,其中1代表正面。当你没有调试器运行此程序时,它会快速输出类似以下的内容:

Halfway done!
Heads came up 490 times. 

如果你在这个程序下运行调试器,你将不得不点击单步执行按钮数千次,程序才会终止。如果你对程序执行到一半时的heads值感兴趣,即当 1000 次掷硬币中的 500 次完成时,你可以在print('Halfway done!')行设置一个断点 ❷。要设置断点,点击文件编辑器中的行号。这应该会在行号旁边出现一个红色圆点,标记断点;见图 5-4。

Mu 界面中放大后的 Python 程序截图,第 7 行旁边有一个红色圆点

图 5-4:设置断点会在行号旁边出现一个红色圆点(圆圈所示)。

注意,你不会想在if语句行上设置断点,因为if语句在循环的每次迭代中都会执行。当你将断点设置在if语句中的代码上时,调试器只有在执行进入if子句时才会中断。

现在当你使用调试器运行程序时,它应该像往常一样在第一行暂停,但如果点击继续,程序应该以全速运行直到达到设置断点的代码行。然后你可以点击继续、单步执行、进入或退出以继续正常操作。

如果你想要删除断点,再次点击行号。红色圆点将消失,并且调试器在将来不会在该行中断。

调试加法程序

为了练习使用 Mu 调试器,打开一个新的文件编辑标签,并输入以下代码:

print('Enter the first number to add:')
first = input()
print('Enter the second number to add:')
second = input()
print('Enter the third number to add:')
third = input()
print('The sum is ' + first + second + third) 

将其保存为buggyAddingProgram.py,并首先在没有启用调试器的情况下运行它。程序将输出类似以下内容:

Enter the first number to add:
5
Enter the second number to add:
3
Enter the third number to add:
42
The sum is 5342 

程序并没有崩溃,但总和显然是错误的。

再次运行程序,这次在调试器下运行。点击Debug按钮,程序应该暂停在第 1 行,这是它即将执行的代码。

点击Step Over按钮一次以执行第一个print()调用。在这里你应该使用 Step Over 而不是 Step In,因为你不想进入print()函数的代码(尽管 Mu 应该阻止调试器进入 Python 的内置函数)。调试器将移动到第 2 行,并在文件编辑器中高亮显示第 2 行,如图 5-2 所示。这显示了程序当前执行的当前位置。

Mu 界面的截图

图 5-2:点击 Step Over 后的 Mu 编辑器窗口。描述

再次点击Step Over以执行input()函数调用。当 Mu 等待你在输出面板中为input()调用输入一些内容时,高亮会消失。输入 5 并按回车键。高亮会重新出现。

持续点击Step Over,并将 3 和 42 作为接下来的两个数字输入。当调试器到达第 7 行时,程序中的最后一个print()调用,Mu 编辑器窗口应该看起来像图 5-3。

Mu 界面的截图

图 5-3:位于 Mu 编辑器窗口右侧的调试检查器面板显示,变量被设置为字符串而不是整数,这导致了错误。描述

在调试检查器面板中,你应该看到firstsecondthird变量被设置为字符串值'5''3''42',而不是整数值 5、3 和 42。当执行最后一行时,Python 将这些字符串连接起来,而不是将数字相加,这导致了错误。

使用调试器逐步执行程序是有帮助的,但也可能很慢。通常,你希望程序正常运行直到它到达特定的代码行。你可以通过断点来配置调试器执行此操作。

设置断点

在特定代码行上设置断点会强制调试器在程序执行到达该行时暂停。打开一个新的文件编辑器标签,并输入以下程序,该程序模拟掷硬币 1000 次。将其保存为coinFlip.py

import random
heads = 0
for i in range(1, 1001):
    raise Exception('Symbol must be a single character string.') # ❶
        heads = heads + 1
    if i == 500:
        raise Exception('Symbol must be a single character string.') # ❶
print('Heads came up ' + str(heads) + ' times.') 

random.randint(0, 1)调用❶有一半的时间会返回0,另一半时间会返回1,模拟了 50/50 的硬币抛掷,其中1代表正面。当你没有调试器运行此程序时,它会快速输出类似以下内容:

Halfway done!
Heads came up 490 times. 

如果你在这个程序下运行调试器,你可能需要点击数千次“单步执行”按钮,程序才会终止。如果你对程序执行中途的heads变量的值感兴趣,当 1000 次抛硬币中的 500 次完成时,你可以在print('Halfway done!')上设置断点。要设置断点,请点击文件编辑器中的行号。这应该会在行号旁边出现一个红色圆点,标记断点;参见图 5-4。

Mu 界面中 Python 程序的放大截图,第 7 行旁边有一个红色圆点

图 5-4:设置断点会在行号旁边出现一个红色圆点(圈出)。

注意,你不会想在if语句的行上设置断点,因为if语句会在循环的每次迭代中执行。当你对if语句中的代码设置断点时,调试器只有在执行进入if子句时才会中断。

现在当你以调试器运行程序时,它应该像往常一样在第一行以暂停状态开始,但如果你点击“继续”,程序应该以全速运行,直到它到达设置断点的行。然后你可以点击“继续”、“单步执行”、“进入”或“退出”来继续正常操作。

如果你想要移除断点,请再次点击行号。红色圆点将消失,调试器在将来不会在该行中断。

摘要

断言、异常、日志记录和调试器都是查找和防止程序中错误的有价值工具。使用 Python 的assert语句进行断言是实现“合理性检查”的好方法,当必要的条件不成立时,它会给你一个早期警告。断言仅用于程序不应该尝试恢复的错误,并且它们应该快速失败。否则,你应该引发异常。

异常可以通过tryexcept语句来捕获和处理。logging模块是在程序运行时查看代码的好方法,它比print()函数更方便使用,因为它有不同的日志级别,并且能够将日志记录到文本文件中。

调试器允许你逐行执行程序。或者,你可以以正常速度运行程序,让调试器在到达设置断点的行时暂停执行。使用调试器,你可以在程序生命周期的任何时刻查看任何变量的值。

不小心将错误引入代码是编程生活中不可避免的事实,无论你有多少年的编程经验。这些调试工具和技术将帮助你编写出能够正常工作的程序。

实践问题

  1. 编写一个assert语句,如果变量spam是一个小于10的整数,则会触发AssertionError

2.  编写一个assert语句,如果变量eggsbacon包含彼此相同的字符串,即使它们的字母大小写不同,也会触发AssertionError。(也就是说,'hello''hello'被认为是相同的,同样'goodbye''GOODbye'也是相同的。)

3.  编写一个assert语句,使其始终触发AssertionError

4.  你的程序必须包含哪两行代码才能调用logging.debug()

5.  你的程序必须包含哪两行代码才能使logging.debug()向名为programLog.txt的文件发送日志消息?

6.  有哪些五种日志级别?

7.  你可以添加哪一行代码来禁用程序中的所有日志消息?

8.  为什么使用日志消息比使用print()显示相同消息更好?

9.  在调试器中,Step Over、Step In 和 Step Out 按钮之间的区别是什么?

10.  点击继续后,调试器将在何时停止?

11.  什么是断点?

12.  如何在 Mu 中设置代码行的断点?

实践程序:调试抛硬币游戏

以下程序旨在成为一个简单的抛硬币猜谜游戏。玩家有两个猜测的机会。(这是一个简单的游戏。)然而,程序中存在多个错误。运行程序几次以找到导致程序无法正确工作的错误。

import random
guess = ''
while guess not in ('heads', 'tails'):
    print('Guess the coin toss! Enter heads or tails:')
    guess = input()
toss = random.randint(0, 1)  # 0 is tails, 1 is heads
if toss == guess:
    print('You got it!')
else:
    print('Nope! Guess again!')
    guess = input()
    if toss == guess:
        print('You got it!')
    else:
        print('Nope. You are really bad at this game.') 

6 列表

automatetheboringstuff.com/3e/chapter6.html

在你开始认真编写程序之前,你需要了解的一个主题是列表数据类型及其近亲元组。列表和元组可以包含多个值,这使得编写处理大量数据的程序变得更容易。由于列表本身可以包含其他列表,因此你可以使用它们将数据组织成层次结构。

在本章中,我将讨论列表的基础知识。我还会教你关于方法,这些方法是绑定到特定数据类型值的函数。然后,我将简要介绍序列数据类型(列表、元组和字符串)及其差异。在下一章中,我将介绍字典数据类型。

列表数据类型

列表 是一个包含有序序列中多个值的值。术语 列表值 指的是列表本身(你可以将其存储在变量中或传递给函数,就像任何其他值一样),而不是列表值中的值。列表值看起来像这样:['cat', 'bat', 'rat', 'elephant']。就像字符串值使用引号来标记字符串的开始和结束一样,列表以一个开方括号开始,以一个闭方括号结束,[]

我们称列表内的值为 项目。项目由逗号分隔(即,它们是 逗号分隔的)。例如,将以下内容输入到交互式外壳中:

>>> [1, 2, 3]  # A list of three integers
[1, 2, 3]
>>> ['cat', 'bat', 'rat', 'elephant']  # A list of four strings
['cat', 'bat', 'rat', 'elephant']
>>> ['hello', 3.1415, True, None, 42]  # A list of several values
['hello', 3.1415, True, None, 42]
>>> spam = ['cat', 'bat', 'rat', 'elephant'] # ❶
>>> spam
['cat', 'bat', 'rat', 'elephant'] 

变量 spam ❶ 仅分配了一个值:列表值。但列表值本身包含其他值。

注意,值 [] 是一个空列表,不包含任何值,类似于 '',空字符串。

索引

假设你有一个名为 spam 的变量,其中存储了列表 ['cat', 'bat', 'rat', 'elephant']。Python 代码 spam[0] 将评估为 'cat',代码 spam[1] 将评估为 'bat',依此类推。方括号中跟随列表的整数称为 索引。列表中的第一个值位于索引 0,第二个值位于索引 1,第三个值位于索引 2,依此类推。图 6-1 显示了分配给 spam 的列表值及其评估到的索引表达式。请注意,由于第一个索引是 0,最后一个索引是列表大小减一。因此,包含四个项目的列表其最后一个索引为 3

一个图表,显示列表中的每个项目如何对应一个索引。

图 6-1:变量 spam 中存储的列表值,显示每个索引指向哪个值 描述

为了举例说明如何使用索引,请将以下表达式输入到交互式外壳中。我们首先将一个列表赋值给变量 spam

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0]
'cat'
>>> spam[1]
'bat'
>>> spam[2]
'rat'
>>> spam[3]
'elephant'
>>> ['cat', 'bat', 'rat', 'elephant'][3]
'elephant'
>>> 'Hello, ' + spam[0] # ❶
'Hello, cat' # ❷
>>> 'The ' + spam[1] + ' ate the ' + spam[0] + '.'
'The bat ate the cat.' 

注意,表达式 'Hello, ' + spam[0] ❶ 计算结果为 'Hello, ' + 'cat',因为 spam[0] 计算结果为字符串 'cat'。这个表达式进而计算结果为字符串值 'Hello, cat' ❷。

如果你使用一个超出列表值中值的数量的索引,Python 将给出一个 IndexError 错误信息:

>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[10000]
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam[10000]
IndexError: list index out of range 

列表也可以包含其他列表值。你可以使用多个索引来访问这些列表中的值,如下所示:

>>> spam = [['cat', 'bat'], [10, 20, 30, 40, 50]]
>>> spam[0]
['cat', 'bat']
>>> spam[0][1]
'bat'
>>> spam[1][4]
50 

第一个索引决定了使用哪个列表值,第二个索引表示列表值中的值。例如,spam[0][1] 打印 'bat',即第一个列表中的第二个值。

负索引

当索引从 0 开始并向上时,你也可以使用负整数作为索引。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[-1]  # Last index
'elephant'
>>> spam[-3]  # Third to last index
'bat'
>>> 'The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.'
'The elephant is afraid of the bat.' 

整数值 -1 指的是列表中的最后一个索引,值 -2 指的是列表中的倒数第二个索引,依此类推。

切片

正如索引可以从列表中获取单个值一样,一个 切片 也可以从列表中获取多个值,形式为一个新列表。我们像索引一样在方括号中输入切片,但包括用冒号分隔的两个整数。注意索引和切片之间的区别:

  • spam[2] 是一个包含索引(一个整数)的列表。

  • spam[1:4] 是一个包含切片(两个整数)的列表。

在一个切片中,第一个整数是切片开始的索引。第二个整数是切片结束的索引。从切片创建的列表将延伸到第二个索引的值,但不包括该值。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0:4]
['cat', 'bat', 'rat', 'elephant']
>>> spam[1:3]
['bat', 'rat']
>>> spam[0:-1]
['cat', 'bat', 'rat'] 

作为快捷方式,你可以在切片中省略冒号两侧的一个或两个索引:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[:2]
['cat', 'bat']
>>> spam[1:]
['bat', 'rat', 'elephant']
>>> spam[:]
['cat', 'bat', 'rat', 'elephant'] 

省略第一个索引等同于使用 0,即列表的开始。省略第二个索引等同于使用列表的长度,这将切片到列表的末尾。

len() 函数

len() 函数将返回传递给它的列表值中的值的数量。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'moose']
>>> len(spam)
3 

这种行为与函数计算字符串值中字符数的方式类似。

值更新

通常,变量名位于赋值语句的左侧,例如 spam = 42。然而,你也可以使用列表的索引来更改该索引处的值:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[1] = 'aardvark'
>>> spam
['cat', 'aardvark', 'rat', 'elephant']
>>> spam[2] = spam[1]
>>> spam
['cat', 'aardvark', 'aardvark', 'elephant']
>>> spam[-1] = 12345
>>> spam
['cat', 'aardvark', 'aardvark', 12345] 

在这个例子中,spam[1] = 'aardvark' 的意思是“将列表 spam 中索引 1 的值赋给字符串 'aardvark'。”你也可以使用负索引,如 -1 来更新列表。

连接和复制

你可以使用 +* 运算符连接和复制列表,就像字符串一样:

>>> [1, 2, 3] + ['A', 'B', 'C']
[1, 2, 3, 'A', 'B', 'C']
>>> ['X', 'Y', 'Z'] * 3
['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
>>> spam = [1, 2, 3]
>>> spam = spam + ['A', 'B', 'C']
>>> spam
[1, 2, 3, 'A', 'B', 'C'] 

+ 运算符将两个列表组合起来创建一个新的列表值,而 * 运算符将列表与一个整数值组合起来以复制列表。

del 语句

del 语句将删除列表中的索引处的值。删除值之后列表中的所有值都将向上移动一个索引。例如,将以下内容输入到交互式外壳中:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat'] 

del 语句也可以操作简单变量来删除它,就像是一个“未赋值”语句。如果你在删除变量后尝试使用该变量,你会得到一个 NameError 错误,因为该变量不再存在。然而,在实践中,你几乎不需要删除简单变量,del 语句主要用于从列表中删除值。

与列表一起工作

当你刚开始编写程序时,你可能倾向于创建许多单独的变量来存储一组相似值。例如,如果我想存储我的猫的名字,我可能会想编写如下代码:

cat_name_1 = 'Zophie'
cat_name_2 = 'Pooka'
cat_name_3 = 'Simon'
cat_name_4 = 'Lady Macbeth' 

结果表明,这是一种糟糕的编程方式。一方面,如果猫的数量发生变化(你总是可以拥有更多的猫),你的程序将无法存储比你变量更多的猫。这些程序还包含大量重复或几乎相同的代码。为了实际看到这一点,将以下程序输入文件编辑器并保存为 allMyCats1.py

print('Enter the name of cat 1:')
cat_name_1 = input()
print('Enter the name of cat 2:')
cat_name_2 = input()
print('Enter the name of cat 3:')
cat_name_3 = input()
print('Enter the name of cat 4:')
cat_name_4 = input()
print('The cat names are:')
print(cat_name_1 + ' ' + cat_name_2 + ' ' + cat_name_3 + ' ' + cat_name_4) 

而不是使用多个重复的变量,你可以使用一个包含列表值的单个变量。例如,这是 allMyCats1.py 程序的一个新版本和改进版本。这个新版本使用单个列表,可以存储用户输入的任何数量的猫。在一个新的文件编辑器窗口中,输入以下源代码并将其保存为 allMyCats2.py

cat_names = []
while True:
    print('Enter the name of cat ' + str(len(cat_names) + 1) +
      ' (Or enter nothing to stop.):')
    name = input()
    if name == '':
        break
    cat_names = cat_names + [name]  # List concatenation
print('The cat names are:')
for name in cat_names:
    print('  ' + name) 

当你运行这个程序时,输出将类似于以下内容:

Enter the name of cat 1 (Or enter nothing to stop.):
Zophie
Enter the name of cat 2 (Or enter nothing to stop.):
Pooka
Enter the name of cat 3 (Or enter nothing to stop.):
Simon
Enter the name of cat 4 (Or enter nothing to stop.):
Lady Macbeth
Enter the name of cat 5 (Or enter nothing to stop.):

The cat names are:
  Zophie
  Pooka
  Simon
  Lady Macbeth 

使用列表的好处是,你的数据现在在一个结构中,因此你的程序可以比使用几个重复变量更灵活地处理数据。

for 循环和列表

在第三章,你学习了如何使用 for 循环执行一定次数的代码块。技术上讲,for 循环对列表值中的每个项目重复一次代码块。例如,如果你运行此代码

for i in range(4):
    print(i) 

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

0
1
2
3 

这是因为 range(4) 的返回值是一个序列值,Python 认为它与 [0, 1, 2, 3] 类似。以下程序与上一个程序具有相同的输出:

for i in [0, 1, 2, 3]:
    print(i) 

之前的 for 循环实际上在每次迭代中通过变量 i 设置为 [0, 1, 2, 3] 列表中的连续值来遍历其子句。

Python 的一种常见技术是使用 range(len(some_list))for 循环一起遍历列表的索引。例如,将以下内容输入到交互式外壳中:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for i in range(len(supplies)):
...     print('Index ' + str(i) + ' in supplies is: ' + supplies[i])
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

在之前显示的 for 循环中使用 range(len(supplies)) 很方便,因为循环中的代码可以访问索引(作为变量 i)和该索引处的值(作为 supplies[i])。最好的是,range(len(supplies)) 将遍历 supplies 的所有索引,无论列表包含多少项。

in 和 not in 操作符

你可以使用 innot in 运算符来确定一个值是否在列表中。像其他运算符一样,innot in 出现在表达式中,连接两个值:要查找的值和可能找到该值的列表。这些表达式将评估为布尔值。要了解它们是如何工作的,请在交互式外壳中输入以下内容:

>>> 'howdy' in ['hello', 'hi', 'howdy', 'heyas']
True
>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> 'cat' in spam
False
>>> 'howdy' not in spam
False
>>> 'cat' not in spam
True 

以下程序允许用户输入一个宠物名字,然后检查该名字是否在宠物列表中。打开一个新的文件编辑器窗口,输入以下代码,并将其保存为 myPets.py

my_pets = ['Zophie', 'Pooka', 'Fat-tail']
print('Enter a pet name:')
name = input()
if name not in my_pets:
    print('I do not have a pet named ' + name)
else:
    print(name + ' is my pet.') 

输出可能看起来像这样:

Enter a pet name:
Footfoot
I do not have a pet named Footfoot 

请记住,not in 运算符与布尔 not 运算符不同。

多重赋值技巧

多重赋值技巧(技术上称为元组解包)是一种快捷方式,允许你在一行代码中将多个变量与列表中的值一起赋值。因此,而不是这样做

>>> cat = ['fat', 'gray', 'loud']
>>> size = cat[0]
>>> color = cat[1]
>>> disposition = cat[2] 

你可以输入以下代码行:

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition = cat 

变量的数量和列表的长度必须完全相等,否则 Python 会给你一个 ValueError

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition, name = cat
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    size, color, disposition, name = cat
ValueError: not enough values to unpack (expected 4, got 3) 

这个技巧可以使你的代码比输入三行单独的代码更短且更易读。

列表项枚举

你可以使用 enumerate() 函数而不是使用 range(len(some_list)) 技巧和 for 循环来获取列表中项的整数索引。在循环的每次迭代中,enumerate() 将返回两个值:列表中项的索引和列表中的项本身。例如,这段代码与第 115 页“for 循环和列表”中的代码等效:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for index, item in enumerate(supplies):
...     print('Index ' + str(index) + ' in supplies is: ' + item)
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

当你在循环块中需要项及其索引时,enumerate() 函数很有用。

随机选择和排序

random 模块有几个接受列表作为参数的函数。random.choice() 函数将从列表中返回一个随机选择的项。在交互式外壳中输入以下内容:

>>> import random
>>> pets = ['Dog', 'Cat', 'Moose']
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Dog' 

你可以将 random.choice(some_list) 视为 some_list[random.randint(0, len(some_list) – 1)]` 的简写形式。

random.shuffle() 函数将就地重新排列列表中的项。在交互式外壳中输入以下内容:

>>> import random
>>> people = ['Alice', 'Bob', 'Carol', 'David']
>>> random.shuffle(people)
>>> people
['Carol', 'David', 'Alice', 'Bob']
>>> random.shuffle(people)
>>> people
['Alice', 'David', 'Bob', 'Carol'] 

此函数就地修改列表,而不是返回一个新列表。

扩展赋值运算符

与字符串一起工作的 +* 运算符也适用于列表,因此让我们简要介绍一下扩展赋值运算符。在给变量赋值时,你通常会使用变量本身。例如,将 42 赋值给变量 spam 后,你可以使用以下代码将 spam 中的值增加 1

>>> spam = 42
>>> spam = spam + 1
>>> spam
43 

作为快捷方式,你可以使用扩展赋值运算符 +=(这是常规运算符后跟一个等号)来完成相同的事情:

>>> spam = 42
>>> spam += 1
>>> spam
43 

对于 +, -, *, /, 和 % 运算符,有扩展赋值运算符,如表 6-1 所述。

表 6-1:扩展赋值运算符

增强赋值语句 等价的赋值语句
spam += 1 spam = spam + 1
spam -= 1 spam = spam - 1
spam *= 1 spam = spam * 1
spam /= 1 spam = spam / 1
spam %= 1 spam = spam % 1

+= 运算符也可以进行字符串和列表的连接,而 *= 运算符可以进行字符串和列表的复制。在交互式外壳中输入以下内容:

>>> spam = 'Hello,'
>>> spam += ' world!' # Same as spam = spam + 'world!'
>>> spam
'Hello, world!'
>>> bacon = ['Zophie']
>>> bacon *= 3 # Same as bacon = bacon * 3
>>> bacon
['Zophie', 'Zophie', 'Zophie'] 

与多重赋值技巧类似,增强赋值运算符是简化代码并使其更易于阅读的快捷方式。### 方法

方法 与函数相同,只是它是在值上 调用 的。例如,如果列表值存储在 spam 中,您将像这样在列表上调用 index() 列表方法(我将在稍后解释):spam.index('hello')。方法部分位于值之后,由点分隔。

每种数据类型都有自己的方法集。例如,列表数据类型具有几个有用的方法,用于在列表中查找、添加、删除和其他操作值。将方法视为始终与值相关联的函数。在我们的 spam 列表示例中,该函数在假设情况下将是 index(spam, 'hello')。但由于 index() 是列表方法而不是函数,我们调用 spam.index('hello')。在列表值上调用 index() 是 Python 知道 index() 是列表方法的方式。让我们来了解 Python 中的列表方法。

查找值

列表值有一个 index() 方法,可以传递一个值。如果该值存在于列表中,则该方法将返回值的索引。如果值不在列表中,则 Python 产生 ValueError 错误。在交互式外壳中输入以下内容:

>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> spam.index('hello')
0
>>> spam.index('heyas')
3
>>> spam.index('howdy howdy howdy')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.index('howdy howdy howdy')
ValueError: 'howdy howdy howdy' is not in list 

当列表包含值的重复时,该方法返回其首次出现的位置:

>>> spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka']
>>> spam.index('Pooka')
1 

注意,index() 返回 1,而不是 3

添加值

向列表中添加新值,请使用 append()insert() 方法。append() 方法将参数添加到列表的末尾:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.append('moose')
>>> spam
['cat', 'dog', 'bat', 'moose'] 

insert() 方法可以在列表中的任何索引处插入值。insert() 的第一个参数是新值的索引,第二个参数是要插入的新值。在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.insert(1, 'chicken')
>>> spam
['cat', 'chicken', 'dog', 'bat'] 

注意,代码没有执行任何赋值操作,例如 spam = spam.append('moose')spam = spam.insert(1, 'chicken')append()insert() 的返回值是 None,因此您绝对不希望将其存储为新变量的值。相反,这些方法在原地修改列表,这是在“可变和不可变数据类型”一节中更详细讨论的主题。

方法属于单一的数据类型。append()insert() 方法是列表方法,我们只能在列表值上调用它们,不能在其他数据类型的值上调用,例如字符串或整数。要查看尝试这样做会发生什么,请在交互式外壳中输入以下内容:

>>> eggs = 'hello'
>>> eggs.append('world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs.append('world')
AttributeError: 'str' object has no attribute 'append'
>>> bacon = 42
>>> bacon.insert(1, 'world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    bacon.insert(1, 'world')
AttributeError: 'int' object has no attribute 'insert' 

注意出现的 AttributeError 错误信息。

删除值

remove() 方法接受一个要从其调用的列表中删除的值:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('bat')
>>> spam
['cat', 'rat', 'elephant'] 

尝试删除列表中不存在的值将导致 ValueError 错误。例如,将以下内容输入到交互式外壳中,并注意显示的错误:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('chicken')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.remove('chicken')
ValueError: list.remove(x): x not in list 

如果值在列表中多次出现,该方法将只删除它的第一个实例:

>>> spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat']
>>> spam.remove('cat')
>>> spam
['bat', 'rat', 'cat', 'hat', 'cat'] 

del 语句在你知道要从列表中删除的值的索引时很有用,而 remove() 方法在你知道值本身时很有用。

排序值

你可以使用 sort() 方法对数字值的列表或字符串值的列表进行排序。例如,将以下内容输入到交互式外壳中:

>>> spam = [2, 5, 3.14, 1, -7]
>>> spam.sort()
>>> spam
[-7, 1, 2, 3.14, 5]
>>> spam = ['Ants', 'Cats', 'Dogs', 'Badgers', 'Elephants']
>>> spam.sort()
>>> spam
['Ants', 'Badgers', 'Cats', 'Dogs', 'Elephants'] 

该方法以数值顺序返回数字,以字母顺序返回字符串。你还可以通过将 True 作为 reverse 关键字参数传递给 sort() 方法调用,以逆序排序值:

>>> spam.sort(reverse=True)
>>> spam
['Elephants', 'Dogs', 'Cats', 'Badgers', 'Ants'] 

注意 sort() 方法的三个要点。首先,它就地排序列表;不要尝试通过编写像 spam = spam.sort() 这样的代码来捕获返回值。

其次,你不能对包含数字值和字符串值的列表进行排序,因为 Python 不知道如何比较这些值。将以下内容输入到交互式外壳中,并注意显示的 TypeError 错误:

>>> spam = [1, 3, 2, 4, 'Alice', 'Bob']
>>> spam.sort()
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.sort()
TypeError: '<' not supported between instances of 'str' and 'int' 

第三,sort() 使用 ASCII 顺序 而不是实际的字母顺序进行字符串排序。这意味着大写字母在小写字母之前,将小写字母 a 放在大写字母 Z 之后。例如,将以下内容输入到交互式外壳中:

>>> spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats']
>>> spam.sort()
>>> spam
['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats'] 

如果你需要按常规字母顺序排序值,请在 sort() 方法调用中传递 str.lower 作为 key 关键字参数:

>>> spam = ['a', 'z', 'A', 'Z']
>>> spam.sort(key=str.lower)
>>> spam
['a', 'A', 'z', 'Z'] 

这个参数会导致 sort() 函数将列表中的所有项目视为小写,而实际上并不改变列表中的值。

反转值

如果你需要快速反转列表中项的顺序,可以调用 reverse() 列表方法。将以下内容输入到交互式外壳中:

>>> spam = ['cat', 'dog', 'moose']
>>> spam.reverse()
>>> spam
['moose', 'dog', 'cat'] 

sort() 列表方法一样,reverse() 不会返回列表,这就是为什么我们写 spam.reverse() 而不是 spam = spam.reverse()

短路布尔运算符

布尔运算符有一个微妙的行为,很容易忽略。回想一下,如果由 and 运算符组合的两个值中的任何一个为 False,则整个表达式为 False,如果由 or 运算符组合的两个值中的任何一个为 True,则整个表达式为 True。如果我给你一个 False and spam 的表达式,无论 spam 变量的值是 True 还是 False,整个表达式都会是 False。同样,对于 True or spam;无论 spam 的值如何,它都会评估为 True

Python(以及许多其他编程语言)利用这个事实来优化代码,使其运行得更快,因为它根本不会检查布尔操作符的右侧。这个快捷方式被称为短路。大多数时候,你的程序的行为将与如果 Python 检查整个表达式时相同(尽管快几微秒)。然而,考虑这个简短的程序,其中我们检查列表中的第一个项目是否是 'cat'

spam = ['cat', 'dog']
if spam[0] == 'cat':
    print('A cat is the first item.')
else:
    print('The first item is not a cat.') 

正如编写的那样,这个程序打印 A cat is the first item. 但如果 spam 中的列表为空,spam[0] 代码将导致 IndexError: list Index out of range 错误。为了修复这个问题,我们将调整 if 语句的条件,以利用短路:

spam = []
if len(spam) > 0 and spam[0] == 'cat':
    print('A cat is the first item.')
else:
    print('The first item is not a cat.') 

这个程序永远不会出错,因为如果 len(spam) > 0False(即 spam 中的列表为空),那么短路 and 操作符意味着 Python 不会运行会导致 IndexError 错误的 spam[0] == 'cat' 代码。当你编写涉及 andor 操作符的代码时,请记住这种短路行为。

短程序:使用列表的 Magic 8 Ball

使用列表,你可以编写第四章的 magic8Ball.py 程序的一个更加优雅的版本。你不需要几行几乎相同的 elif 语句,而可以创建一个代码与之交互的单个列表。打开一个新的文件编辑窗口,并输入以下代码。将其保存为 magic8Ball2.py

import random

messages = ['It is certain',
    'It is decidedly so',
    'Yes definitely',
    'Reply hazy try again',
    'Ask again later',
    'Concentrate and ask again',
    'My reply is no',
    'Outlook not so good',
    'Very doubtful']

print('Ask a yes or no question:')
input('>')
print(messages[random.randint(0, len(messages) - 1)]) 

当你运行它时,你会看到它的工作方式与之前的 magic8Ball.py 程序相同。

random.randint(0, len(messages) - 1) 调用生成一个随机数作为索引,无论 messages 的大小如何。也就是说,你会得到一个介于 0len(messages) - 1 的值之间的随机数。这种方法的优点是,你可以轻松地向 messages 列表中添加和删除字符串,而无需更改其他代码行。如果你稍后更新你的代码,你将需要更改更少的行,从而减少了你引入错误的机会。

从列表中随机选择一个项目是如此常见,以至于 Python 有 random.choice(messages) 函数,它做的是 random.randint(0, len(messages) – 1) 相同的事情。

序列数据类型

列表并不是唯一表示有序值序列的数据类型。例如,如果你把字符串看作是单个文本字符的“列表”,那么字符串和列表实际上是相似的。Python 的序列数据类型包括列表、字符串、range() 返回的范围对象以及元组(在第 127 页的“元组数据类型”中解释)。你可以用列表做的许多事情也可以用字符串和其他序列类型的值来做。为了看到这一点,将以下内容输入到交互式外壳中:

>>> name = 'Zophie'
>>> name[0]
'Z'
>>> name[-2]
'i'
>>> name[0:4]
'Zoph'
>>> 'Zo' in name
True
>>> 'z' in name
False
>>> 'p' not in name
False
>>> for i in name:
...     print('* * * ' + i + ' * * *')
...
* * * Z * * *
* * * o * * *
* * * p * * *
* * * h * * *
* * * i * * *
* * * e * * * 

你可以用序列值做所有可以用列表做的事情:索引、切片、for 循环、len()in 以及 not in 操作符。

可变和不可变数据类型

但是列表和字符串在重要方面有所不同。列表值是一个可变数据类型:你可以添加、删除或更改其值。然而,字符串是不可变的:它不能被更改。尝试重新分配字符串中的单个字符会导致TypeError错误,就像你在交互式外壳中输入以下内容一样:

>>> name = 'Zophie a cat'
>>> name[7] = 'the'
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    name[7] = 'the'
TypeError: 'str' object does not support item assignment 

正确修改字符串的方法是使用切片和连接来通过复制旧字符串的部分来构建一个字符串:

>>> name = 'Zophie a cat'
>>> new_name = name[0:7] + 'the' + name[8:12]
>>> name
'Zophie a cat'
>>> new_name
'Zophie the cat' 

我们使用了[0:7][8:12]来引用我们不希望替换的字符。请注意,原始的'Zophie a cat'字符串没有被修改,因为字符串是不可变的。

虽然列表值确实是可变的,但以下代码的第二行并没有修改列表eggs

>>> eggs = ['A', 'B', 'C']
>>> eggs = ['x', 'y', 'z']
>>> eggs
['x', 'y', 'z'] 

在这里,eggs中的列表值并没有被改变;相反,一个新的完全不同的列表值(['x', 'y', 'z'])正在替换旧的列表值(['A', 'B', 'C'])。

如果你想要实际修改eggs中的原始列表以包含['x', 'y', 'z'],你必须使用del语句和append()方法,如下所示:

>>> eggs = ['A', 'B', 'C']
>>> del eggs[2]
>>> del eggs[1]
>>> del eggs[0]
>>> eggs.append('x')
>>> eggs.append('y')
>>> eggs.append('z')
>>> eggs
['x', 'y', 'z'] 

在这个例子中,eggs变量最终得到与它开始时相同的列表值。只是这个列表被改变(变异)而不是被覆盖。我们称这种在原地更改列表

可变类型与不可变类型之间的区别可能看似没有意义,但“第 129 页的‘引用’”将解释在调用函数时使用可变参数与不可变参数的不同行为。首先,让我们了解一下元组数据类型,它是列表数据类型的不可变形式。

元组数据类型

元组数据类型与列表数据类型之间只有两个区别。第一个区别是,你使用括号而不是方括号来编写元组。例如,在交互式外壳中输入以下内容:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[0]
'hello'
>>> eggs[1:3]
(42, 0.5)
>>> len(eggs)
3 

元组与列表的第二大区别是,元组,就像字符串一样,是不可变的:你不能修改、追加或删除它们的值。在交互式外壳中输入以下内容,并查看产生的TypeError错误信息:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[1] = 99
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs[1] = 99
TypeError: 'tuple' object does not support item assignment 

如果你元组中只有一个值,你可以在括号内的值后面放置一个尾随逗号来表示这一点。否则,Python 会认为你在常规括号内输入了一个值。(与一些其他编程语言不同,在 Python 中在列表或元组的最后一个项目后面放置尾随逗号是可以的。)在交互式外壳中输入以下type()函数调用以查看区别:

>>> type(('hello',))
<class 'tuple'>
>>> type(('hello'))
<class 'str'> 

你可以使用元组来向任何阅读你代码的人传达你不想让这些值序列发生改变。如果你需要一个永远不会改变的有序值序列,请使用元组。使用元组而不是列表的第二个好处是,由于它们是不可变的,并且其内容不会改变,Python 可以实现优化,使得使用元组的代码比使用列表的代码稍微快一些。

列表和元组类型转换

正如str(42)将返回表示整数42的字符串一样,list()tuple()函数将返回传递给它们的值的列表和元组版本。将以下内容输入到交互式外壳中,并注意返回值的数据类型与传递的值不同:

>>> tuple(['cat', 'dog', 5])
('cat', 'dog', 5)
>>> list(('cat', 'dog', 5))
['cat', 'dog', 5]
>>> list('hello')
['h', 'e', 'l', 'l', 'o'] 

如果你需要一个元组的可变版本,将元组转换为列表是很有用的。

参考文献

一个常见的比喻是,变量是“存储”字符串和整数等值的盒子。然而,这个解释是对 Python 实际所做事情的简化。一个更好的比喻是,变量是附有字符串的纸标签,它们附着在值上。将以下内容输入到交互式外壳中:

>>> spam = 42 # ❶
>>> eggs = spam # ❷
>>> spam = 99 # ❸
>>> spam
99
>>> eggs
42 

当你将42赋值给spam变量时,你实际上是在计算机内存中创建42的值,并在spam变量中存储对其的引用。当你复制spam中的值并将其赋值给变量eggs时,你是在复制引用。spameggs变量都引用计算机内存中的42值。使用变量的标签比喻,你已经将spam标签和eggs标签附着到了同一个42值上。当你将spam赋值为新的99值时,你改变了spam标签所引用的内容。图 6-2 是代码的图形表示。

三个带有标签的 Python 赋值语句表示的值

图 6-2:变量赋值不会重写值;它改变的是引用。描述

这种变化不会影响eggs,它仍然引用42的值。

但列表并不这样工作,因为列表值可以改变;也就是说,列表是可变的。以下代码将使这种区别更容易理解。将其输入到交互式外壳中:

>>> spam = [0, 1, 2, 3] # ❶
>>> eggs = spam  # The reference, not the list, is being copied. # ❷
>>> eggs[1] = 'Hello!'  # This changes the list value. # ❸
>>> spam
[0, 'Hello!', 2, 3]
>>> eggs  # The eggs variable refers to the same list.
[0, 'Hello!', 2, 3] 

这段代码可能看起来有些奇怪。它只接触了eggs列表,但eggsspam列表似乎都发生了变化。

当你创建列表❶时,你将对其的引用赋值给spam变量。但下一行只将spam中的列表引用复制到eggs❷,而不是列表本身。仍然只有一个列表,现在spameggs都引用它。只有一个底层列表的原因是列表本身从未真正被复制。因此,当你修改eggs的第一个元素❸时,你是在修改spam所引用的同一个列表。你可以在图 6-3 中看到这一点。

三个带有标签的 Python 赋值语句表示的值

图 6-3:因为spameggs引用的是同一个列表,所以改变一个也会改变另一个。描述

这会变得稍微复杂一些,因为列表也不直接包含值的序列,而是包含对值的引用序列。我在第 131 页的“copy()deepcopy()函数”中进一步解释了这一点。

虽然 Python 变量在技术上包含对值的引用,但人们经常随意地说变量包含值。但请记住以下两个规则:

  • 在 Python 中,变量永远不会包含值。它们只包含对值的引用。

  • 在 Python 中,=赋值运算符只复制引用。它永远不会复制值。

大多数情况下,您不需要了解这些细节,但有时,这些简单的规则会有意想不到的效果,您应该确切地了解 Python 正在做什么。

参数

引用对于理解参数如何传递给函数特别重要。当函数被调用时,Python 会将参数的引用复制到参数变量中。对于像列表(以及我在第七章中将要描述的字典)这样的可变值,这意味着函数中的代码会就地修改原始值。为了看到这一事实的后果,打开一个新的文件编辑器窗口,输入以下代码,并将其保存为passingReference.py

def eggs(some_parameter):
    some_parameter.append('Hello')

spam = [1, 2, 3]
eggs(spam)
print(spam)  # Prints [1, 2, 3, 'Hello'] 

注意,当您调用eggs()时,返回值不会将新值分配给spam。相反,它直接修改列表本身。当运行此程序时,它输出[1, 2, 3, 'Hello']

即使spamsome_parameter包含不同的引用,它们都指向同一个列表。这就是为什么函数内部的append('Hello')方法调用即使在函数调用返回后也会影响列表。

请记住这种行为。忘记 Python 以这种方式处理列表和字典变量可能会导致意外的行为和令人困惑的错误。

copy()deepcopy()函数

虽然传递引用通常是处理列表和字典的最方便方式,但如果函数修改了传递给它的列表或字典,您可能不希望这些更改出现在原始列表或字典值中。为了控制这种行为,Python 提供了一个名为copy的模块,该模块提供了copy()deepcopy()函数。其中第一个,copy.copy(),可以复制可变值(如列表或字典)的副本,而不仅仅是引用的副本。在交互式外壳中输入以下内容:

>>> import copy
>>> spam = ['A', 'B', 'C']
>>> cheese = copy.copy(spam) # Creates a duplicate copy of the list
>>> cheese[1] = 42  # Changes cheese
>>> spam  # The spam variable is unchanged.
['A', 'B', 'C']
>>> cheese  # The cheese variable is changed.
['A', 42, 'C'] 

现在spamcheese变量引用的是不同的列表,这就是为什么当您在索引1处分配42时,只有cheese中的列表被修改。

正如变量引用值而不是包含值一样,列表包含引用到值而不是值本身。您可以在图 6-4 中看到这一点。

标签“spam”上方的 X,分配给列表“[‘A,’ ‘B’, ‘C’]”的示例。标签“0”上方的勾选标记分配给值“A”,标签“1”分配给值“B”,标签“2”分配给值“C”。所有三个标签本身都分配给了标签“spam”

图 6-4:列表不直接包含值(左);它们包含对值的引用(右)。

如果你需要复制的列表包含列表,请使用 copy.deepcopy() 函数而不是 copy.copy()copy.deepcopy() 函数将复制这些内部列表。### 短程序:矩阵屏幕保护程序

在黑客科幻电影 The Matrix 中,计算机显示器显示着闪烁的绿色数字流,就像数字雨从玻璃窗上倾泻而下。这些数字可能没有意义,但看起来很酷。为了好玩,我们可以用 Python 创建自己的矩阵屏幕保护程序。将以下代码输入到一个新文件中,并将其保存为 matrixscreensaver.py

import random, sys, time

WIDTH = 70  # The number of columns

try:
    # For each column, when the counter is 0, no stream is shown.
    # Otherwise, it acts as a counter for how many times a 1 or 0
    # should be displayed in that column.
    columns = [0] * WIDTH
    while True:
        # Loop over each column:
        for i in range(WIDTH):
            if random.random() < 0.02:
                # Restart a stream counter on this column.
                # The stream length is between 4 and 14 characters long.
                columns[i] = random.randint(4, 14)

            # Print a character in this column:
            if columns[i] == 0:
                # Change this ' '' to '.' to see the empty spaces:
                print(' ', end='')
            else:
                # Print a 0 or 1:
                print(random.choice([0, 1]), end='')
                columns[i] -= 1  # Decrement the counter for this column.
        print()  # Print a newline at the end of the row of columns.
        time.sleep(0.1)  # Each row pauses for one tenth of a second.
except KeyboardInterrupt:
    sys.exit()  # When Ctrl-C is pressed, end the program. 

当你运行这个程序时,它会生成一系列的二进制 1 和 0,如图 6-5 所示。

一个显示随机放置的 1 和 0 字符的 Windows 命令提示符窗口,字符长度各异

图 6-5:矩阵屏幕保护程序

与之前的尖峰和锯齿程序类似,这个程序通过在无限循环中打印文本行来创建滚动动画,该循环在用户按下 CTRL-C 时停止。这个程序中的主要数据结构是 columns 列表,它包含 70 个整数,每个整数对应输出窗口的一列。当 columns 中的整数是 0 时,它会为该列打印一个空格。当它大于 0 时,它会随机打印一个 01,然后递减该整数。一旦整数减少到 0,该列再次打印一个空格。程序随机设置 columns 中的整数为介于 414 之间的整数,以生成随机的二进制 0 和 1 流。

让我们来看看程序的每个部分:

import random, sys, time

WIDTH = 70  # The number of columns 

我们导入 random 模块以使用其 choice()randint() 函数,导入 sys 模块以使用其 exit() 函数,以及导入 time 模块以使用其 sleep() 函数。我们还设置了一个名为 WIDTH 的变量,将其设置为 70,以便程序为 70 列字符生成输出。你可以根据运行程序窗口的大小将此值更改为更大的或更小的整数。

WIDTH 变量使用全大写名称,因为它是一个常量变量。一个 常量 是一旦设置后代码不应该更改的变量。使用常量可以使你编写更易读的代码,例如 columns = [0] * WIDTH 而不是 columns = [0] * 70,这样当你稍后再次阅读代码时,可能会疑惑 70 的含义。在 Python 中,没有东西可以阻止你更改常量的值,但大写名称可以提醒程序员不要这样做。

程序的大部分内容发生在 try 块内部,该块会捕获用户按下 CTRL-C 以引发 KeyboardInterrupt 异常的情况:

try:
    # For each column, when the counter is 0, no stream is shown.
    # Otherwise, it acts as a counter for how many times a 1 or 0
    # should be displayed in that column.
    columns = [0] * WIDTH 

columns 变量包含一个由 0 组成的整数列表。这个列表中的整数数量等于 WIDTH。这些整数中的每一个都控制着输出窗口的某一列是否打印二进制数字流:

 while True:
        # Loop over each column:
        for i in range(WIDTH):
            if random.random() < 0.02:
                # Restart a stream counter on this column.
                # The stream length is between 4 and 14 characters long.
                columns[i] = random.randint(4, 14) 

我们希望这个程序永远运行,所以我们将它全部放在一个无限while True:循环中。在这个循环内部有一个for循环,它遍历单行中的每一列。循环变量i代表列的索引;它从0开始,到但不包括WIDTHcolumns[0]中的值代表应该打印在最左边的列中,columns[1]为从左数第二列打印,以此类推。

对于每一列,有 2%的概率将columns[i]中的整数设置为414之间的一个数字。我们通过比较random.random()(一个返回0.01.0之间随机浮点数的函数)和0.02来计算这个概率。如果你想使流更密集或更稀疏,可以相应地增加或减少这个数字。我们将每个列的计数器整数设置为 4 到 14 之间的一个随机数:

 # Print a character in this column:
            if columns[i] == 0:
                # Change this ' '' to '.' to see the empty spaces:
                print(' ', end='')
            else:
                # Print a 0 or 1:
                print(random.choice([0, 1]), end='')
                columns[i] -= 1  # Decrement the counter for this column. 

for循环内部,程序会判断是否应该打印一个随机的01二进制数或者一个空格。如果columns[i]0,则打印一个空格。否则,它将列表[0, 1]传递给random.choice()函数,该函数从列表中返回一个随机值以打印。代码还会递减columns[i]的计数器,使其更接近0,并且不再打印二进制数。

如果你想看到程序打印的“空”空间,尝试将字符串' '改为'.'并再次运行程序。输出应该看起来像这样:

............................1.........................................
................0...........1......................1..................
................1...........0................1.....0..................
............1...0...........0.....0..........1.....0..................
............1.1.1...........0.....0..........1.....1..1...............
............0.0.0...........0.....1.........00.....1..1............... 

else块结束后,for循环块也结束了:

 print()  # Print a newline at the end of the row of columns.
        time.sleep(0.1)  # Each row pauses for one tenth of a second.
except KeyboardInterrupt:
    sys.exit()  # When Ctrl-C is pressed, end the program. 

for循环之后的print()调用打印一个换行符,因为之前的每个列的print()调用都传递了end=''关键字参数,以防止在每个列后打印换行符。对于打印的每一行,程序通过调用time.sleep(0.1)引入了十分之一秒的暂停。

程序的最后部分是一个except块,如果用户按下 CTRL-C 引发KeyboardInterrupt异常,则退出程序。

摘要

列表是有用的数据类型,因为它们允许你在单个变量中编写可以修改的值的数量可变的代码。在这本书的后面部分,你会看到使用列表来完成其他情况下可能很难或不可能完成的任务的程序。

列表是一个可变的数据类型序列,这意味着其内容可以改变。元组和字符串虽然是序列数据类型,但它们是不可变的,不能被更改。我们可以用新的元组或字符串值覆盖包含元组或字符串值的变量,这并不等同于修改现有值——比如append()remove()方法在列表上所做的修改。

变量不直接存储列表值;它们存储对列表的引用。当你复制变量或在函数调用中传递列表时,这是一个重要的区别。因为正在复制的值是列表引用,所以请注意,你对列表所做的任何更改可能会影响程序中的另一个变量。如果你想在一个变量中对列表进行更改而不修改原始列表,可以使用 copy()deepcopy()

实践问题

1.  [] 是什么?

2.  如果你要将值 'hello' 赋给名为 spam 的变量中列表的第三个值?(假设 spam 包含 [2, 4, 6, 8, 10]。)

对于以下三个问题,假设 spam 包含列表 ['a', 'b', 'c', 'd']

3.  spam[int(int('3' * 2) // 11)] 的值是多少?

4.  spam[-1] 的值是多少?

5.  spam[:2] 的值是多少?

对于以下三个问题,假设 bacon 包含列表 [3.14, 'cat', 11, 'cat', True]

6.  bacon.index('cat') 的值是多少?

7.  bacon.append(99) 会将 bacon 中的列表值变成什么样子?

8.  bacon.remove('cat') 会将 bacon 中的列表值变成什么样子?

9.  列表连接和列表复制的运算符是什么?

10.  append()insert() 列表方法之间的区别是什么?

11.  从列表中移除值有两种方法?

12.  列举一些列表值与字符串值相似的方式。

13.  列表和元组之间的区别是什么?

14.  如何编写一个只包含整数 42 的元组值?

15.  如何获取列表值的元组形式?如何获取元组值的列表形式?

16.  “包含”列表值的变量实际上并不直接包含列表。它们包含什么?

17.  copy.copy()copy.deepcopy() 之间的区别是什么?

Practice Programs

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

逗号代码

假设你有一个类似这样的列表值:

spam = ['apples', 'bananas', 'tofu', 'cats']

编写一个函数,该函数接受一个列表值作为参数,并返回一个字符串,其中所有项目由逗号和空格分隔,并在最后一个项目之前插入 and。例如,将之前的 spam 列表传递给函数将返回 'apples, bananas, tofu, and cats'。但你的函数应该能够处理传递给它的任何列表值。确保测试将空列表 [] 传递给函数的情况。

硬币翻转连续出现

对于这个练习,我们将尝试进行一个实验。如果你抛硬币 100 次,每次正面写一个H,每次反面写一个T,你会创建一个看起来像T T T T H H H H T T的列表。如果你要求一个人做出 100 次随机抛硬币的结果,你可能会得到交替的正面-反面结果,如H T H T H H T H T T——这看起来是随机的(对人类来说),但实际上并不是数学上的随机。人类几乎不可能连续写下六个正面或六个反面,尽管在真正的随机抛硬币中这种情况很可能发生。人类在随机性方面是可预测地差的。

编写一个程序来找出在随机生成的 100 个正面和反面的列表中,连续六个正面或连续六个反面的出现频率。你的程序应该将实验分为两部分:第一部分生成一个包含 100 个随机选择的'H''T'值的列表,第二部分检查其中是否存在连续的序列。将所有这些代码放入一个循环中,重复实验 10000 次,以便找出有多少百分比的钱币抛掷包含连续六个正面或六个反面的序列。作为一个提示,函数调用random.randint(0, 1)在 50%的时间内返回0值,在另外 50%的时间内返回1值。

你可以从以下模板开始:

import random
number_of_streaks = 0
for experiment_number in range(10000):  # Run 100,000 experiments total.
    # Code that creates a list of 100 'heads' or 'tails' values

    # Code that checks if there is a streak of 6 heads or tails in a row

print('Chance of streak: %s%%' % (number_of_streaks / 100)) 

当然,这只是一个估计,但 10000 是一个相当大的样本量。一些数学知识可以给你一个确切的答案,并节省你编写程序的努力,但程序员在数学上通常是出了名的差。

要创建一个列表,可以使用一个for循环,将随机选择的'H''T'追加到列表中 100 次。为了确定是否存在连续六个正面或六个反面,创建一个类似于some_list[i:i+6]的切片(它包含从索引i开始的六个项目)并将其与列表值['H', 'H', 'H', 'H', 'H', 'H']['T', 'T', 'T', 'T', 'T', 'T']进行比较。### 列表数据类型

一个列表是一个包含多个值的有序序列的值。术语列表值指的是列表本身(你可以将其存储在变量中或传递给函数,就像任何其他值一样),而不是列表值内的值。列表值看起来像这样:['cat', 'bat', 'rat', 'elephant']。就像字符串值使用引号来标记字符串的开始和结束一样,列表以一个开方括号开始,以一个闭方括号结束,[]

我们称列表内的值为项目。项目用逗号分隔(即,它们是逗号分隔的)。例如,将以下内容输入到交互式外壳中:

>>> [1, 2, 3]  # A list of three integers
[1, 2, 3]
>>> ['cat', 'bat', 'rat', 'elephant']  # A list of four strings
['cat', 'bat', 'rat', 'elephant']
>>> ['hello', 3.1415, True, None, 42]  # A list of several values
['hello', 3.1415, True, None, 42]
>>> spam = ['cat', 'bat', 'rat', 'elephant'] # ❶
>>> spam
['cat', 'bat', 'rat', 'elephant'] 

spam变量❶仅分配了一个值:列表值。但列表值本身包含其他值。

注意,值[]是一个不包含任何值的空列表,类似于'',空字符串。

索引

假设您有一个名为 spam 的变量存储了列表 ['cat', 'bat', 'rat', 'elephant']。Python 代码 spam[0] 将计算结果为 'cat',代码 spam[1] 将计算结果为 'bat',依此类推。方括号中跟随列表的整数称为 索引。列表中的第一个值位于索引 0,第二个值位于索引 1,第三个值位于索引 2,依此类推。图 6-1 显示了分配给 spam 的列表值及其计算到的索引表达式。请注意,因为第一个索引是 0,所以最后一个索引是列表大小的减一。因此,包含四个项目的列表其最后一个索引为 3

一个展示列表中每个元素如何对应索引的图表

图 6-1:存储在变量 spam 中的列表值,显示每个索引指向哪个值 描述

为了举例说明如何使用索引,请在交互式外壳中输入以下表达式。我们首先将一个列表分配给变量 spam

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0]
'cat'
>>> spam[1]
'bat'
>>> spam[2]
'rat'
>>> spam[3]
'elephant'
>>> ['cat', 'bat', 'rat', 'elephant'][3]
'elephant'
>>> 'Hello, ' + spam[0] # ❶
'Hello, cat' # ❷
>>> 'The ' + spam[1] + ' ate the ' + spam[0] + '.'
'The bat ate the cat.' 

注意到表达式 'Hello, ' + spam[0] ❶ 计算结果为 'Hello, ' + 'cat',因为 spam[0] 计算结果为字符串 'cat'。这个表达式进一步计算结果为字符串值 'Hello, cat' ❷。

如果您使用一个超出列表值中值的索引,Python 将给出 IndexError 错误信息:

>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[10000]
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam[10000]
IndexError: list index out of range 

列表也可以包含其他列表值。您可以使用多个索引访问这些列表中的列表值,如下所示:

>>> spam = [['cat', 'bat'], [10, 20, 30, 40, 50]]
>>> spam[0]
['cat', 'bat']
>>> spam[0][1]
'bat'
>>> spam[1][4]
50 

第一个索引指定要使用哪个列表值,第二个指定列表值内的值。例如,spam[0][1] 打印 'bat',即第一个列表中的第二个值。

负数索引

当索引从 0 开始向上时,您也可以使用负整数作为索引。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[-1]  # Last index
'elephant'
>>> spam[-3]  # Third to last index
'bat'
>>> 'The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.'
'The elephant is afraid of the bat.' 

整数值 -1 指向列表中的最后一个索引,值 -2 指向列表中的倒数第二个索引,依此类推。

切片

正如索引可以从列表中获取单个值一样,切片 可以从列表中获取多个值,形式为一个新的列表。我们输入一个切片在方括号中,就像索引一样,但包括两个由冒号分隔的整数。注意索引和切片之间的区别:

  • spam[2] 是一个包含索引(一个整数)的列表。

  • spam[1:4] 是一个包含切片(两个整数)的列表。

在切片中,第一个整数是切片开始的索引。第二个整数是切片结束的索引。从切片创建的列表将延伸到第二个索引的值,但不包括第二个索引的值。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0:4]
['cat', 'bat', 'rat', 'elephant']
>>> spam[1:3]
['bat', 'rat']
>>> spam[0:-1]
['cat', 'bat', 'rat'] 

作为快捷方式,您可以在切片冒号两侧省略一个或两个索引:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[:2]
['cat', 'bat']
>>> spam[1:]
['bat', 'rat', 'elephant']
>>> spam[:]
['cat', 'bat', 'rat', 'elephant'] 

省略第一个索引等同于使用 0,即列表的开始。省略第二个索引等同于使用列表的长度,这将切片到列表的末尾。

len() 函数

len() 函数将返回传递给它的列表值的值的数量。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'moose']
>>> len(spam)
3 

这种行为与函数计算字符串值中字符数的方式类似。

值更新

通常,变量名位于赋值语句的左侧,例如 spam = 42。然而,你也可以使用列表的索引来更改该索引处的值:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[1] = 'aardvark'
>>> spam
['cat', 'aardvark', 'rat', 'elephant']
>>> spam[2] = spam[1]
>>> spam
['cat', 'aardvark', 'aardvark', 'elephant']
>>> spam[-1] = 12345
>>> spam
['cat', 'aardvark', 'aardvark', 12345] 

在这个例子中,spam[1] = 'aardvark' 表示“将列表 spam 中索引 1 的值赋给字符串 'aardvark'。”你也可以使用负索引,如 -1 来更新列表。

连接和复制

你可以使用 +* 运算符连接和复制列表,就像字符串一样:

>>> [1, 2, 3] + ['A', 'B', 'C']
[1, 2, 3, 'A', 'B', 'C']
>>> ['X', 'Y', 'Z'] * 3
['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
>>> spam = [1, 2, 3]
>>> spam = spam + ['A', 'B', 'C']
>>> spam
[1, 2, 3, 'A', 'B', 'C'] 

+ 运算符将两个列表合并以创建一个新的列表值,而 * 运算符将一个列表和一个整数值组合起来以复制列表。

删除语句

del 语句会删除列表中指定索引的值。删除值之后列表中的所有值都会向上移动一个索引。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat'] 

del 语句还可以对简单变量进行操作以删除它,就像是一个“取消赋值”语句。如果你在删除变量后尝试使用该变量,你会得到一个 NameError 错误,因为该变量不再存在。然而,在实际应用中,你几乎不需要删除简单变量,del 语句主要用于从列表中删除值。

索引

假设你有一个名为 spam 的变量存储了列表 ['cat', 'bat', 'rat', 'elephant']。Python 代码 spam[0] 将计算结果为 'cat',代码 spam[1] 将计算结果为 'bat',依此类推。方括号中跟随列表的整数称为索引。列表中的第一个值位于索引 0,第二个值位于索引 1,第三个值位于索引 2,依此类推。图 6-1 显示了分配给 spam 的列表值及其对应的索引表达式。请注意,因为第一个索引是 0,所以最后一个索引是列表大小的减一。因此,包含四个项目的列表其最后一个索引为 3

一个显示列表中的每个项目如何对应一个索引的图表

图 6-1:存储在变量 spam 中的列表值,显示每个索引指向哪个值 描述

为了举例说明如何使用索引,请在交互式外壳中输入以下表达式。我们首先将一个列表赋值给变量 spam

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0]
'cat'
>>> spam[1]
'bat'
>>> spam[2]
'rat'
>>> spam[3]
'elephant'
>>> ['cat', 'bat', 'rat', 'elephant'][3]
'elephant'
>>> 'Hello, ' + spam[0] # ❶
'Hello, cat' # ❷
>>> 'The ' + spam[1] + ' ate the ' + spam[0] + '.'
'The bat ate the cat.' 

注意,表达式 'Hello, ' + spam[0] ❶ 计算结果为 'Hello, ' + 'cat',因为 spam[0] 计算结果为字符串 'cat'。这个表达式进一步计算结果为字符串值 'Hello, cat' ❷。

如果你使用一个超出列表值中值的数量的索引,Python 将给出一个 IndexError 错误信息:

>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[10000]
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam[10000]
IndexError: list index out of range 

列表也可以包含其他列表值。您可以使用多个索引访问这些列表中的值,如下所示:

>>> spam = [['cat', 'bat'], [10, 20, 30, 40, 50]]
>>> spam[0]
['cat', 'bat']
>>> spam[0][1]
'bat'
>>> spam[1][4]
50 

第一个索引决定了使用哪个列表值,第二个索引表示列表值中的值。例如,spam[0][1] 打印 'bat',即第一个列表中的第二个值。

负数索引

当索引从 0 开始向上计数时,您也可以使用负整数作为索引。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[-1]  # Last index
'elephant'
>>> spam[-3]  # Third to last index
'bat'
>>> 'The ' + spam[-1] + ' is afraid of the ' + spam[-3] + '.'
'The elephant is afraid of the bat.' 

整数值 -1 指的是列表中的最后一个索引,值 -2 指的是列表中的倒数第二个索引,依此类推。

切片

就像索引可以从列表中获取单个值一样,切片 也可以从列表中获取多个值,形式为一个新的列表。我们使用方括号输入切片,就像索引一样,但包括两个由冒号分隔的整数。注意索引和切片之间的区别:

  • spam[2] 是一个带有索引(一个整数)的列表。

  • spam[1:4] 是一个带有切片(两个整数)的列表。

在切片中,第一个整数是切片开始的索引。第二个整数是切片结束的索引。从切片创建的列表将延伸到第二个索引的值,但不包括该值。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[0:4]
['cat', 'bat', 'rat', 'elephant']
>>> spam[1:3]
['bat', 'rat']
>>> spam[0:-1]
['cat', 'bat', 'rat'] 

作为快捷方式,您可以在冒号两侧省略一个或两个索引:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[:2]
['cat', 'bat']
>>> spam[1:]
['bat', 'rat', 'elephant']
>>> spam[:]
['cat', 'bat', 'rat', 'elephant'] 

省略第一个索引等同于使用 0,即列表的开始。省略第二个索引等同于使用列表的长度,这将切片到列表的末尾。

len() 函数

len() 函数将返回传递给它的列表值中的值的数量。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'moose']
>>> len(spam)
3 

这种行为与函数计算字符串值中字符数的方式类似。

值更新

通常,变量名位于赋值语句的左侧,例如 spam = 42。但是,您也可以使用列表的索引来更改该索引处的值:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam[1] = 'aardvark'
>>> spam
['cat', 'aardvark', 'rat', 'elephant']
>>> spam[2] = spam[1]
>>> spam
['cat', 'aardvark', 'aardvark', 'elephant']
>>> spam[-1] = 12345
>>> spam
['cat', 'aardvark', 'aardvark', 12345] 

在此示例中,spam[1] = 'aardvark' 表示“将列表 spam 中索引 1 处的值赋给字符串 'aardvark'。”您也可以使用负索引,如 -1 来更新列表。

连接和复制

您可以使用 +* 运算符连接和复制列表,就像字符串一样:

>>> [1, 2, 3] + ['A', 'B', 'C']
[1, 2, 3, 'A', 'B', 'C']
>>> ['X', 'Y', 'Z'] * 3
['X', 'Y', 'Z', 'X', 'Y', 'Z', 'X', 'Y', 'Z']
>>> spam = [1, 2, 3]
>>> spam = spam + ['A', 'B', 'C']
>>> spam
[1, 2, 3, 'A', 'B', 'C'] 

+ 运算符将两个列表合并以创建一个新的列表值,而 * 运算符将列表与一个整数值结合以复制列表。

del 语句

del 语句会删除列表中的索引处的值。删除值之后列表中的所有值将向上移动一个索引。例如,在交互式外壳中输入以下内容:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat', 'elephant']
>>> del spam[2]
>>> spam
['cat', 'bat'] 

del 语句也可以在简单变量上操作以删除它,就像它是一个“未赋值”语句一样。如果你在删除变量后尝试使用该变量,你会得到一个 NameError 错误,因为该变量不再存在。然而,在实践中,你几乎不需要删除简单变量,del 语句主要用于从列表中删除值。

与列表一起工作

当你刚开始编写程序时,你可能倾向于创建许多单独的变量来存储一组相似值。例如,如果我想存储我的猫的名字,我可能会想到编写如下代码:

cat_name_1 = 'Zophie'
cat_name_2 = 'Pooka'
cat_name_3 = 'Simon'
cat_name_4 = 'Lady Macbeth' 

结果表明,这是一种编写代码的糟糕方式。首先,如果猫的数量发生变化(你总是可以拥有更多的猫),你的程序将无法存储比你变量更多的猫。这些程序还包含大量重复或几乎相同的代码。为了实际看到这一点,将以下程序输入到文件编辑器中,并将其保存为 allMyCats1.py

print('Enter the name of cat 1:')
cat_name_1 = input()
print('Enter the name of cat 2:')
cat_name_2 = input()
print('Enter the name of cat 3:')
cat_name_3 = input()
print('Enter the name of cat 4:')
cat_name_4 = input()
print('The cat names are:')
print(cat_name_1 + ' ' + cat_name_2 + ' ' + cat_name_3 + ' ' + cat_name_4) 

与使用多个重复的变量相比,你可以使用一个包含列表值的单个变量。例如,这里有一个新的改进版本的 allMyCats1.py 程序。这个新版本使用单个列表,可以存储用户输入的任意数量的猫。在一个新的文件编辑窗口中,输入以下源代码并将其保存为 allMyCats2.py

cat_names = []
while True:
    print('Enter the name of cat ' + str(len(cat_names) + 1) +
      ' (Or enter nothing to stop.):')
    name = input()
    if name == '':
        break
    cat_names = cat_names + [name]  # List concatenation
print('The cat names are:')
for name in cat_names:
    print('  ' + name) 

当你运行这个程序时,输出将类似于以下内容:

Enter the name of cat 1 (Or enter nothing to stop.):
Zophie
Enter the name of cat 2 (Or enter nothing to stop.):
Pooka
Enter the name of cat 3 (Or enter nothing to stop.):
Simon
Enter the name of cat 4 (Or enter nothing to stop.):
Lady Macbeth
Enter the name of cat 5 (Or enter nothing to stop.):

The cat names are:
  Zophie
  Pooka
  Simon
  Lady Macbeth 

使用列表的好处是,你的数据现在在一个结构中,因此你的程序可以比使用多个重复变量更灵活地处理数据。

for 循环和列表

在第三章中,你学习了如何使用 for 循环执行一定次数的代码块。技术上讲,for 循环对列表值中的每个项目重复一次代码块。例如,如果你运行此代码

for i in range(4):
    print(i) 

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

0
1
2
3 

这是因为 range(4) 的返回值是一个序列值,Python 认为它与 [0, 1, 2, 3] 类似。以下程序与上一个程序具有相同的输出:

for i in [0, 1, 2, 3]:
    print(i) 

前面的 for 循环实际上在每次迭代中通过变量 i 设置为 [0, 1, 2, 3] 列表中的连续值来遍历其子句。

一个常见的 Python 技巧是使用 range(len(some_list))for 循环一起迭代列表的索引。例如,将以下内容输入到交互式外壳中:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for i in range(len(supplies)):
...     print('Index ' + str(i) + ' in supplies is: ' + supplies[i])
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

在之前显示的 for 循环中使用 range(len(supplies)) 很方便,因为循环中的代码可以访问索引(作为变量 i)和该索引的值(作为 supplies[i])。最好的是,range(len(supplies)) 将遍历 supplies 的所有索引,无论列表包含多少项。

in 和 not in 操作符

你可以使用 innot in 操作符来确定一个值是否在列表中。像其他操作符一样,innot in 出现在表达式中,连接两个值:要查找的值和可能找到该值的列表。这些表达式将评估为布尔值。要查看它们是如何工作的,请在交互式外壳中输入以下内容:

>>> 'howdy' in ['hello', 'hi', 'howdy', 'heyas']
True
>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> 'cat' in spam
False
>>> 'howdy' not in spam
False
>>> 'cat' not in spam
True 

以下程序允许用户输入一个宠物名字,然后检查该名字是否在宠物列表中。打开一个新的文件编辑器窗口,输入以下代码,并将其保存为 myPets.py

my_pets = ['Zophie', 'Pooka', 'Fat-tail']
print('Enter a pet name:')
name = input()
if name not in my_pets:
    print('I do not have a pet named ' + name)
else:
    print(name + ' is my pet.') 

输出可能看起来像这样:

Enter a pet name:
Footfoot
I do not have a pet named Footfoot 

请记住,not in 操作符与布尔 not 操作符不同。

多重赋值技巧

多重赋值技巧(技术上称为元组解包)是一种快捷方式,允许你在一行代码中将多个变量与列表中的值赋值。因此,而不是这样做

>>> cat = ['fat', 'gray', 'loud']
>>> size = cat[0]
>>> color = cat[1]
>>> disposition = cat[2] 

你可以输入以下行代码:

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition = cat 

变量的数量和列表的长度必须完全相等,否则 Python 会给你一个 ValueError

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition, name = cat
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    size, color, disposition, name = cat
ValueError: not enough values to unpack (expected 4, got 3) 

这个技巧使你的代码比输入三行代码更短、更易读。

列表项目枚举

你可以使用 enumerate() 函数代替 range(len(some_list)) 技巧和 for 循环来获取列表中项目的整数索引。在循环的每次迭代中,enumerate() 将返回两个值:列表中项目的索引和列表中的项目本身。例如,此代码与第 115 页“for 循环和列表”中的代码等效:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for index, item in enumerate(supplies):
...     print('Index ' + str(index) + ' in supplies is: ' + item)
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

enumerate() 函数在需要循环块中的项目和项目索引时非常有用。

随机选择和排序

random 模块有几个接受列表作为参数的函数。random.choice() 函数将从列表中返回一个随机选择的项。在交互式外壳中输入以下内容:

>>> import random
>>> pets = ['Dog', 'Cat', 'Moose']
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Dog' 

你可以将 random.choice(some_list) 视为 some_list[random.randint(0, len(some_list) - 1)] 的简写形式。

random.shuffle() 函数将就地重新排列列表中的项目。在交互式外壳中输入以下内容:

>>> import random
>>> people = ['Alice', 'Bob', 'Carol', 'David']
>>> random.shuffle(people)
>>> people
['Carol', 'David', 'Alice', 'Bob']
>>> random.shuffle(people)
>>> people
['Alice', 'David', 'Bob', 'Carol'] 

此函数将就地修改列表,而不是返回一个新列表。

for 循环和列表

在第三章中,你学习了如何使用 for 循环执行一定次数的代码块。技术上讲,for 循环对列表值中的每个项目重复一次代码块。例如,如果你运行此代码

for i in range(4):
    print(i) 

此程序的输出如下:

0
1
2
3 

这是因为 range(4) 的返回值是一个序列值,Python 认为它与 [0, 1, 2, 3] 类似。以下程序与上一个程序具有相同的输出:

for i in [0, 1, 2, 3]:
    print(i) 

之前的 for 循环实际上在每次迭代中通过将变量 i 设置为 [0, 1, 2, 3] 列表中的连续值来遍历其子句。

Python 中一个常见的技巧是使用 range(len(some_list))for 循环一起遍历列表的索引。例如,在交互式壳中输入以下内容:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for i in range(len(supplies)):
...     print('Index ' + str(i) + ' in supplies is: ' + supplies[i])
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

在之前显示的 for 循环中使用 range(len(supplies)) 很方便,因为循环中的代码可以访问索引(作为变量 i)以及该索引的值(作为 supplies[i])。最好的是,range(len(supplies)) 将遍历 supplies 的所有索引,无论列表包含多少项。

innot in 操作符

你可以使用 innot in 操作符来确定一个值是否在列表中。像其他操作符一样,innot in 出现在表达式中,连接两个值:要查找的值和可能找到该值的列表。这些表达式将评估为布尔值。要查看它们是如何工作的,请在交互式壳中输入以下内容:

>>> 'howdy' in ['hello', 'hi', 'howdy', 'heyas']
True
>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> 'cat' in spam
False
>>> 'howdy' not in spam
False
>>> 'cat' not in spam
True 

以下程序允许用户输入一个宠物名字,然后检查该名字是否在宠物列表中。打开一个新的文件编辑器窗口,输入以下代码,并将其保存为 myPets.py

my_pets = ['Zophie', 'Pooka', 'Fat-tail']
print('Enter a pet name:')
name = input()
if name not in my_pets:
    print('I do not have a pet named ' + name)
else:
    print(name + ' is my pet.') 

输出可能看起来像这样:

Enter a pet name:
Footfoot
I do not have a pet named Footfoot 

请记住,not in 操作符与布尔 not 操作符是不同的。

多重赋值技巧

多重赋值技巧(技术上称为元组解包)是一个快捷方式,允许你在一行代码中将多个变量与列表中的值进行赋值。因此,而不是这样做

>>> cat = ['fat', 'gray', 'loud']
>>> size = cat[0]
>>> color = cat[1]
>>> disposition = cat[2] 

你可以输入以下代码行:

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition = cat 

变量的数量和列表的长度必须完全相等,否则 Python 会给你一个 ValueError

>>> cat = ['fat', 'gray', 'loud']
>>> size, color, disposition, name = cat
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    size, color, disposition, name = cat
ValueError: not enough values to unpack (expected 4, got 3) 

这个技巧使你的代码比输入三行代码更短、更易读。

列表项枚举

而不是使用 range(len(some_list)) 技巧与 for 循环来获取列表中项的整数索引,你可以调用 enumerate() 函数。在循环的每次迭代中,enumerate() 将返回两个值:列表中项的索引和列表中的项本身。例如,此代码与第 115 页“for 循环和列表”中的代码等效:

>>> supplies = ['pens', 'staplers', 'flamethrowers', 'binders']
>>> for index, item in enumerate(supplies):
...     print('Index ' + str(index) + ' in supplies is: ' + item)
...
Index 0 in supplies is: pens
Index 1 in supplies is: staplers
Index 2 in supplies is: flamethrowers
Index 3 in supplies is: binders 

enumerate() 函数在需要循环块中的项及其索引时很有用。

随机选择和排序

random 模块有几个接受列表作为参数的函数。random.choice() 函数将从列表中返回一个随机选择的项。在交互式壳中输入以下内容:

>>> import random
>>> pets = ['Dog', 'Cat', 'Moose']
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Cat'
>>> random.choice(pets)
'Dog' 

你可以将 random.choice(some_list) 视为 some_list[random.randint(0, len(some_list) – 1)]` 的简写形式。

random.shuffle() 函数将就地重新排列列表中的项。在交互式壳中输入以下内容:

>>> import random
>>> people = ['Alice', 'Bob', 'Carol', 'David']
>>> random.shuffle(people)
>>> people
['Carol', 'David', 'Alice', 'Bob']
>>> random.shuffle(people)
>>> people
['Alice', 'David', 'Bob', 'Carol'] 

此函数会就地修改列表,而不是返回一个新的列表。

增量赋值运算符

与字符串一起工作的 +* 运算符也适用于列表,因此让我们短暂地了解一下增量赋值运算符。在将值赋给变量时,您将经常使用该变量本身。例如,在将 42 赋给变量 spam 之后,您可以使用以下代码将 spam 中的值增加 1

>>> spam = 42
>>> spam = spam + 1
>>> spam
43 

作为快捷方式,您可以使用增量赋值运算符 +=(这是常规运算符后跟一个等号)来完成相同的事情:

>>> spam = 42
>>> spam += 1
>>> spam
43 

对于 +-*/% 运算符,存在增量赋值运算符,如表 6-1 所述。

表 6-1:增量赋值运算符

增量赋值语句 等价的赋值语句
spam += 1 spam = spam + 1
spam -= 1 spam = spam - 1
spam *= 1 spam = spam * 1
spam /= 1 spam = spam / 1
spam %= 1 spam = spam % 1

+= 运算符还可以执行字符串和列表连接,而 *= 运算符可以执行字符串和列表复制。在交互式外壳中输入以下内容:

>>> spam = 'Hello,'
>>> spam += ' world!' # Same as spam = spam + 'world!'
>>> spam
'Hello, world!'
>>> bacon = ['Zophie']
>>> bacon *= 3 # Same as bacon = bacon * 3
>>> bacon
['Zophie', 'Zophie', 'Zophie'] 

与多重赋值技巧一样,增量赋值运算符是使您的代码更简单、更易读的快捷方式。

方法

方法 与函数相同,只是它是在一个值上 调用 的。例如,如果列表值存储在 spam 中,您将像这样调用该列表的 index() 列表方法(我将在稍后解释):spam.index('hello')。方法部分位于值之后,由点分隔。

每种数据类型都有自己的方法集。例如,列表数据类型具有用于在列表中查找、添加、删除和其他操作值的几个有用方法。将方法视为始终与一个值相关联的函数。在我们的 spam 列表示例中,函数将是假设的 index(spam, 'hello')。但由于 index() 是列表方法而不是函数,我们调用 spam.index('hello')。在列表值上调用 index() 是 Python 知道 index() 是列表方法的方式。让我们了解 Python 中的列表方法。

查找值

列表值有一个 index() 方法,可以传递一个值。如果该值存在于列表中,则该方法将返回该值的索引。如果该值不在列表中,则 Python 产生一个 ValueError 错误。在交互式外壳中输入以下内容:

>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> spam.index('hello')
0
>>> spam.index('heyas')
3
>>> spam.index('howdy howdy howdy')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.index('howdy howdy howdy')
ValueError: 'howdy howdy howdy' is not in list 

当列表包含值的重复时,该方法返回其首次出现的位置:

>>> spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka']
>>> spam.index('Pooka')
1 

注意,index() 返回 1,而不是 3

添加值

要向列表添加新值,请使用 append()insert() 方法。append() 方法将参数添加到列表的末尾:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.append('moose')
>>> spam
['cat', 'dog', 'bat', 'moose'] 

insert() 方法可以在列表中的任何位置插入一个值。insert() 的第一个参数是新值的索引,第二个参数是要插入的新值。在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.insert(1, 'chicken')
>>> spam
['cat', 'chicken', 'dog', 'bat'] 

注意代码没有执行任何赋值操作,例如 spam = spam.append('moose')spam = spam.insert(1, 'chicken')append()insert() 的返回值是 None,所以你绝对不希望将其存储为新变量的值。相反,这些方法会就地修改列表,这在“可变和不可变数据类型”一节(第 126 页)中有更详细的介绍。

方法属于单一的数据类型。append()insert() 方法是列表方法,我们只能在列表值上调用它们,而不能在其他数据类型(如字符串或整数)的值上调用。要查看尝试这样做会发生什么,请在交互式外壳中输入以下内容:

>>> eggs = 'hello'
>>> eggs.append('world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs.append('world')
AttributeError: 'str' object has no attribute 'append'
>>> bacon = 42
>>> bacon.insert(1, 'world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    bacon.insert(1, 'world')
AttributeError: 'int' object has no attribute 'insert' 

注意显示的 AttributeError 错误信息。

删除值

remove() 方法接受一个要从中删除的值:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('bat')
>>> spam
['cat', 'rat', 'elephant'] 

尝试删除列表中不存在的值将导致 ValueError 错误。例如,请在交互式外壳中输入以下内容并注意显示的错误:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('chicken')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.remove('chicken')
ValueError: list.remove(x): x not in list 

如果值在列表中多次出现,该方法将仅删除它的第一个实例:

>>> spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat']
>>> spam.remove('cat')
>>> spam
['bat', 'rat', 'cat', 'hat', 'cat'] 

当你知道要从列表中删除的值的索引时,del 语句很有用,而当你知道要删除的值本身时,remove() 方法很有用。

排序值

你可以使用 sort() 方法对数字值的列表或字符串值的列表进行排序。例如,请在交互式外壳中输入以下内容:

>>> spam = [2, 5, 3.14, 1, -7]
>>> spam.sort()
>>> spam
[-7, 1, 2, 3.14, 5]
>>> spam = ['Ants', 'Cats', 'Dogs', 'Badgers', 'Elephants']
>>> spam.sort()
>>> spam
['Ants', 'Badgers', 'Cats', 'Dogs', 'Elephants'] 

该方法以数值顺序返回数字,以字母顺序返回字符串。您还可以传递 True 作为 reverse 关键字参数以逆序排序值:

>>> spam.sort(reverse=True)
>>> spam
['Elephants', 'Dogs', 'Cats', 'Badgers', 'Ants'] 

注意 sort() 方法的三个要点。首先,它就地排序列表;不要尝试通过编写像 spam = spam.sort() 这样的代码来捕获返回值。

第二,你不能对包含数字值和字符串值的列表进行排序,因为 Python 不知道如何比较这些值。请在交互式外壳中输入以下内容并注意显示的 TypeError 错误:

>>> spam = [1, 3, 2, 4, 'Alice', 'Bob']
>>> spam.sort()
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.sort()
TypeError: '<' not supported between instances of 'str' and 'int' 

第三,sort() 使用 ASCII 顺序 而不是实际的字母顺序来排序字符串。这意味着大写字母在小写字母之前,将小写字母 a 放在大写字母 Z 之后。例如,请在交互式外壳中输入以下内容:

>>> spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats']
>>> spam.sort()
>>> spam
['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats'] 

如果你需要按常规字母顺序排序值,请在 sort() 方法调用中传递 str.lower 作为 key 关键字参数:

>>> spam = ['a', 'z', 'A', 'Z']
>>> spam.sort(key=str.lower)
>>> spam
['a', 'A', 'z', 'Z'] 

这个参数会导致 sort() 函数将列表中的所有项都视为小写,而实际上并不改变列表中的值。

反转值

如果你需要快速反转列表中项的顺序,你可以调用 reverse() 列表方法。请在交互式外壳中输入以下内容:

>>> spam = ['cat', 'dog', 'moose']
>>> spam.reverse()
>>> spam
['moose', 'dog', 'cat'] 

sort() 列表方法一样,reverse() 也不返回列表,这就是为什么我们写 spam.reverse() 而不是 spam = spam.reverse()

查找值

列表值有一个index()方法,可以传递一个值。如果该值存在于列表中,则该方法将返回该值的索引。如果该值不在列表中,则 Python 会产生一个ValueError错误。将以下内容输入到交互式外壳中:

>>> spam = ['hello', 'hi', 'howdy', 'heyas']
>>> spam.index('hello')
0
>>> spam.index('heyas')
3
>>> spam.index('howdy howdy howdy')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.index('howdy howdy howdy')
ValueError: 'howdy howdy howdy' is not in list 

当列表包含值的重复项时,该方法返回其首次出现的位置:

>>> spam = ['Zophie', 'Pooka', 'Fat-tail', 'Pooka']
>>> spam.index('Pooka')
1 

注意index()返回1,而不是3

添加值

要向列表中添加新值,请使用append()insert()方法。append()方法将参数添加到列表的末尾:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.append('moose')
>>> spam
['cat', 'dog', 'bat', 'moose'] 

insert()方法可以在列表中的任何索引处插入值。insert()的第一个参数是新值的索引,第二个参数是要插入的新值。将以下内容输入到交互式外壳中:

>>> spam = ['cat', 'dog', 'bat']
>>> spam.insert(1, 'chicken')
>>> spam
['cat', 'chicken', 'dog', 'bat'] 

注意代码没有执行任何赋值操作,例如spam = spam.append('moose')spam = spam.insert(1, 'chicken')append()insert()的返回值是None,所以您绝对不希望将其存储为新变量的值。相反,这些方法会就地修改列表,这在“可变和不可变数据类型”一节(第 126 页)中进行了更详细的介绍。

方法属于单一的数据类型。append()insert()方法是列表方法,我们只能在列表值上调用它们,而不能在其他数据类型(如字符串或整数)的值上调用。要查看尝试这样做会发生什么,将以下内容输入到交互式外壳中:

>>> eggs = 'hello'
>>> eggs.append('world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs.append('world')
AttributeError: 'str' object has no attribute 'append'
>>> bacon = 42
>>> bacon.insert(1, 'world')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    bacon.insert(1, 'world')
AttributeError: 'int' object has no attribute 'insert' 

注意显示的AttributeError错误消息。

移除值

remove()方法接受一个要从中移除的值的列表:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('bat')
>>> spam
['cat', 'rat', 'elephant'] 

尝试删除列表中不存在的值将导致ValueError错误。例如,将以下内容输入到交互式外壳中,并注意它显示的错误:

>>> spam = ['cat', 'bat', 'rat', 'elephant']
>>> spam.remove('chicken')
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.remove('chicken')
ValueError: list.remove(x): x not in list 

如果值在列表中多次出现,该方法将仅移除它的第一个实例:

>>> spam = ['cat', 'bat', 'rat', 'cat', 'hat', 'cat']
>>> spam.remove('cat')
>>> spam
['bat', 'rat', 'cat', 'hat', 'cat'] 

当您知道要从列表中移除的值的索引时,del语句很有用,而remove()方法在您知道值本身时很有用。

排序值

您可以使用sort()方法对数字值的列表或字符串值的列表进行排序。例如,将以下内容输入到交互式外壳中:

>>> spam = [2, 5, 3.14, 1, -7]
>>> spam.sort()
>>> spam
[-7, 1, 2, 3.14, 5]
>>> spam = ['Ants', 'Cats', 'Dogs', 'Badgers', 'Elephants']
>>> spam.sort()
>>> spam
['Ants', 'Badgers', 'Cats', 'Dogs', 'Elephants'] 

该方法以数值顺序返回数字,以字母顺序返回字符串。您还可以传递True作为reverse关键字参数以按反向顺序排序值:

>>> spam.sort(reverse=True)
>>> spam
['Elephants', 'Dogs', 'Cats', 'Badgers', 'Ants'] 

注意关于sort()方法的三个要点。首先,它会在原地对列表进行排序;不要尝试通过编写像spam = spam.sort()这样的代码来捕获返回值。

其次,您不能对包含数字值和字符串值的列表进行排序,因为 Python 不知道如何比较这些值。将以下内容输入到交互式外壳中,并注意显示的TypeError错误:

>>> spam = [1, 3, 2, 4, 'Alice', 'Bob']
>>> spam.sort()
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam.sort()
TypeError: '<' not supported between instances of 'str' and 'int' 

第三,sort()使用ASCII 顺序而不是实际的字母顺序进行字符串排序。这意味着大写字母排在小写字母之前,将小写字母a放在大写字母Z之后。例如,在交互式 shell 中输入以下内容:

>>> spam = ['Alice', 'ants', 'Bob', 'badgers', 'Carol', 'cats']
>>> spam.sort()
>>> spam
['Alice', 'Bob', 'Carol', 'ants', 'badgers', 'cats'] 

如果你需要按常规字母顺序排序值,请在sort()方法调用中传递str.lower作为key关键字参数:

>>> spam = ['a', 'z', 'A', 'Z']
>>> spam.sort(key=str.lower)
>>> spam
['a', 'A', 'z', 'Z'] 

这个参数导致sort()函数将列表中的所有项都视为小写,而实际上并不改变列表中的值。

反转值

如果你需要快速反转列表中项的顺序,你可以调用reverse()列表方法。在交互式 shell 中输入以下内容:

>>> spam = ['cat', 'dog', 'moose']
>>> spam.reverse()
>>> spam
['moose', 'dog', 'cat'] 

sort()列表方法类似,reverse()不会返回列表,这就是为什么我们写spam.reverse()而不是spam = spam.reverse()

短路布尔运算符

布尔运算符有一个微妙的行为,很容易忽略。回想一下,如果由and运算符组合的两个值中的任何一个为False,整个表达式就是False,如果由or运算符组合的两个值中的任何一个为True,整个表达式就是True。如果给你表达式False and spam,无论spam变量的值是True还是False,整个表达式都会是False。同样,对于True or spam;无论spam的值如何,它都会评估为True

Python(以及许多其他编程语言)利用这个事实来优化代码,使其运行得更快,因为它根本不会检查布尔运算符的右侧。这个捷径被称为短路。大多数时候,你的程序的行为将与 Python 检查整个表达式时的行为相同(尽管快几微秒)。然而,考虑这个简短的程序,其中我们检查列表中的第一个项是否为'cat'

spam = ['cat', 'dog']
if spam[0] == 'cat':
    print('A cat is the first item.')
else:
    print('The first item is not a cat.') 

如此编写,此程序会打印A cat is the first item.。但如果spam列表为空,spam[0]代码将导致IndexError: list Index out of range错误。为了修复这个问题,我们将调整if语句的条件以利用短路:

spam = []
if len(spam) > 0 and spam[0] == 'cat':
    print('A cat is the first item.')
else:
    print('The first item is not a cat.') 

此程序永远不会出错,因为如果len(spam) > 0False(即spam列表为空),那么短路and运算符意味着 Python 不会运行会导致IndexError错误的spam[0] == 'cat'代码。当你编写涉及andor运算符的代码时,请记住这种短路行为。

简短程序:带有列表的魔法 8 球

使用列表,你可以编写一个更优雅的版本的第四章的magic8Ball.py程序。而不是几行几乎相同的elif语句,你可以创建一个代码与之工作的单个列表。打开一个新的文件编辑窗口,并输入以下代码。将其保存为magic8Ball2.py

import random

messages = ['It is certain',
    'It is decidedly so',
    'Yes definitely',
    'Reply hazy try again',
    'Ask again later',
    'Concentrate and ask again',
    'My reply is no',
    'Outlook not so good',
    'Very doubtful']

print('Ask a yes or no question:')
input('>')
print(messages[random.randint(0, len(messages) - 1)]) 

当你运行它时,你会看到它与之前的 magic8Ball.py 程序工作方式相同。

random.randint(0, len(messages) - 1) 调用生成一个随机数作为索引,无论 messages 的大小如何。也就是说,你会得到一个介于 0len(messages) - 1 之间的随机数。这种方法的好处是你可以轻松地向 messages 列表中添加和删除字符串,而无需更改其他代码行。如果你稍后更新你的代码,你需要更改的行会更少,从而减少了你引入错误的机会。

从列表中随机选择一个项目是如此常见,以至于 Python 有 random.choice(messages) 函数,它执行与 random.randint(0, len(messages) – 1) 相同的操作。

序列数据类型

列表并不是唯一表示有序值序列的数据类型。例如,如果你将字符串视为单个文本字符的“列表”,那么字符串和列表实际上是相似的。Python 的序列数据类型包括列表、字符串、range() 函数返回的 range 对象以及元组(在“元组数据类型”一节中解释,见第 127 页)。你可以用列表做的许多事情也可以用字符串和其他序列类型值来做。为了看到这一点,请在交互式外壳中输入以下内容:

>>> name = 'Zophie'
>>> name[0]
'Z'
>>> name[-2]
'i'
>>> name[0:4]
'Zoph'
>>> 'Zo' in name
True
>>> 'z' in name
False
>>> 'p' not in name
False
>>> for i in name:
...     print('* * * ' + i + ' * * *')
...
* * * Z * * *
* * * o * * *
* * * p * * *
* * * h * * *
* * * i * * *
* * * e * * * 

你可以用序列值做所有与列表相同的事情:索引、切片、for 循环、len()in 以及 not in 操作符。

可变和不可变数据类型

但是列表和字符串在重要方面有所不同。列表值是一个可变的数据类型:你可以添加、删除或更改其值。然而,字符串是不可变的:它不能被更改。尝试重新分配字符串中的单个字符会导致 TypeError 错误,正如你在交互式外壳中输入以下内容可以看到的那样:

>>> name = 'Zophie a cat'
>>> name[7] = 'the'
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    name[7] = 'the'
TypeError: 'str' object does not support item assignment 

正确的“变异”字符串的方法是使用切片和连接来构建一个新的字符串,通过从旧字符串的部分复制:

>>> name = 'Zophie a cat'
>>> new_name = name[0:7] + 'the' + name[8:12]
>>> name
'Zophie a cat'
>>> new_name
'Zophie the cat' 

我们使用了 [0:7][8:12] 来引用我们不想替换的字符。请注意,原始的 'Zophie a cat' 字符串没有被修改,因为字符串是不可变的。

虽然列表值确实是可变的,但以下代码的第二行并没有修改列表 eggs

>>> eggs = ['A', 'B', 'C']
>>> eggs = ['x', 'y', 'z']
>>> eggs
['x', 'y', 'z'] 

在这里,eggs 中的列表值并没有被改变;相反,一个新的完全不同的列表值(['x', 'y', 'z'])正在替换旧的列表值(['A', 'B', 'C'])。

如果你想要实际修改 eggs 中的原始列表以包含 ['x', 'y', 'z'],你必须使用 del 语句和 append() 方法,如下所示:

>>> eggs = ['A', 'B', 'C']
>>> del eggs[2]
>>> del eggs[1]
>>> del eggs[0]
>>> eggs.append('x')
>>> eggs.append('y')
>>> eggs.append('z')
>>> eggs
['x', 'y', 'z'] 

在这个例子中,eggs 变量最终具有与它开始时相同的列表值。只是这个列表已经被改变(变异)而不是被覆盖。我们称这种“就地改变列表”的行为。

可变与不可变类型可能看起来是一个无意义的区别,但“第 129 页的‘引用’”将解释在调用具有可变参数和不可变参数的函数时的不同行为。首先,让我们了解元组数据类型,它是列表数据类型的一种不可变形式。

元组数据类型

元组数据类型和列表数据类型之间只有两个区别。第一个区别是,你使用圆括号而不是方括号来编写元组。例如,将以下内容输入到交互式外壳中:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[0]
'hello'
>>> eggs[1:3]
(42, 0.5)
>>> len(eggs)
3 

元组与列表的不同之处在于,元组,就像字符串一样,是不可变的:你不能修改、追加或删除它们的值。将以下内容输入到交互式外壳中,并查看产生的 TypeError 错误信息:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[1] = 99
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs[1] = 99
TypeError: 'tuple' object does not support item assignment 

如果你的元组中只有一个值,你可以在括号内的值后面放置一个尾随逗号来表示这一点。否则,Python 会认为你输入了常规括号内的值。(与一些其他编程语言不同,在 Python 中,在列表或元组的最后一个项目后面放置尾随逗号是可以的。)将以下 type() 函数调用输入到交互式外壳中,以查看区别:

>>> type(('hello',))
<class 'tuple'>
>>> type(('hello'))
<class 'str'> 

你可以使用元组来向阅读你代码的人传达,你不想让这些值序列发生变化。如果你需要一个永远不会变化的有序值序列,请使用元组。使用元组而不是列表的第二个好处是,由于它们是不可变的,其内容不会改变,Python 可以实现优化,使得使用元组的代码比使用列表的代码稍微快一些。

列表和元组类型转换

就像 str(42) 会返回 '42',这是整数 42 的字符串表示一样,list()tuple() 函数会返回传递给它们的值的列表和元组版本。将以下内容输入到交互式外壳中,并注意返回值的数据类型与传递的值不同:

>>> tuple(['cat', 'dog', 5])
('cat', 'dog', 5)
>>> list(('cat', 'dog', 5))
['cat', 'dog', 5]
>>> list('hello')
['h', 'e', 'l', 'l', 'o'] 

将元组转换为列表在需要元组值的可变版本时很有用。

可变和不可变数据类型

但列表和字符串在重要方面有所不同。列表值是一种可变数据类型:你可以添加、删除或更改其值。然而,字符串是不可变的:它不能被更改。尝试重新分配字符串中的单个字符会导致 TypeError 错误,就像你可以在交互式外壳中看到的那样:

>>> name = 'Zophie a cat'
>>> name[7] = 'the'
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    name[7] = 'the'
TypeError: 'str' object does not support item assignment 

正确“修改”字符串的方法是使用切片和连接来通过从旧字符串的部分复制来构建一个新的字符串:

>>> name = 'Zophie a cat'
>>> new_name = name[0:7] + 'the' + name[8:12]
>>> name
'Zophie a cat'
>>> new_name
'Zophie the cat' 

我们使用了 [0:7][8:12] 来引用我们不想替换的字符。请注意,原始的 'Zophie a cat' 字符串没有被修改,因为字符串是不可变的。

虽然列表值是可变的,但以下代码的第二行并没有修改列表 eggs

>>> eggs = ['A', 'B', 'C']
>>> eggs = ['x', 'y', 'z']
>>> eggs
['x', 'y', 'z'] 

在这里,eggs 中的列表值并没有被更改;相反,一个新的完全不同的列表值(['x', 'y', 'z'])正在替换旧的列表值(['A', 'B', 'C'])。

如果您想实际修改 eggs 中的原始列表以包含 ['x', 'y', 'z'],您必须使用 del 语句和 append() 方法,如下所示:

>>> eggs = ['A', 'B', 'C']
>>> del eggs[2]
>>> del eggs[1]
>>> del eggs[0]
>>> eggs.append('x')
>>> eggs.append('y')
>>> eggs.append('z')
>>> eggs
['x', 'y', 'z'] 

在这个例子中,eggs 变量最终具有与它开始时相同的列表值。只是这个列表已经被更改(变异)而不是被覆盖。我们称这种操作为就地更改列表

可变类型与不可变类型的区别可能看似没有意义,但“参考文献”第 129 页将解释在调用具有可变参数与不可变参数的函数时的不同行为。首先,让我们了解一下元组数据类型,它是列表数据类型的不可变形式。

元组数据类型

元组数据类型与列表数据类型之间只有两个区别。第一个区别是您使用括号而不是方括号来编写元组。例如,将以下内容输入到交互式外壳中:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[0]
'hello'
>>> eggs[1:3]
(42, 0.5)
>>> len(eggs)
3 

元组与列表的第二种和主要区别是,元组,就像字符串一样,是不可变的:您不能修改、追加或删除它们的值。将以下内容输入到交互式外壳中,并查看产生的 TypeError 错误信息:

>>> eggs = ('hello', 42, 0.5)
>>> eggs[1] = 99
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    eggs[1] = 99
TypeError: 'tuple' object does not support item assignment 

如果您的元组中只有一个值,您可以通过在括号内的值后放置一个尾随逗号来表示这一点。否则,Python 会认为您在常规括号内输入了一个值。(与其他一些编程语言不同,在 Python 中,在列表或元组的最后一个项目后放置尾随逗号是可以的。)将以下 type() 函数调用输入到交互式外壳中,以查看区别:

>>> type(('hello',))
<class 'tuple'>
>>> type(('hello'))
<class 'str'> 

您可以使用元组来向阅读您代码的人传达您不打算更改该值序列的意图。如果您需要一个永远不会更改的有序值序列,请使用元组。使用元组而不是列表的第二个好处是,由于它们是不可变的,且其内容不会改变,Python 可以实现优化,使得使用元组的代码比使用列表的代码稍微快一些。

列表和元组类型转换

正如 str(42) 将返回 '42',这是整数 42 的字符串表示一样,函数 list()tuple() 将返回传递给它们的值列表和元组版本。将以下内容输入到交互式外壳中,并注意返回值的数据类型与传递的值不同:

>>> tuple(['cat', 'dog', 5])
('cat', 'dog', 5)
>>> list(('cat', 'dog', 5))
['cat', 'dog', 5]
>>> list('hello')
['h', 'e', 'l', 'l', 'o'] 

如果您需要将元组值转换为可变版本,转换元组到列表是很有用的。

参考文献

一个常见的隐喻是变量是像字符串和整数一样“存储”值的盒子。然而,这个解释是对 Python 实际所做事情的简化。一个更好的隐喻是变量是附有字符串的纸标签,这些标签附加到值上。将以下内容输入到交互式外壳中:

>>> spam = 42 # ❶
>>> eggs = spam # ❷
>>> spam = 99 # ❸
>>> spam
99
>>> eggs
42 

当你将42赋值给spam变量时,你实际上是在计算机内存中创建了一个42值,并将对该值的引用存储在spam变量中。当你复制spam中的值并将其赋值给变量eggs时,你是在复制引用。spameggs变量都引用计算机内存中的42值。使用变量的标签隐喻,你已经将spam标签和eggs标签附加到了同一个42值上。当你将spam赋值为新的99值时,你改变了spam标签所引用的内容。图 6-2 是代码的图形表示。

三个带有标签的 Python 赋值语句表示的值

图 6-2:变量赋值不会重写值;它改变的是引用。描述

这个变化不会影响eggs,它仍然引用42值。

但列表并不这样工作,因为列表的值可以改变;也就是说,列表是可变的。下面是一段代码,可以帮助你更容易地理解这种区别。将其输入到交互式 shell 中:

>>> spam = [0, 1, 2, 3] # ❶
>>> eggs = spam  # The reference, not the list, is being copied. # ❷
>>> eggs[1] = 'Hello!'  # This changes the list value. # ❸
>>> spam
[0, 'Hello!', 2, 3]
>>> eggs  # The eggs variable refers to the same list.
[0, 'Hello!', 2, 3] 

这段代码可能看起来有些奇怪。它只修改了eggs列表,但eggsspam列表似乎都发生了变化。

当你创建列表❶时,你将对其的引用赋值给spam变量。但下一行只复制了spam中的列表引用到eggs❷,而不是列表本身。仍然只有一个列表,spameggs现在都引用它。只有一个底层列表的原因是列表本身从未被实际复制。所以,当你修改eggs的第一个元素❸时,你是在修改spam所引用的同一个列表。你可以在图 6-3 中看到这一点。

三个带有标签的 Python 赋值语句表示的值

图 6-3:因为spameggs引用的是同一个列表,所以修改一个会影响到另一个。描述

这会变得稍微复杂一些,因为列表也不直接包含值的序列,而是包含对值的引用的序列。我在第 131 页的“copy()deepcopy()函数”中进一步解释了这一点。

虽然 Python 变量在技术上包含对值的引用,但人们常常随意地说变量包含值。但请记住这两条规则:

  • 在 Python 中,变量从不包含值。它们只包含对值的引用。

  • 在 Python 中,=赋值运算符只复制引用,它永远不会复制值。

大部分情况下,你不需要知道这些细节,但有时,这些简单的规则会产生意想不到的效果,你应该确切地了解 Python 正在做什么。

参数

引用对于理解参数如何传递给函数特别重要。当调用函数时,Python 会将参数的引用复制到参数变量中。对于列表(以及我在第七章中将要描述的字典)这样的可变值,这意味着函数中的代码会就地修改原始值。为了看到这一事实的后果,打开一个新的文件编辑窗口,输入以下代码,并将其保存为 passingReference.py

def eggs(some_parameter):
    some_parameter.append('Hello')

spam = [1, 2, 3]
eggs(spam)
print(spam)  # Prints [1, 2, 3, 'Hello'] 

注意,当你调用 eggs() 时,返回值不会将新值赋给 spam。相反,它直接就地修改列表。当运行此程序时,它输出 [1, 2, 3, 'Hello']

即使 spamsome_parameter 包含不同的引用,它们都指向同一个列表。这就是为什么在函数内部调用 append('Hello') 方法会影响列表,即使在函数调用返回之后也是如此。

记住这种行为。忘记 Python 以这种方式处理列表和字典变量可能会导致意外的行为和令人困惑的错误。

copy()deepcopy() 函数

虽然传递引用通常是处理列表和字典最方便的方式,但如果函数修改了传递给它的列表或字典,你可能不希望这些更改出现在原始列表或字典值中。为了控制这种行为,Python 提供了一个名为 copy 的模块,该模块提供了 copy()deepcopy() 函数。其中第一个,copy.copy(),可以复制可变值(如列表或字典)的副本,而不仅仅是引用的副本。将以下内容输入到交互式外壳中:

>>> import copy
>>> spam = ['A', 'B', 'C']
>>> cheese = copy.copy(spam) # Creates a duplicate copy of the list
>>> cheese[1] = 42  # Changes cheese
>>> spam  # The spam variable is unchanged.
['A', 'B', 'C']
>>> cheese  # The cheese variable is changed.
['A', 42, 'C'] 

现在 spamcheese 变量指向不同的列表,这就是为什么当你将 42 赋值到索引 1 时,只有 cheese 中的列表被修改。

正如变量 引用 值而不是包含值一样,列表包含 引用 到值而不是值本身。你可以在图 6-4 中看到这一点。

在标签“spam”上方标记的列表“[‘A,’ ‘B’, ‘C’]”的 X。标签“0”上方标记的值“A”,标签“1”标记的值“B”,以及标签“2”标记的值“C”。所有三个标签本身都指向标签“spam”

图 6-4:列表不直接包含值(左);它们包含值的引用(右)。

如果你需要复制的列表包含列表,请使用 copy.deepcopy() 函数而不是 copy.copy()copy.deepcopy() 函数将复制这些内部列表。

参数

引用对于理解参数如何传递给函数特别重要。当函数被调用时,Python 会将参数的引用复制到参数变量中。对于像列表(以及我在第七章中将要描述的字典)这样的可变值,这意味着函数中的代码会就地修改原始值。为了看到这一事实的后果,打开一个新的文件编辑器窗口,输入以下代码,并将其保存为passingReference.py

def eggs(some_parameter):
    some_parameter.append('Hello')

spam = [1, 2, 3]
eggs(spam)
print(spam)  # Prints [1, 2, 3, 'Hello'] 

注意,当你调用eggs()时,返回值不会将新值赋给spam。相反,它直接修改了列表。当运行此程序时,它输出[1, 2, 3, 'Hello']

尽管spamsome_parameter包含不同的引用,但它们都引用了同一个列表。这就是为什么在函数内部调用append('Hello')方法会影响列表,即使在函数调用返回之后也是如此。

请记住这种行为。忘记 Python 以这种方式处理列表和字典变量可能会导致意外的行为和令人困惑的错误。

copy()和 deepcopy()函数

虽然传递引用通常是处理列表和字典的最方便方式,但如果函数修改了传递给它的列表或字典,您可能不希望在原始列表或字典值中看到这些更改。为了控制这种行为,Python 提供了一个名为copy的模块,该模块提供了copy()deepcopy()函数。其中第一个,copy.copy(),可以复制可变值(如列表或字典)的副本,而不仅仅是引用的副本。在交互式 shell 中输入以下内容:

>>> import copy
>>> spam = ['A', 'B', 'C']
>>> cheese = copy.copy(spam) # Creates a duplicate copy of the list
>>> cheese[1] = 42  # Changes cheese
>>> spam  # The spam variable is unchanged.
['A', 'B', 'C']
>>> cheese  # The cheese variable is changed.
['A', 42, 'C'] 

现在,spamcheese变量引用的是不同的列表,这就是为什么当你将42赋值到索引1时,只有cheese中的列表被修改。

正如变量引用值而不是包含值一样,列表包含对值的引用而不是值本身。您可以在图 6-4 中看到这一点。

在标签“spam”上方有一个 X,该标签分配给列表“[‘A,’ ‘B’, ‘C’]”。在标签“0”上方有一个勾选标记,该标签分配给值“A”,标签“1”分配给值“B”,标签“2”分配给值“C”。所有三个标签本身都分配给了标签“spam”

图 6-4:列表不直接包含值(左);它们包含对值的引用(右)。

如果您需要复制的列表包含列表,请使用copy.deepcopy()函数而不是copy.copy()copy.deepcopy()函数将复制这些内部列表。

短程序:矩阵屏幕保护程序

在黑客科幻电影《黑客帝国》中,电脑显示器上显示着流动的绿色数字流,就像数字雨从玻璃窗上倾泻而下。这些数字可能没有意义,但看起来很酷。为了好玩,我们可以用 Python 创建自己的矩阵屏幕保护程序。将以下代码输入到一个新文件中,并将其保存为matrixscreensaver.py

import random, sys, time

WIDTH = 70  # The number of columns

try:
    # For each column, when the counter is 0, no stream is shown.
    # Otherwise, it acts as a counter for how many times a 1 or 0
    # should be displayed in that column.
    columns = [0] * WIDTH
    while True:
        # Loop over each column:
        for i in range(WIDTH):
            if random.random() < 0.02:
                # Restart a stream counter on this column.
                # The stream length is between 4 and 14 characters long.
                columns[i] = random.randint(4, 14)

            # Print a character in this column:
            if columns[i] == 0:
                # Change this ' '' to '.' to see the empty spaces:
                print(' ', end='')
            else:
                # Print a 0 or 1:
                print(random.choice([0, 1]), end='')
                columns[i] -= 1  # Decrement the counter for this column.
        print()  # Print a newline at the end of the row of columns.
        time.sleep(0.1)  # Each row pauses for one tenth of a second.
except KeyboardInterrupt:
    sys.exit()  # When Ctrl-C is pressed, end the program. 

当你运行这个程序时,它会产生如图 6-5 所示的二进制 1 和 0 的流。

一个显示随机放置的 1 和 0 字符的各种长度的 Windows 命令提示符窗口

图 6-5:矩阵屏幕保护程序

与之前的尖峰和锯齿程序类似,这个程序通过在一个无限循环中打印文本行来创建滚动动画,该循环在用户按下 CTRL-C 时停止。这个程序中的主要数据结构是 columns 列表,它包含 70 个整数,每个整数对应输出窗口的一列。当 columns 中的整数是 0 时,它为该列打印一个空格。当它大于 0 时,它会随机打印一个 01,然后递减该整数。一旦整数减少到 0,该列再次打印一个空格。程序随机设置 columns 中的整数为介于 414 之间的整数,以产生随机的二进制 0 和 1 流。

让我们来看看程序的每个部分:

import random, sys, time

WIDTH = 70  # The number of columns 

我们导入 random 模块以使用其 choice()randint() 函数,导入 sys 模块以使用其 exit() 函数,以及导入 time 模块以使用其 sleep() 函数。我们还设置了一个名为 WIDTH 的变量,其值为 70,这样程序就可以为 70 列字符生成输出。你可以根据运行程序时窗口的大小将此值更改为更大的或更小的整数。

WIDTH 变量使用全大写名称,因为它是一个常量变量。一个 常量 是一个一旦设置后代码不应该更改的变量。使用常量可以使你编写更易读的代码,例如 columns = [0] * WIDTH 而不是 columns = [0] * 70,这样当你稍后再次阅读代码时,可能会想知道 70 是什么意思。在 Python 中,没有东西阻止你更改常量的值,但大写名称可以提醒程序员不要这样做。

程序的大部分内容都发生在 try 块内部,该块捕获用户按下 CTRL-C 以引发 KeyboardInterrupt 异常的情况:

try:
    # For each column, when the counter is 0, no stream is shown.
    # Otherwise, it acts as a counter for how many times a 1 or 0
    # should be displayed in that column.
    columns = [0] * WIDTH 

columns 变量包含一个由 0 组成的整数列表。这个列表中的整数数量等于 WIDTH。这些整数中的每一个控制输出窗口的列是否打印一串二进制数:

 while True:
        # Loop over each column:
        for i in range(WIDTH):
            if random.random() < 0.02:
                # Restart a stream counter on this column.
                # The stream length is between 4 and 14 characters long.
                columns[i] = random.randint(4, 14) 

我们希望这个程序永远运行,所以我们将它全部放在一个无限 while True: 循环中。在这个循环内部有一个 for 循环,它遍历单行中的每一列。循环变量 i 代表列的索引;它从 0 开始,到但不包括 WIDTHcolumns[0] 中的值表示应该打印在最左边的列中,columns[1] 对左数第二列执行此操作,依此类推。

对于每一列,有 2%的概率将columns[i]中的整数设置为介于414之间的数字。我们通过比较random.random()(一个返回介于0.01.0之间的随机浮点数的函数)和0.02来计算这个概率。如果你想使流更密集或更稀疏,可以相应地增加或减少这个数字。我们将每一列的计数器整数设置为介于 4 和 14 之间的随机数:

 # Print a character in this column:
            if columns[i] == 0:
                # Change this ' '' to '.' to see the empty spaces:
                print(' ', end='')
            else:
                # Print a 0 or 1:
                print(random.choice([0, 1]), end='')
                columns[i] -= 1  # Decrement the counter for this column. 

for循环内部,程序还确定是否应该打印一个随机的01二进制数或一个空格。如果columns[i]0,则打印一个空格。否则,它将列表[0, 1]传递给random.choice()函数,该函数返回列表中的一个随机值以打印。代码还会递减columns[i]的计数器,使其更接近0,并且不再打印二进制数。

如果你想看到程序打印的“空”空间,尝试将' '字符串更改为'.'并再次运行程序。输出应该看起来像这样:

............................1.........................................
................0...........1......................1..................
................1...........0................1.....0..................
............1...0...........0.....0..........1.....0..................
............1.1.1...........0.....0..........1.....1..1...............
............0.0.0...........0.....1.........00.....1..1............... 

else块结束后,for循环块也结束了:

 print()  # Print a newline at the end of the row of columns.
        time.sleep(0.1)  # Each row pauses for one tenth of a second.
except KeyboardInterrupt:
    sys.exit()  # When Ctrl-C is pressed, end the program. 

for循环之后的print()调用会打印一个换行符,因为之前的print()调用在每一列传递时都使用了end=''关键字参数来防止在每一列后打印换行符。对于打印的每一行,程序通过调用time.sleep(0.1)引入了十分之一秒的暂停。

程序的最后部分是一个except块,如果用户按下 CTRL-C 来引发KeyboardInterrupt异常,则退出程序。

摘要

列表是有用的数据类型,因为它们允许你在单个变量中编写针对可修改数量的值的代码。在这本书的后面部分,你会看到使用列表执行那些否则可能很难或不可能完成的任务的程序。

列表是一种可变的数据类型,这意味着其内容可以更改。元组和字符串虽然是序列数据类型,但它们是不可变的,不能更改。我们可以用新的元组或字符串值覆盖包含元组或字符串值的变量,这并不等同于修改现有值——比如列表上的append()remove()方法所做的。

变量不会直接存储列表值;它们存储列表的引用。当你复制变量或在函数调用中传递列表作为参数时,这是一个重要的区别。因为正在复制的值是列表引用,所以请注意,你对列表所做的任何更改可能会影响程序中的另一个变量。如果你想在一个变量中对列表进行更改而不修改原始列表,可以使用copy()deepcopy()

实践问题

1.  []是什么?

2.  如果你要将值'hello'赋给名为spam的变量中存储的列表的第三个值?(假设spam包含[2, 4, 6, 8, 10]。)

对于以下三个问题,假设spam包含列表['a', 'b', 'c', 'd']

3.  spam[int(int('3' * 2) // 11)]的值是多少?

  1. spam[-1]评估结果是什么?

  2. spam[:2]评估结果是什么?

对于以下三个问题,假设bacon包含列表[3.14, 'cat', 11, 'cat', True]

  1. bacon.index('cat')评估结果是什么?

  2. bacon.append(99)会使bacon中的列表值看起来如何?

  3. bacon.remove('cat')会使bacon中的列表值看起来如何?

  4. 列表连接和列表复制的运算符是什么?

  5. append()insert()列表方法之间的区别是什么?

  6. 从列表中移除值有两种方法是什么?

  7. 列表值与字符串值有哪些相似之处?

  8. 列表和元组之间有什么区别?

  9. 如何编写只包含整数值42的元组值?

  10. 如何获取列表值的元组形式?如何获取元组值的列表形式?

  11. 变量“包含”列表值实际上并不直接包含列表。它们包含什么?

  12. copy.copy()copy.deepcopy()之间的区别是什么?

练习程序

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

逗号代码

假设你有一个这样的列表值:

spam = ['apples', 'bananas', 'tofu', 'cats']

编写一个函数,该函数接受一个列表值作为参数,并返回一个字符串,其中所有项目由逗号和空格分隔,并在最后一个项目之前插入and。例如,将之前的spam列表传递给函数将返回'apples, bananas, tofu, and cats'。但你的函数应该能够处理传递给它的任何列表值。确保测试将空列表[]传递给函数的情况。

硬币翻转连续序列

对于这个练习,我们将尝试进行一个实验。如果你抛硬币 100 次,每次正面写一个*H*,每次反面写一个*T*,你会创建一个看起来像*T T T T H H H H T T*的列表。如果你要求一个人做出 100 次随机抛硬币,你可能会得到交替的正面-反面结果,如*H T H T H H T H T T*——这看起来是随机的(对人类来说),但实际上并不是数学上的随机。人类几乎永远不会写下连续六个正面或六个反面的序列,尽管在真正的随机抛硬币中这种情况很可能发生。人类在随机性方面是可预测地糟糕的。

编写一个程序来找出在随机生成的 100 个正面和反面列表中,连续六个正面或连续六个反面的出现频率。你的程序应该将实验分为两部分:第一部分生成一个包含 100 个随机选择的'H''T'值的列表,第二部分检查其中是否存在连续的序列。将所有这些代码放入一个循环中,重复实验 10,000 次,以便找出有多少百分比的钱币翻转包含连续六个正面或六个反面。作为一个提示,函数调用random.randint(0, 1)在 50%的时间内返回0值,在另外 50%的时间内返回1值。

你可以从以下模板开始:

import random
number_of_streaks = 0
for experiment_number in range(10000):  # Run 100,000 experiments total.
    # Code that creates a list of 100 'heads' or 'tails' values

    # Code that checks if there is a streak of 6 heads or tails in a row

print('Chance of streak: %s%%' % (number_of_streaks / 100)) 

当然,这只是一个估计,但 10000 是一个相当大的样本量。一些数学知识可以给你一个确切的答案,并节省你编写程序的时间,但程序员在数学上通常很糟糕。

要创建一个列表,使用一个for循环,将随机选择的'H''T'追加到列表中 100 次。为了确定是否存在连续六个正面或六个反面,创建一个类似于some_list[i:i+6]的切片(它包含从索引i开始的六个项目)并将其与列表值['H', 'H', 'H', 'H', 'H', 'H']['T', 'T', 'T', 'T', 'T', 'T']进行比较。

逗号代码

假设你有一个类似这样的列表值:

spam = ['apples', 'bananas', 'tofu', 'cats']

编写一个函数,该函数接受一个列表值作为参数,并返回一个字符串,其中所有项目由逗号和空格分隔,并在最后一个项目之前插入*and*。例如,将之前的spam列表传递给函数将返回'apples, bananas, tofu, and cats'。但你的函数应该能够处理传递给它的任何列表值。务必测试将空列表[]传递给函数的情况。

硬币翻转连续序列

对于这个练习,我们将尝试进行一个实验。如果你抛硬币 100 次,每次正面写一个H,每次反面写一个T,你会创建一个看起来像T T T T H H H H T T的列表。如果你要求一个人做出 100 次随机抛硬币的结果,你可能会得到交替的正面-反面结果,如H T H T H H T H T T——这看起来是随机的(对人类来说),但实际上并不是数学上的随机。人类几乎永远不会写下连续六个正面或六个反面,尽管在真正的随机抛硬币中这种情况很可能会发生。人类在随机性方面是可预测地糟糕的。

编写一个程序来找出在随机生成的 100 个正面和反面的列表中,连续六个正面或连续六个反面的出现频率。你的程序应该将实验分为两部分:第一部分生成一个包含 100 个随机选择的'H''T'值的列表,第二部分检查其中是否存在连续序列。将所有这些代码放入一个循环中,重复实验 10000 次,以便你可以找出有多少百分比的钱币翻转包含连续六个正面或六个反面。作为一个提示,函数调用random.randint(0, 1)将 50%的时间返回0值,另一半时间返回1值。

你可以从以下模板开始:

import random
number_of_streaks = 0
for experiment_number in range(10000):  # Run 100,000 experiments total.
    # Code that creates a list of 100 'heads' or 'tails' values

    # Code that checks if there is a streak of 6 heads or tails in a row

print('Chance of streak: %s%%' % (number_of_streaks / 100)) 

当然,这只是一个估计,但 10000 是一个相当大的样本量。一些数学知识可以给你一个确切的答案,并节省你编写程序的时间,但程序员在数学上通常很糟糕。

要创建一个列表,使用一个for循环将随机选择的'H''T'追加到列表中 100 次。为了确定是否存在连续六个正面或六个反面,创建一个类似于some_list[i:i + 6]的切片(它包含从索引i开始的六个项目)然后将其与列表值['H', 'H', 'H', 'H', 'H', 'H']['T', 'T', 'T', 'T', 'T', 'T']进行比较。

posted @ 2026-02-06 10:27  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报