《Python编程:从入门到实战》(第2版)学习笔记 第10章 文件和异常(下)

本文介绍了Python中如何处理异常,如ZeroDivisionError和FileNotFoundError,并展示了如何使用try-except代码块来确保程序的健壮性。此外,文章还讲解了如何使用文件进行读写操作,以及如何利用json模块存储和加载数据,以便在程序运行中持久化用户信息。
摘要由CSDN通过智能技术生成

【写在前面】为进一步提高自己的python代码能力,打算把几本经典书籍重新过一遍,形成系统的知识体系,同时适当记录一些学习笔记,我尽量及时更新!先从经典的《Python编程:从入门到实战》书籍开始吧。有问题欢迎在评论区讨论,互相学习,good good study,day day up!上一章《Python编程:从入门到实战》(第2版)学习笔记 第9章 类介绍了类,这章介绍文件和异常【分为上和下两篇】

【特别说明】这是第二版的《Python编程:从入门到实战》,书本的Python版本是3.7.2,我自己运行代码的环境是3.7.0,不同Python版本的功能不同。 

接上一篇文章 《Python编程:从入门到实战》(第2版)学习笔记 第10章 文件和异常(上)

10.3 异常

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

异常是使用try-except 代码块处理的,它执行指定的操作,同时指出发生异常时怎么办。这样即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

10.3.1 处理ZeroDivisionError 异常

下面来看一种导致Python引发异常的简单错误。

division_calculator.py
>>> print(5/0)

显然,Python无法这样除以0,你将看到一个traceback:

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

traceback指出的错误ZeroDivisionError 是个异常对象。这时Python将停止运行程序,并指出哪种异常,而我们可根据这些信息对程序进行修改。

10.3.2 使用try-except 代码块

当你认为可能发生错误时,可编写一个try-except代码块来处理可能引发的异常:

>>> try:
>>>         print(5/0)
>>> except ZeroDivisionError:
>>>         print("You can't divide by zero!")

将导致错误的代码行print(5/0) 放在一个try 代码块中,如果代码运行起来没有问题,Python将跳过except 代码块;如果导致了错误,将查找与之匹配的except代码块并运行其中的代码。

在本例中,try 代码块中的代码引发了ZeroDivisionError 异常,因此Python查找了except 代码块,并运行其中的代码。这样,用户看到了一条友好的错误消息,而不是traceback:

You can't divide by zero!

如果try-except 代码块后面还有其他代码,程序将接着运行。

10.3.3 使用异常避免崩溃

发生错误时,如果程序还有工作尚未完成,妥善地处理错误就尤其重要。这种情况经常会出现在要求用户输入的程序中;如果程序能够妥善地处理无效输入,就不至于崩溃。下面来创建一个只执行除法运算的简单计算器:

division_calculator.py

>>> 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时,它将崩溃:

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_calculator.py", line 9, in <module>
        answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero

程序崩溃不好,但让用户看到traceback也不是个好主意。不懂技术的用户会被搞糊涂,怀有恶意的用户还会通过traceback获悉你不想他知道的信息。例如,他将知道程序文件的名称,还将看到部分不能正确运行的代码。有时训练有素的攻击者可根据这些信息判断出可对你的代码发起什么样的攻击。

10.3.4 else 代码块

通过将可能引发错误的代码放在try-except 代码块中,可提高程序抵御错误的能力。这个示例还包含一个else 代码块。依赖try 代码块成功执行的代码都应放到else 代码块中:

>>> --snip--
>>> while True:
>>>         --snip--
>>>         if second_number == 'q':
>>>                 break
>>>         try:
>>>                 answer = int(first_number) / int(second_number)
>>>         except ZeroDivisionError:
>>>                 print("You can't divide by 0!")
>>>         else:
>>>                 print(answer)

如果除法运算成功,就使用else 代码块来打印结果,用户根本看不到traceback:

Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q

try-except-else 代码块的工作原理:Python尝试执行try 代码块中的代码(可能引发异常的代码),仅在try 代码块成功执行时才需要运行的代码,应放在else 代码块中。except 代码块指出如果运行try 中的代码引发了指定的异常该怎么办。通过预测可能发生错误的代码,可编写健壮的程序。

