Python for Everybody 中文版

Chapter 0 Base Material

| 关于   «  7. PY4E - Python 为所有人   ::   目录   ::   9. PY4E - Python for Everybody  »

8. PY4E - Python for Everybody

切换导航

PY4E

第 1 章:简介 第 2 章:变量 第 3 章:条件语句 第 4 章:函数 第 5 章:迭代 第 6 章:字符串 第 7 章:文件 第 8 章:列表 第 9 章:字典 第 10 章:元组 第 11 章:正则表达式 第 12 章:网络程序 第 13 章:Python 与 Web 服务 第 14 章:Python 对象 第 15 章:Python 与数据库 第 16 章:数据可视化

8.1. 内存

8.1.1. 持久性

到目前为止,我们已经学习了如何使用条件执行、函数和迭代编写程序,并将我们的意图传达给 中央处理器 。我们已经学习了如何在 主存 中创建和使用数据结构。中央处理器和内存是我们的软件运行和工作的地方。所有“思考”都发生在这里。

但如果您回想我们在硬件架构讨论中的内容,一旦电源关闭,存储在 CPU 或内存中的任何内容都会被擦除。因此,到目前为止,我们的程序仅仅是一时的趣味练习,用于学习 Python。

Secondary Memory

内存

在本章中,我们将开始处理 内存 (或文件)。内存不会在断电时被擦除。或者以 USB 闪存盘为例,我们从程序中写入的数据可以从系统中移除并传输到另一个系统。

我们将主要关注读写文本文件,例如我们在文本编辑器中创建的那些文件。稍后我们将看到如何与数据库文件一起工作,这些文件是二进制文件,专门设计为通过数据库软件进行读写。

8.1.2. 打开文件

当我们想要读取或写入一个文件(例如在您的硬盘上)时,首先必须 打开 该文件。打开文件会与您的操作系统通信,而操作系统知道每个文件的数据存储位置。当您打开一个文件时,您是在要求操作系统通过文件名找到该文件并确保文件存在。在此示例中,我们打开文件 mbox.txt ,该文件应存储在您启动 Python 时所在的同一文件夹中。您可以从 www.py4e.com/code3/mbox.txt 下载此文件。

>>> fhand = open('mbox.txt')
>>> print(fhand)
<_io.TextIOWrapper name='mbox.txt' mode='r' encoding='cp1252'>

如果 open 成功,操作系统会返回给我们一个 文件句柄 。文件句柄并非文件中实际包含的数据,而是一个我们可以用来读取数据的“句柄”。如果请求的文件存在且您拥有读取该文件的适当权限,您将获得一个句柄。

A File Handle

一个文件句柄

如果文件不存在, open 将抛出异常追踪信息,您将无法获得句柄以访问文件内容:

