《Python编程:从入门到实践》 第 10 章 文件和异常

第 10 章 文件和异常

这章会学习如何处理文件,让程序可以去分析大量的数据;学习错误处理,避免程序在意外情况下崩溃;学习 Python 创建的特殊对象:异常;还会学习到 json 模块,用它来保存用户数据,避免程序停止运行后丢失。

在本章学习到的技巧可提高程序的适用性、可用性和稳定性。

10.1 从文件中读取数据

会读取文件中的数据是很重要的。要使用文本文件中的信息,要先将信息读取到内存中。你可以一次全部读取,也可以每次一行的逐行读取。

10.1.1 读取整个文件

使用 reda() 方法会读取整个文件的内容:

1
2
3
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)

关键字 with 在不再需要访问文件后将其关闭。这种方式很优雅,你只管打开文件,并在需要时使用它,Python 自会在合适的时候自动将其关闭。不建议手动调用 close() ,因为若有异常在合适的时候去关闭文件将是件很难的事情。

10.1.2 文件路径

在常见系统中使用相对路径

1
2
3
4
# Linux 和 OS X 是斜杆 /
with open('text_files/filename.txt') as file_object:
# Windows 是反斜杠 \
with open('text_files\filename.txt') as file_object:

在常见系统中使用绝对路径

1
2
3
4
5
6
# Linux 和 OS X 是斜杆 /
file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:
# Windows 是反斜杠 \
file_path = 'C:\Users\ehmatthes\other_files\text_files\filename.txt'
with open(file_path) as file_object:

注意:Windows 有的时候是可以正确处理文件路径中的斜杠。若结果不符合预期,请确保在路径中使用的是反斜杠。

10.1.3 逐行读取

使用循环的方式即可逐行读取:

1
2
3
4
5
filename = 'pi_digits.txt'

with open(filename) as file_object:
for line in file_object:
print(line.rstrip())

注意:print 语句也会加上一个换行符

10.1.4 创建一个包含文件各行内容的列表

使用 with 时,open() 返回的文件对象只在 with 代码块内可用。下面示例在 with 代码块中将各行储存在个列表中,并在 with 代码块外使用该列表:

1
2
3
4
5
6
7
filename = 'pi_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

for line in lines:
print(line.rstrip())

lines 的作用域,在外面也可以用。

10.1.5 使用文件的内容

创建个字符串 pi_string,用循环把列表中的各行都加入到 pi_string 中,并删除两边的空格和换行符:

1
2
3
4
5
6
7
8
9
10
11
filename = 'pi_30_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

pi_string = ''
for line in lines:
pi_string += line.strip()

print(pi_string)
print(len(pi_string))

注意:Python 读取都文本都为字符串。如果读取的为数字,并要作为数字使用,就必须使用函数 int() 将其转换为整数,或使用函数 float() 将其转换为浮点数。

10.1.6 包含一百万位的大型文件

读取个小数点后1000000位的文件,输入小数点后50位,与这个圆周率的长度:

1
2
3
4
5
6
7
8
9
10
filename = 'pi_million_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip()

print(pi_string[:52] + "...")
print(len(pi_string))

Python 对处理的数据量没有任何限制,只要系统的内存足够多的,想处理多少都可以。

10.1.7 圆周率值中包含你的生日吗

在圆周率的前1000000位中查找输入的日生是否在其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
filename = 'pi_million_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

pi_string = ''
for line in lines:
pi_string += line.rstrip()
birthday = input("Enter your birthday, in the form mmddyy: ")

if birthday in pi_string:
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appear in the first million digits of pi.")

10.2 写入文件

保存数据最简单的方式之一就是将其写入到文件。

10.2.1 写入空文件

要将文本写入文件需要在调用 open() 时提供另一个实参,告诉 Python 要写入打开的文件。下面来将一条简单的消息存储到文件中:

1
2
3
4
filename = 'programming.txt'

with open(filename, 'w') as file_object:
file_object.write("I love programming.")

调用 open() 的第一个实参是打开文件的名称;第二个实参 'w' 告诉 Python 以写入模式打开文件。打开文件时可指定读取模式 'r'写入模式 'w'附加模式 'a'读写模式 'r+'。如果无模式实参,Python 默认将以只读模式打开文件。

如果要写的入文件不存在,open() 将自动创建它。然而,以写入模式 'w' 打开文件时要千万小心,因为如果文件已经存在, Python 将在返回文件对象前清空该文件。

注意:Python 只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数 str() 将其转换为字符串格式。

10.2.2 写入多行

函数 write() 不会在你写入的文本末尾添加换行符,要让每个字符串都单独占一行,需要在 write() 语句中包含换行符:

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'w') as file_object:
file_object.write("I love programming.\n")
file_object.write("I love creating new games.\n")

10.2.3 附加到文件

要给文件添加内容,而不是覆盖原有的内容,可用附加模式打开文件。这种模式会把要写入到文件的行都将添加到文件已有行的末尾。如果文件不存在将创建个新的文件。下面示例在之前的文件中又加入了两行:

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'a') as file_object:
file_object.write("I also love finding meaning in large datasets.\n")
file_object.write("I love creating apps that can run in a browser.\n")

10.3 异常