10.3.5 处理FileNotFoundError 异常

使用文件时可能找不到文件:可能查找的文件在其他地方,文件名不正确或者文件根本就不存在。这些情形都可使用try-except 代码块处理。

下面读取一个不存在的文件:

alice.py

>>> filename = 'alice.txt'
>>> with open(filename, encoding='utf-8') as f:
>>>         contents = f.read()

使用变量f 来表示文件对象(常见做法);给参数encoding 指定了值,在系统的默认编码与要读取文件使用的编码不一致时,必须这样做。

Python无法读取不存在的文件,报错:

Traceback (most recent call last):
    File "alice.py", line 3, in <module>
        with open(filename, encoding='utf-8') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'

上述traceback的最后一行报告了FileNotFoundError 异常,这是Python找不到要打开的文件时创建的异常。这个错误是函数open() 导致的,因此必须将try 语句放在包含open() 的代码行之前:

>>> filename = 'alice.txt'
>>> try:
>>>         with open(filename, encoding='utf-8') as f:
>>>                 contents = f.read()
>>> except FileNotFoundError:
>>>         print(f"Sorry, the file {filename} does not exist.")

最终的结果是显示一条友好的错误消息:

Sorry, the file alice.txt does not exist.

10.3.6 分析文本

你可以分析包含整本书的文本文件。本节使用的文本来自古登堡计划,该计划提供了一系列不受版权限制的文学作品。如果你要在编程项目中使用文学文本,这是很不错的资源。

下面提取童话《爱丽丝漫游奇境记》(Alice in Wonderland )的文本,并尝试计算它包含多少个单词。我们使用方法split() ,它能根据一个字符串创建一个单词列表。示例:

>>> title = "Alice in Wonderland"

>>> title.split()

['Alice', 'in', 'Wonderland']

方法split() 以空格为分隔符将字符串分拆成多个部分,然后存储到列表中,得到一个包含字符串中所有单词的列表,虽然有些单词可能包含标点。为计算整篇小说包含多少个单词,我将对其调用split() ,再计算得到的列表包含多少个元素,从而确定大致包含多少单词:

>>> filename = 'alice.txt'
>>> try:
>>>         with open(filename, encoding='utf-8') as f:
>>>                 contents = f.read()  #一个长长的字符串
>>> except FileNotFoundError:
>>>         print(f"Sorry, the file {filename} does not exist.")
>>> else:
>>>         # 计算该文件大致包含多少个单词
>>>         words = contents.split()
>>>         num_words = len(words)
>>>         print(f"The file {filename} has about {num_words} words.")

输出:

The file alice.txt has about 29465 words.

这个数稍大一点,因为使用的文本文件包含出版商给的额外信息,但还是可以估算《爱丽丝漫游奇境记》的篇幅。

10.3.7 使用多个文件

下面多分析几本书。首先定义一个名为count_words() 的函数:

word_count.py

>>> def count_words(filename):
>>>         """计算一个文件大致包含多少单词"""
>>>         try:
>>>                 with open(filename, encoding='utf-8') as f:
>>>                         contents = f.read()
>>>         except FileNotFoundError:
>>>                 print(f"Sorry, the file {filename} does not exist.")
>>>         else:
>>>                 words = contents.split()
>>>                 num_words = len(words)
>>>                 print(f"The file {filename} has about {num_words} words.")
>>> filename = 'alice.txt'
>>> count_words(filename)

现在可以编写一个简单的循环,计算要分析的任何文本包含多少个单词了。将要分析的文件的名称存储在一个列表中,然后对列表中的每个文件调用count_words() 。将尝试计算《爱丽丝漫游奇境记》《悉达多》(Siddhartha )、《白鲸》(Moby Dick )和《小妇人》(Little Women )分别包含多少个单词。注意作者没有将siddhartha.txt放到word_count.py所在的目录中:

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

文件siddhartha.txt不存在,但这丝毫不影响该程序运行:

The file alice.txt has about 29465 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.

这里使用try-except 代码块有两个重要的优点:(1)避免用户看到traceback;(2)让程序继续分析能够找到的其他文件。如果不捕获因找不到siddhartha.txt而引发的FileNotFoundError 异常,用户将看到完整的traceback,而程序将在尝试分析《悉达多》后停止运行,根本不会分析《白鲸》和《小妇人》。

10.3.8 静默失败

并非每次捕获到异常都需要告诉用户,有时候可保持静默,像什么都没有发生一样继续运行。可编写try代码块,但在except代码块中使用pass语句明确地告诉Python什么都不要做。

>>> 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 语句后,如果出现FileNotFoundError 异常时,将执行except 代码块中的代码,但什么都不会发生,没有任何信息输出。

The file alice.txt has about 29465 words.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.

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

10.4 存储数据

很多程序都要求用户输入某种信息,如让用户存储游戏首选项或提供要可视化的数据。不管关注点是什么,程序都把用户提供的信息存储在列表和字典等数据结构中。用户关闭程序时,几乎总是要保存他们提供的信息。一种简单的方式是使用模块json来存储数据。

模块json 让你能够将简单的Python数据结构转储到文件中,并在程序再次运行时加载该文件中的数据。还可使用json 在Python程序之间分享数据。更重要的是,JSON数据格式并非Python专用,你能够与使用其他编程语言的人分享。这是一种轻便而有用的格式,也易于学习。

注意 JSON(JavaScript Object Notation)格式最初是为JavaScript开发的,但随后成了一种常见格式,被包括Python在内的众多语言采用。

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

我们来编写一个存储一组数的简短程序,再编写一个将这些数读取到内存中的程序。第一个程序将使用json.dump() 来存储这组数,而第二个程序将使用json.load() 。

函数json.dump() 接受两个实参:要存储的数据,以及可用于存储数据的文件对象。

number_writer.py

>>> import json
>>> numbers = [2, 3, 5, 7, 11, 13]
>>> filename = 'numbers.json'
>>> with open(filename, 'w') as f:  #以写入模式打开文件
>>>         json.dump(numbers, f)

通常使用文件扩展名.json来指出文件存储的数据为JSON格式。函数json.dump() 将数字列表存储到文件numbers.json中。这个程序没有输出,但可以打开文件numbers.json来看看内容:

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

再编写一个程序,使用json.load() 将列表读取到内存中:

number_reader.py

>>> import json
>>> filename = 'numbers.json'
>>> with open(filename) as f:  #以读取方式打开文件
>>>         numbers = json.load(f)
>>>         print(numbers)

函数json.load() 加载存储在numbers.json中的信息,并将其赋给变量numbers 。打印:

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

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

使用json保存用户生成的数据大有裨益,因为如果不以某种方式存储,用户的信息会在程序停止运行时丢失。下面看例子:提示用户首次运行程序时输入自己的名字,并在再次运行程序时记住他。先来存储用户的名字:

remember_me.py

>>> import json
>>> username = input("What is your name? ")  #输入用户名并赋给变量
>>> filename = 'username.json'
>>> with open(filename, 'w') as f:
>>>         json.dump(username, f)   #将用户名存储到文件中
>>>         print(f"We'll remember you when you come back, {username}!")

打印:

What is your name? Eric
We'll remember you when you come back, Eric!

现在向已存储了名字的用户发出问候:

greet_user.py

>>> import json
>>> filename = 'username.json'
>>> with open(filename) as f:
>>>         username = json.load(f)
>>>         print(f"Welcome back, {username}!")

输出:

Welcome back, Eric!

下面将这两个程序合并到一个程序(remember_me.py)中。这个程序尝试从文件username.json中获取用户名。因此,首先编写一个尝试恢复用户名的try 代码块。如果这个文件不存在,就在except 代码块中提示用户输入用户名,并将其存储到username.json中,以便程序再次运行时能够获取:

remember_me.py

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