>>> fhand = open('stuff.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'stuff.txt'

稍后我们将使用 try 和 except 更优雅地处理尝试打开不存在的文件的情况。

8.1.3. 文本文件与行

文本文件可以被视为行的序列,就像 Python 字符串可以被视为字符的序列一样。例如,这是一个文本文件的样本,记录了开源项目开发团队中各个人员的邮件活动:

From stephen.marquard@uct.ac.za Sat Jan  5 09:14:16 2008
Return-Path: <postmaster@collab.sakaiproject.org>
Date: Sat, 5 Jan 2008 09:12:18 -0500
To: source@collab.sakaiproject.org
From: stephen.marquard@uct.ac.za
Subject: [sakai] svn commit: r39772 - content/branches/
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
...

整个邮件交互文件可从

www.py4e.com/code3/mbox.txt

并且该文件的缩短版本可从

www.py4e.com/code3/mbox-short.txt

这些文件采用了一种标准格式,用于包含多个邮件消息。以“From”开头的行用于分隔消息,而以“From:”开头的行则是消息的一部分。有关 mbox 格式的更多信息,请参见 https://en.wikipedia.org/wiki/Mbox。

要将文件拆分为行,有一个表示“行结束”的特殊字符,称为换行符。

在 Python 中,我们在字符串常量中表示 换行 字符为反斜杠 -n。尽管这看起来像两个字符,但实际上它是一个单个字符。当我们通过输入"stuff"在解释器中查看该变量时,它显示字符串中的 \n ,但当我们使用 print 来显示字符串时,我们看到字符串被换行字符拆分为两行。

>>> stuff = 'Hello\nWorld!'
>>> stuff
'Hello\nWorld!'
>>> print(stuff)
Hello
World!
>>> stuff = 'X\nY'
>>> print(stuff)
X
Y
>>> len(stuff)
3

您之前翻译的条目中,占位符 X\nY 未解析,且存在术语不一致问题。根据 OpenDSA 术语表,"Secondary Memory" 应译为“内存”,但本句涉及的是字符串长度与换行符,此处无直接术语冲突,重点在于保留占位符原样并修正格式。以下是修正后的译文,严格遵循所有规则:

因此,当我们在文件中查看行时,需要 想象 每行末尾有一个特殊的不可见字符(称为换行符),它标记了行的结束。

因此,换行符将文件中的字符分隔为行。

8.1.4. 读取文件

虽然 文件句柄 本身并不包含文件的数据,但很容易构造一个 for 循环来读取并计算文件中的每一行:

fhand = open('mbox-short.txt')
count = 0
for line in fhand:
    count = count + 1
print('Line Count:', count)

# Code: http://www.py4e.com/code3/open.py

我们可以将文件句柄用作 for 循环中的序列。我们的 for 循环只是计算文件中的行数并将其打印出来。 for 循环的粗略英文翻译是:“对于由文件句柄表示的文件的每一行,将 count 变量加一。”

open 函数之所以不会读取整个文件,是因为文件可能非常大,包含数 GB 的数据。 open 语句所花费的时间与文件大小无关。 for 循环实际上会触发从文件中读取数据。

当以这种方式使用 for 循环读取文件时,Python 会自动使用换行符将文件中的数据拆分为单独的行。Python 通过换行符读取每一行,并将换行符作为每次 for 循环迭代中 line 变量的最后一个字符包含在内。

由于循环 for 逐行读取数据,因此它可以高效地读取并统计超大文件中的行数,而无需耗尽主存来存储数据。上述程序可以使用极少的内存统计任意大小文件中的行数,因为每一行在读取、计数后随即被丢弃。

如果你知道文件相对于主存的大小相对较小,你可以使用文件句柄上的 read 方法将整个文件读入一个字符串。

>>> fhand = open('mbox-short.txt')
>>> inp = fhand.read()
>>> print(len(inp))
94626
>>> print(inp[:20])
From stephen.marquar

在此示例中,文件 mbox-short.txt 的全部内容(共 94,626 个字符)被直接读取到变量 inp 中。我们使用字符串切片来打印存储在 inp 中的字符串数据的前 20 个字符。

当以这种方式读取文件时,所有字符(包括所有行和换行符)都作为一个大字符串存储在变量 inp 中。将 read 的输出存储为变量是一个好主意,因为每次调用 read 都会耗尽资源:

>>> fhand = open('mbox-short.txt')
>>> print(len(fhand.read()))
94626
>>> print(len(fhand.read()))
0

记得,这种形式的 open 函数仅应在文件数据能舒适地容纳于您计算机的主存时使用。如果文件过大无法装入主存,您应编写程序以块的方式读取文件,并使用 for 或 while 循环。

8.1.5. 在文件中搜索

当您在文件中搜索数据时,读取整个文件并忽略大多数行,仅处理满足特定条件的行,是一种非常常见的模式。我们可以将读取文件的模式与字符串方法相结合,以构建简单的搜索机制。

例如,如果我们想读取一个文件并仅打印以前缀 "From:" 开头的行,我们可以使用字符串方法 startswith 来选择仅包含所需前缀的那些行:

fhand = open('mbox-short.txt')
for line in fhand:
    if line.startswith('From:'):
        print(line)

# Code: http://www.py4e.com/code3/search1.py

当此程序运行时,我们得到以下输出:

From: stephen.marquard@uct.ac.za

From: louis@media.berkeley.edu

From: zqian@umich.edu

From: rjlowe@iupui.edu
...

之前的输出看起来很棒,因为我们看到的唯一行是那些以 "From:" 开头的行,但我们为什么还会看到额外的空行呢?这是由于不可见的 换行 字符造成的。每一行都以换行符结尾,因此 print 语句打印变量 line 中的字符串(其中包含一个换行符),然后 print 又添加 另一个 换行符,从而产生了我们看到的双倍空行效果。

我们可以使用切片来打印除最后一个字符外的所有字符,但更简单的方法是使用 rstrip 方法,该方法从字符串右侧去除空白字符,如下所示:

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    if line.startswith('From:'):
        print(line)

# Code: http://www.py4e.com/code3/search2.py

当此程序运行时,我们得到以下输出:

From: stephen.marquard@uct.ac.za
From: louis@media.berkeley.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
From: cwen@iupui.edu
...

随着您的文件处理程序变得越来越复杂,您可能希望使用 continue 来构建搜索循环。搜索循环的基本思想是,您正在寻找“有趣”的行,并有效地跳过“无趣”的行。然后,当我们找到一行有趣的行时,我们对这一行执行某些操作。

我们可以将循环结构化为跳过不感兴趣的行的模式,如下所示:

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    # Skip 'uninteresting lines'
    if not line.startswith('From:'):
        continue
    # Process our 'interesting' line
    print(line)

# Code: http://www.py4e.com/code3/search3.py

之前的程序输出结果相同。在英文中,不感兴趣的行是指那些不以 "From:" 开头的行,我们使用 continue 跳过这些行。对于“感兴趣”的行(即那些以 "From:" 开头的行),我们执行处理。

我们可以使用 find 字符串方法来模拟文本编辑器中的搜索功能,该功能查找搜索字符串出现在行中的任何位置。由于 find 用于在另一个字符串中查找字符串的出现位置,并返回该字符串的位置,或者在字符串未找到时返回 -1,因此我们可以编写以下循环来显示包含字符串 "@uct.ac.za"(即来自南非开普敦大学)的行:

fhand = open('mbox-short.txt')
for line in fhand:
    line = line.rstrip()
    if line.find('@uct.ac.za') == -1: continue
    print(line)

# Code: http://www.py4e.com/code3/search4.py

产生以下输出:

From stephen.marquard@uct.ac.za Sat Jan  5 09:14:16 2008
X-Authentication-Warning: set sender to stephen.marquard@uct.ac.za using -f
From: stephen.marquard@uct.ac.za
Author: stephen.marquard@uct.ac.za
From david.horwitz@uct.ac.za Fri Jan  4 07:02:32 2008
X-Authentication-Warning: set sender to david.horwitz@uct.ac.za using -f
From: david.horwitz@uct.ac.za
Author: david.horwitz@uct.ac.za
...

在此,我们也使用了 if 语句的缩写形式,将 continue 与 if 放在同一行。这种 if 的缩写形式的作用与将 continue 放在下一行并缩进时相同。

8.1.6. 让用户选择文件名

我们确实不希望每次想要处理不同的文件时都要编辑我们的 Python 代码。每次程序运行时让用户输入文件名字符串会更易用,这样他们就可以在不修改 Python 代码的情况下用我们的程序处理不同的文件。

这可以通过使用 input 从用户读取文件名来轻松完成,如下所示:

fname = input('Enter the file name: ')
fhand = open(fname)
count = 0
for line in fhand:
    if line.startswith('Subject:'):
        count = count + 1
print('There were', count, 'subject lines in', fname)

# Code: http://www.py4e.com/code3/search6.py

我们从用户处读取文件名并将其放入名为 fname 的变量中,然后打开该文件。现在我们可以针对不同的文件重复运行该程序。

python search6.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt

python search6.py
Enter the file name: mbox-short.txt
There were 27 subject lines in mbox-short.txt

在查看下一节之前,先看看上面的程序,问问自己:“这里可能出什么问题?”或者“我们的友好用户可能会做什么,导致我们的小程序不优雅地退出并抛出跟踪回溯,让我们在用户眼中看起来不那么酷?”

8.1.7. 使用 try, except, 和 open

我告诉过你不要偷看。这是你最后的机会。

如果我们的用户输入的不是一个文件名会怎样?

python search6.py
Enter the file name: missing.txt
Traceback (most recent call last):
  File "search6.py", line 2, in <module>
    fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'

python search6.py
Enter the file name: na na boo boo
Traceback (most recent call last):
  File "search6.py", line 2, in <module>
    fhand = open(fname)
FileNotFoundError: [Errno 2] No such file or directory: 'na na boo boo'

不要发笑。用户最终会做任何可能的事情来破坏你的程序,无论是出于错误还是恶意。事实上,任何软件开发团队的一个重要组成部分是一个人或一组被称为 质量保证 (或简称 QA)的人,他们的工作就是尽可能做最疯狂的事情,试图破坏程序员创建的软件。

QA 团队负责在我们向可能购买软件或支付薪水让我们编写软件的最终用户交付程序之前,发现程序中的缺陷。因此,QA 团队是程序员的最佳朋友。

之前,既然我们已经看到了程序中的缺陷,就可以利用 try/except 结构优雅地修复它。我们需要假设 open 调用可能会失败,并在 open 失败时添加恢复代码,如下所示:

fname = input('Enter the file name: ')
try:
    fhand = open(fname)
except:
    print('File cannot be opened:', fname)
    exit()
count = 0
for line in fhand:
    if line.startswith('Subject:'):
        count = count + 1
print('There were', count, 'subject lines in', fname)

# Code: http://www.py4e.com/code3/search7.py

exit 函数终止程序。它是一个我们调用后永不返回的函数。现在,当我们的用户(或 QA 团队)输入 silliness 或错误的文件名时,我们会“捕获”这些情况并优雅地恢复:

python search7.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt

python search7.py
Enter the file name: na na boo boo
File cannot be opened: na na boo boo

保护对 open 调用的使用是 Python 程序中正确使用 try 和 except 的一个好例子。当我们以“Python 的方式”做事时,我们使用术语"Pythonic"。我们可能会说上面的例子是以 Pythonic 的方式打开文件。

一旦您在 Python 方面更加熟练,就可以与其他 Python 程序员进行辩论,以决定针对某个问题的两个等效解决方案中哪个是“更具 Python 风格”的。目标是“更具 Python 风格”体现了编程是部分工程、部分艺术的理念。我们不仅仅满足于让某物运行,还希望我们的解决方案优雅,并被同行视为优雅。

8.1.8. 写入文件

要写入文件,必须将其以模式 "w" 作为第二个参数打开:

>>> fout = open('output.txt', 'w')
>>> print(fout)
<_io.TextIOWrapper name='output.txt' mode='w' encoding='cp1252'>

如果文件已存在,则以写入模式打开它会清除旧数据并从头开始,因此请小心!如果文件不存在,则会创建一个新文件。

文件句柄对象的 write 方法将数据写入文件,返回写入的字符数。默认的写入模式为文本模式,用于写入(和读取)字符串。

>>> line1 = "This here's the wattle,\n"
>>> fout.write(line1)
24

再次,文件对象会记录其当前位置,因此如果你再次调用 write ,它会将新数据追加到末尾。

我们必须确保在写入文件时管理行尾,即在需要结束行时显式插入换行符。 print 语句会自动追加换行符,但 write 方法不会自动添加换行符。

>>> line2 = 'the emblem of our land.\n'
>>> fout.write(line2)
24

当你完成写入后,必须关闭文件,以确保最后的数据被物理写入磁盘,这样即使断电也不会丢失。

>>> fout.close()

我们可以关闭那些为读取而打开的文件,但如果只是打开少量文件,我们可以稍微随意一些,因为 Python 会确保所有打开的文件在程序结束时被关闭。当我们写入文件时,我们希望显式地关闭文件,以免将一切留给运气。

8.1.9. 调试

当你读写文件时,可能会遇到与空白字符相关的问题。这些错误很难调试,因为空格、制表符和换行符通常是不可见的:

>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2  3
 4

内置函数 repr 可派上用场。它接受任意对象作为参数,并返回该对象的字符串表示形式。对于字符串,它使用反斜杠序列表示空白字符:

>>> print(repr(s))
'1 2\t 3\n 4'

这有助于调试。

之前可能遇到的另一个问题是,不同的系统使用不同的字符来表示行的结束。一些系统使用换行符,表示为 \n 。另一些系统使用回车字符,表示为 \r 。有些系统两者都用。如果您在不同系统之间移动文件,这些不一致可能会导致问题。

对于大多数系统,都存在用于在不同格式之间进行转换的应用程序。您可以在 https://www.wikipedia.org/wiki/Newline 处找到它们(并阅读有关此问题的更多信息)。或者,当然,您也可以自己编写一个。

8.1.10. 术语表

catch

使用 try 和 except 语句防止异常终止程序。

newline

文件中或字符串中用于指示行尾的特殊字符。

Pythonic

一种在 Python 中优雅运行的技术。“使用 try 和 except 是从缺失文件中恢复的 Pythonic 方式”。

Quality Assurance

专注于确保软件产品整体质量的个人或团队。QA 通常参与在产品发布前测试产品并识别问题。

text file

存储在硬盘等永久存储设备中的字符序列。

8.1.11. 练习

练习 1:编写一个程序来读取文件并按行打印文件内容,全部转换为大写。运行该程序的效果如下:

python shout.py
Enter a file name: mbox-short.txt
FROM STEPHEN.MARQUARD@UCT.AC.ZA SAT JAN  5 09:14:16 2008
RETURN-PATH: <POSTMASTER@COLLAB.SAKAIPROJECT.ORG>
RECEIVED: FROM MURDER (MAIL.UMICH.EDU [141.211.14.90])
     BY FRANKENSTEIN.MAIL.UMICH.EDU (CYRUS V2.3.8) WITH LMTPA;
     SAT, 05 JAN 2008 09:14:16 -0500

您可以从 www.py4e.com/code3/mbox-short.txt 下载该文件

练习 2:编写一个程序,提示输入文件名,然后读取文件并查找形如以下形式的行:

X-DSPAM-Confidence: 0.8475

当你遇到一行以 "X-DSPAM-Confidence:" 开头的行时,解析该行以提取行中的浮点数。统计这些行,然后计算这些行的垃圾邮件置信度值的总和。当到达文件末尾时,打印垃圾邮件置信度的平均值。

Enter the file name: mbox.txt
Average spam confidence: 0.894128046745

Enter the file name: mbox-short.txt
Average spam confidence: 0.750718518519

在 *mbox.txt* 和 *mbox-short.txt* 文件上测试您的文件。

练习 3:有时程序员感到无聊或想寻开心时,会在其程序中添加无害的 *复活节彩蛋* 。修改提示用户输入文件名的程序,使其在用户输入确切文件名“na na boo boo”时打印一条搞笑消息。对于所有其他存在或不存在的文件,程序应正常行为。以下是该程序的示例执行:

python egg.py
Enter the file name: mbox.txt
There were 1797 subject lines in mbox.txt

python egg.py
Enter the file name: missing.tyxt
File cannot be opened: missing.tyxt

python egg.py
Enter the file name: na na boo boo
NA NA BOO BOO TO YOU - You have been punk'd!

我们并不鼓励你在程序中放入复活节彩蛋;这只是一个练习。


如果您发现本书中有错误,欢迎使用 Github 发送修正建议给我。

   «  7. PY4E - Python 为所有人   ::   目录   ::   9. PY4E - Python for Everybody  »

关闭窗口