Python 使用被称为异常的特殊对象来管理执行期间发生的错误。每当发生让 Python 不知所措的错误时,它都会创建个异常对象。如果写了异常处理代码,程序将继续运行,否则程序将停止并显示个 traceback ,其中包含有关异常的报告。

异常使用 try-except 代码块处理。try-except 代码块让 Python 执行指定的操作,同时告诉 Python 发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的错误消息,而不是 traceback

10.3.1 处理 ZeroDivisionError 异常

不能将一个数字除以0,但还是让 Python 这样做吧:

1
print(5/0)

显然这样做是不对的,因此将看到个 traceback

1
2
3
4
Traceback (most recent call last):
File "division.py", line 1, in <module>
print(5/0)
ZeroDivisionError: division by zero

10.3.2 使用 try-except 代码块

可编写一个 try-except 代码块来处理可能发生的异常,让 Python 知道发生了指定异常时该怎么办。
处理 ZeroDivisionError 异常的 try-except 类似这样:

1
2
3
4
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")

发生 ZeroDivisionError 异常后,会运行 except 中的代码,显示一段友好的信息,并继续运行。

10.3.3 使用异常避免崩溃

下面是个简单的除法计算器:

1
2
3
4
5
6
7
8
9
10
11
12
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
if second_number == 'q':
break
answer = int(first_number) / int(second_number)
print(answer)

如果用户输入的除数为 0 ,程序将崩溃:

1
2
3
4
5
6
7
8
9
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
Traceback (most recent call last):
File "division.py", line 9, in <module>
answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero

不应该让用户看到 traceback ,这会对普通用户产生困扰,恶意用户或攻击这会通过 traceback 获取到不希望别看到的信息,甚至可分析出对你的代码做出什么样的攻击。

10.3.4 else 代码块

依赖与 try 代码块成功执行的代码都应放到 else 代码块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")

while True:
first_number = input("\nFirst number: ")
if first_number == 'q':
break
second_number = input("Second number: ")
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)

如果 try 中地代码执行过程中出现了 ZeroDivisionError 异常,则程序会运行 except 中的代码,否则运行 else 中的代码。

通过预测错误,可使程序更健壮,即便出现了无效的数据或资源,也可以继续运行,从而抵御无意的用户错误和恶意的攻击。

10.3.5 处理 FileNotFoundError 异常

在使用文件时常见的问题就是找不到文件,如:文件路径不对,文件名不对或文件根本不存在。下面尝试读取一个不存在的文件:

1
2
3
4
5
6
7
8
filename = 'alice.txt' 

try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)

10.3.6 分析文本

方法 split() 以空格为分隔符拆分字符串,并储存在一个列表中。下面计算 Alice in Wonderland 包含多少个单词, 对整篇小说调用 split() 方法返回的列表中元素的个数就是这篇童话大致包含多少个单词:

1
2
3
4
5
6
7
8
9
10
11
12
13
filename = 'alice.txt'

try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + "

10.3.7 使用多个文件

siddhartha.txt 没有在目录中,

1
2
3
4
5
6
def count_words(filename):
--snip--

filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)

文件siddhartha.txt不存在,但这丝毫不影响这个程序处理其他文件:

1
2
3
4
The file alice.txt has about 29461 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.

10.3.8 失败时一声不吭

Python 有一个 pass 语句,可在代码块中使用它来让 Python 什么都不要做:

1
2
3
4
5
6
7
8
9
10
11
12
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
--snip--
except FileNotFoundError:
pass
else:
--snip--

filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)

pass 语句还充当了占位符,它提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。

10.3.9 决定报告哪些错误

只要程序依赖于外部因素,就有可能出现异常。如:用户输入、操作指定的文件、网络连接,这些地方都应有非正常情况的处理。

10.4 存储数据

模块 json 让你能够将简单的 Python 数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。

10.4.1 使用 json.dump()json.load()

函数 json.dump() 接受两个实参:要存储的数据以及可用于存储数据的文件对象。下面演示了如何使用json.dump() 来存储数字列表:

1
2
3
4
5
6
7
import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'
with open(filename, 'w') as f_obj:
json.dump(numbers, f_obj)

使用 json.load() 将这个列表读取到内存中:

1
2
3
4
5
6
7
import json

filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)

print(numbers)

10.4.2 保存和读取用户生成的数据

下面是对 json 模块读写数据的实际应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json

# 如果以前存储了用户名,就加载它
# 否则,就提示用户输入用户名并存储它
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")

10.4.3 重构

代码能够正确地运行,但可做进一步的改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构。重构让代码更清晰、更易于理解、更容易扩展。

下面是重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json

def get_stored_username():
"""如果存储了用户名,就获取它"""
--snip--

def get_new_username():
"""提示用户输入用户名"""
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
return username

def greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username()
if username:
print("Welcome back, " + username + "!")
else:
username = get_new_username()
print("We'll remember you when you come back, " + username + "!")

greet_user()

10.5 小结

  • 如何读取文件;
  • 如何逐行读取文件;
  • 如果写入文件;
  • 如何把文本附加到文件末尾;
  • 什么是异常;
  • 如何处理可能引发的异常;
  • 如何储存 Python 数据结构。