用户首次运行该程序时,文件username.json不存在,将引发FileNotFoundError 异常,将执行except 代码块,用户输入用户名,再使用json.dump() 存储该用户名并打印一句问候语。首次运行,输出如下:

What is your name? Eric
We'll remember you when you come back, Eric!

否则,输出:

Welcome back, Eric!

10.4.3 重构

你经常会遇到:代码能够正确地运行,但将其划分为一系列完成具体工作的函数还可以改进。这样的过程称为重构。重构让代码更清晰、更易于理解、更容易扩展。

要重构remember_me.py,可将其大部分逻辑放到一个或多个函数中。代码重点是问候用户,于是放到一个名为greet_user() 的函数中:

remember_me.py

>>> import json
>>> def greet_user():
>>>         """问候用户,并指出其名字。"""
>>>         filename = 'username.json'
>>>         try:
>>>                 with open(filename) as f:
>>>                         username = json.load(f)
>>>         except FileNotFoundError:
>>>                 username = input("What is your name? ")
>>>                 with open(filename, 'w') as f:
>>>                         json.dump(username, f)
>>>                         print(f"We'll remember you when you come back, {username}!")
>>>         else:
>>>                 print(f"Welcome back, {username}!")
>>> greet_user()

函数greet_user() 不仅问候用户,还在存储了用户名时获取它、在没有存储用户名时提示用户输入。

下面来重构greet_user() ,减少其任务。先将获取已存储用户名的代码移到另一个函数中:

>>> import json
>>> def get_stored_username():
>>>         """如果存储了用户名,就获取它"""
>>>         filename = 'username.json'
>>>         try:
>>>                 with open(filename) as f:
>>>                         username = json.load(f)
>>>         except FileNotFoundError:
>>>                 return None
>>>         else:
>>>                 return username
>>>
>>> def greet_user():
>>>         """问候用户,并指出其名字"""
>>>         username = get_stored_username()
>>>         if username:
>>>                 print(f"Welcome back, {username}!")
>>>         else:
>>>                 username = input("What is your name? ")
>>>                 filename = 'username.json'
>>>                 with open(filename, 'w') as f:
>>>                         json.dump(username, f)
>>>                         print(f"We'll remember you when you come back, {username}!")
>>> greet_user()

新增的函数get_stored_username() 目标明确:如果存储了用户名,就获取并返回它;如果文件username.json不存在,该函数就返回None 。这是一种不错的做法:函数要么返回预期的值,要么返回None 。这让我们能够使用函数的返回值做简单的测试。

还需要重构greet_user() 中的另一个代码块,将没有存储用户名时提示用户输入的代码放在一个独立的函数中:

>>> 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:
>>>                 json.dump(username, f)
>>>         return username
>>> def greet_user():
>>>         """问候用户,并指出其名字"""
>>>         username = get_stored_username()
>>>         if username:
>>>                 print(f"Welcome back, {username}!")
>>>         else:
>>>                 username = get_new_username()
>>>                 print(f"We'll remember you when you come back, {username}!")
>>> greet_user()

在remember_me.py的这个最终版本中,每个函数都执行单一而清晰的任务。我们调用greet_user() ,它打印一条合适的消息:要么欢迎老用户回来,要么问候新用户。它首先调用get_stored_username() ,该函数只负责获取已存储的用户名(如果存储了的话)。在必要时调用get_new_username() ,该函数只负责获取并存储新用户的用户名。要编写出清晰而易于维护和扩展的代码,这种划分必不可少。

10.5 小结

在本章中学习了:

  • 如何使用文件
  • 如何一次性读取整个文件以及如何以每次一行的方式读取文件的内容
  • 如何写入文件以及如何将文本附加到文件末尾
  • 什么是异常以及如何处理程序可能引发的异常
  • 如何存储Python数据结构,以保存用户提供的信息,避免用户每次运行程序时都需要重新提供

在第11章中,将学习高效的代码测试方式。这可帮助你确定代码正确无误,以及发现扩展现有程序时可能引入的bug。

说明:记录学习笔记,如果错误欢迎指正!写文章不易,转载请联系我。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值