第十章 文件和异常

系列文章目录

第一章 起步
第二章 变量和简单数据类型
第三章 列表简介
第四章 操作列表
第五章 if 语句
第六章 字典
第七章 用户输入和 while 语句
第八章 函数
第九章 类
第十章 文章和异常
第十一章 测试代码



前言

至此,我们已经掌握了编写组织有序、易于使用的程序所需的基本技能,接下来该考虑一下让程序目标更明确、用途更大了。

在本章中,学习处理文件和保存数据可让你的程序使用起来更容易;学习异常处理可以帮助你应对文件不存在的情况以及处理其他可能导致程序崩溃的问题。所以通过本章的学习,我们便可以提高程序的实用性、可用性和稳定性。


10.1 从文件中读取数据

文本文件可存储的数据量非常多,而每当需要分析或修改存储在文件中的信息时,读取文件都很有用,对数据分析应用程序来说尤为如此。例如可以编写一个这样的程序:读取一个文本文件的内容,重新设置这些数据的格式并将其写入文件,让浏览器能够显示这些内容。

要使用文本文件中的信息,首先需要将信息读取到内存中。为此,你可以一次性读取文件的全部内容,也可以以每次一行的方式逐步读取。

10.1.1 读取整个文件

要读取文件,也得先存在一个文件来让我们读取才对嘛。下面有一个名为 pi_digits.txt 的文本文件,它包含精确到小数点后30位的圆周率,且在小数点后每10位处换行:
要读取的文件
下面的程序打开并读取这个文件(必须保存到程序所在目录,否则得用绝对引用),再将其内容显示到屏幕上:

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

首先看第一行代码,函数 open() 是用来打开文件的,它接受一个参数:要打印文件的名称。在这里,Python会在当前执行的文件(即本程序)所在的目录中查找指定的文件。该函数执行过后会返回一个表示文件的对象。在这里,open('pi_digits.txt') 返回一个表示文件 pi_digits.txt 的对象,并将其赋给 file_object 以供后续使用。

另外,第一行还有一个关键字 with ,这个关键字的作用是让 Python 在不需要访问文件后将其关闭。

其实,在这个程序中,我们只调用了 open() ,但没有调用 close() 。这是因为,调用 close() 函数来关闭文件容易导致很多错误(而且并非每次我们都能够准确得在恰当时机来关闭所用的文件),但通过使用前面所示的结构,可让 Python 自己去确定:我们只需打开文件,并在需要的时候去使用它,Python 会自动在合适的时候将其关闭。

有了表示 pi_digits.txt 的文件对象后,使用方法 read() 读取这个文件的全部内容,并将其作为一个长长的字符串赋值给变量 contents ,再打印这个变量,就可以将这个文本文件的全部内容显示出来:

3.1415926535 
  8979323846 
  2643383279

哎嘿~相比于原始文件,该输出唯一不同的地方是末尾多了一个空行。这是因为 read() 到达文件末尾是返回一个空字符串,而将这个空字符串显示出来时就是一个空行。而要删除多出来的空行,可在函数调用 print() 中使用 rstrip():print(contents.rstrip())

10.1.2 文件路径

将类似于 pi_digits.txt 的简单文件名传递给函数 open() 时,Python 将在当前执行的文件(即 .py 程序文件)所在的目录中查找。但根据我们组织文件的形式,有时有可能要打开的文件并不在程序文件所属目录中的文件。例如,我们可能将程序文件存储在了文件夹 python_work 中,而该文件夹中有一个名为 text_files 的文件用于存储程序文件操作的文本文件。虽然文件夹 text_files 包含在文件夹 python_work 中,但仅向 open() 传递位于前者的文件名称也不可行,因为 Python 只在文件夹 python_work 中查找,而不会在子文件夹 text_files 中查找。所以要让 Python 打开不与程序文件位于同一目录中的文件,需要提供文件路径,让 Python 到系统的特定位置去查找。

由于文件夹 text_files 位于文件夹 python_work 中,可以使用相对文件路径来打开其中的文件。相对路径让 Python 到指定的位置去查找,而该位置是相对于当前运行的程序所在目录的。例如可这样编写代码:with open('text_files/file_name.txt') as file_object:

注意 显示文件路径时,Windows 系统使用反斜杠( \ )而不是斜杠( / ),但在代码中依然可以使用斜杠。

还可以将文件所在计算机中的准确位置告诉 Python,这样就不用关心当前运行的程序存储在什么地方了。这称之为绝对路径。但绝对路径通常比相对路径长,因此将其赋给一个变量,在将该变量传递给 open() 会有所帮助:

file_path = '/home/ehmatthes/other_files/text_files/filename.text'
with open(file_path) as file_object:

注意 如果在文件路径中直接使用反斜杠,将引发错误,因为反斜杠用于对字符串中的字符进行转义。例如,对于路径 “ C:\path\to\file.txt ”,其中的 \t 将被解读为制表符。如果一定要使用反斜杠,可对路径中的每个反斜杠都进行转义,如 “ C:\\path\\to\\file.txt ”。

10.1.3 逐行读取

读取文件时,常常需要检查其中的每一行:可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。而要以每次一行的方式检查文件,可对文件对象使用 for 循环:

filename = 'pi_digits.txt'

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

在这里的第一行有一个使用文件时的见做法:将要读取的文件的名称赋给变量 filename 。这样一来,变量 filename 表示的并非实际文件——它只是一个让Python 知道到哪里去查找文件的字符串,因此可以轻松地将 ‘pi_digits.txt’ 替换为套使用的另一个文件的名称。使用 rstrip() 的道理同上:在这个文件中,每行的末尾都有一个看不见的换行符,而函数调用 print() 时也会加上一个换行符,因此每行末尾都有两个换行符,即一个来自文件另一个来自函数调用 print() 。

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

使用关键字 with 时,open() 返回的文件对象只在 with 代码块内使用。如果要在 with 代码块外访问文件的内容,可在 with 代码块内将文件的各行存储在一个列表中,并在 with 代码块外使用该列表:可以立即处理文件的各个部分,也可以推迟到程序后面再处理。

下面的示例在 with 代码块中将文件 pi_digits.txt 的各行存储在一个列表中,再在 with 代码块外打印:

filename = 'pi_digits.txt'

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

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

方法 readlines() 从文件中读取每一行再将其存储在一个列表中,并将该列表赋给变量 lines 。而因为列表 lines 的每个元素都对应于文件中的一行,所以输出与文件内容完全一致。

10.1.5 使用文件的内容

将文件读取到内存中,就能以任何方式使用这些数据了。下面以最简单的方式来使用圆周率的值。

首先,创建一个字符串,它包含文件中存储的所有数字,且没有任何空格:

filename = 'pi_digits.txt'

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

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

print(pi_string)
print(len(pi_string))

代码很容易看懂,也确实去掉了每行末尾的换行符,但打印出来这个字符串及长度确实这样的:

3.1415926535  8979323846  2643383279
36

这是因为变量 pi_string 指向的字符串包含原来位于每行左边的空格,为删除这些空格,可使用 strip() 而非 rstrip() :pi_string += line.rstrip(),这样就获得了一个字符串,其中包含精确到30位小数的圆周率值:

3.141592653589793238462643383279
32

注意 读取文本文件时,Python 将其中的所有文本都解读为字符串。如果读取的是数并要将其作为数值来使用,就必须使用函数 int() 将其转换为整数或使用函数 float() 将其转换为浮点数。

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

前面分析的都是一个只有三行的文本文件,但这些代码示例也可以处理大得多的文件,例如我们可以更换一个新的文件 pi_million_digits.txt ,这里包含一个精确到小数点后一百万位的圆周率数字的字符串:

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(f"{pi_string[:52]}...")
print(len(pi_string))

看吧,代码都是一样的,只是第一行换了一个文件名,倒数第二行限制了打印位数为小数点后50位(以免终端为显示全部一百万位数字而不断滚动),输出如下:

3.14159265358979323846264338327950288419716939937510...
1000002

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

是不是有人也一直想知道自己的生日是不是包含在圆周率中,为此,我们可以将生日表示为一个由数字组成的字符串,再检查这个字符串是否包含在 pi_string 中:

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()

birthday = input("Enter your birthday, in the from 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 我们要写入打开的文件。为明白其中的工作原理,我们来将一条简单的消息存储到文件中,而不是将其打印到屏幕上:

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 将在返回文件对象前清空该文件的内容。

在程序最后一行,我们使用了文件对象的方法 write() 将一个字符串写入文件。这个程序没有。这个程序没有终端输出,但如果打开文件 programming.txt ,将看到这样的内容
嗯,写入文件

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

10.2.2 写入多行

函数 write() 不会在写入的文本末尾添加换行符,因此需要在方法调用 write() 中包含换行符,这样才可以让输出出现在不同的行中:

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 附加到文件

如果要给文件添加内容,而不是覆盖原有的内容,可以以附加模式打开文件。以附加模式打开文件时,Python 不会在返回文件对象前清空文件的内容,而是将写入文件的行添加到文件末尾。当然,如果指定的文件不存在,Python 将为你创建一个空文件。

下面来修改上面的程序,使其在既有文件 programming.txt 中再添加一些自己热爱编程的原因:

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")

哝,结果就是这样:原来的内容还在,后两行则是新添的内容

I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.

10.3 异常

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

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

10.3.1 处理 ZeroDivisionError 异常

下面来看一种导致 Python 引发异常的简单错误。我们都知道,在 Python 中不能用 0 作除数,例如运行 print(5/0) 就会看到一个 traceback :

Traceback (most recent call last):
  File "D:/pycharm/hellopython/python_work/第十章 文件和异常.py", line 65, in <module>
    print(5/0)
ZeroDivisionError: division by zero

在上述 traceback 中,最后一行指出的错误 ZeroDivisionError 是个异常对象。Python 无法按照我们的要求做时,就会创建这种对象。在这种情况下,Python 将停止运行程序,并指出引发了哪种异常,而我们就可以根据这些信息对程序进行修改。

10.3.2 使用 try-except 代码块

当我们认为可能会发生错误时,可编写一个 try-except 代码来处理可能引发的异常。例如针对刚刚的情况,我们处理 ZeroDivisionError 异常的 try-except 代码块类似于下面这样:

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

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

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

You can't divide by zero!

如果 try-except 代码块后面还有其他代码,程序将接着运行,因为已经告诉了 Python 如何处理这种错误。下一小节我们就再来看一个捕获错误后程序继续运行的示例好了。

10.3.3 使用异常避免崩溃

发生错误时,如果程序还有工作尚未完成,则妥善地处理错误就显得尤其重要。这种情况经常会出现在要求客户提供输入的程序中;如果程序能够妥善地处理无效输入,就能再提示用户提供有效输入,而不至于崩溃。

下面来创建一个只执行除法运算的简单计算器:

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("\nSecond 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 "D:/pycharm/hellopython/python_work/第十章 文件和异常.py", line 82, in <module>
    answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero

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

10.3.4 else代码块

通过将可能引发错误的代码放在 try-except 代码块中,可提高程序抵御错误的能力。错误是执行除法运算的代码行导致的,因此需要将它放到 try-except 代码块中。这个示例还包含一个 else 代码块。依赖 try 代码块成功执行的代码都应放到 else 代码块中:

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
    try:
        answer = int(first_number) / int(second_number)
    except ZeroDivisionError:
        print("You can't divide by 0!")
    else:
        print(answer)

try-except-else 代码块的工作原理大致如下。Python 尝试执行 try 代码块中的代码,只有可能引发异常的代码块才需要放在 try 语句中。有时候,有一些仅在 try 代码块成功时才需要运行的代码,这些代码应放在 else 代码块中。except 代码块告诉 Python 如果尝试运行 try 代码块中的代码时引发了指定的异常该怎么办。所以就会有以下输出:

Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 2
2.5

First number: 5
Second number: 2
2.5

First number: q

10.3.5 处理 FileNotFoundError 异常

使用文件时,一种常见的问题是找不到文件:查找的文件可能在其他地方,文件名可能不正确,或者这个文件根本就不存在。对于所有这些,都可以使用 try-except 代码块以直观的方式处理。

首先我们来尝试读取一个不存在的文件:用下面的程序尝试读取没有存储在本程序所在文件目录的文件 alice.txt 的内容

filename = 'alice.txt'

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

相比较本章前面的文件打开方式,这里有两个不同之处。一是使用变量 f 来表示文件对象,这是一种常见的做法。二是给参数 encoding 指定了值,在系统的默认编码与要读取文件使用的编码不一致时,必须这样做。

Python 无法读取一个不存在的文件,因此它引发一个异常:

Traceback (most recent call last):
  File "D:/pycharm/hellopython/python_work/第十章 文件和异常.py", line 92, 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} deoe not exist.")

看吧,如果文件不存在,那么这个程序就什么都做不了,错误处理代码也意义不大。下一小节就让我们来扩展这个示例,看看我们在使用多个文件时,异常处理可以提供怎样的帮助。

10.3.6 分析文本

我们可以分析包含整本书的文本文件,例如下面我们来提取《爱丽丝漫游奇境记》(Alice in Wonderland)的文本,并尝试计算它包含多少个单词。我们将使用方法 split() ,它能根据一个字符串创建一个单词列表。为了便于理解我们来小试牛刀:对只包含童话名 “Alice in Wonderland” 的字符串调用方法 split() :

title = "Alice in Wonderland"
print(title.split())

嗯嗯,直接就把单词列表打印出来了:

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

方法 split() 以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。结果便是一个包含字符串中所有单词的列表,虽然有些单词可能包含标点。基于此我们便可以得到整篇童话大致有多少个单词:

filename = 'alice.txt'

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

我们将文件 alice.txt 移到了正确的目录中,让 try 代码块能够成功执行,最后便得到了以下输出,指出了单词个数:

The file alice.txt has about 29465 words.

悄悄告诉你,这个数稍大一点,因为使用的文本文件包含出版商提供的额外信息,这其中便包括了文件使用的编码种类:UTF-8

10.3.7使用多个文件

下面我们来多分析几本书。在此之前,先将一部分代码放在名为 count_words() 的函数中会省力很多哟,然后再编写一个简单循环,并将要分析的文件名称存储在一个列表中,对每个列表中的文件调用 count_words() 后,就可以计算多个文件中的单词量了:

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

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 deoe not exist.
The file moby_dick.txt has about 215830 words.
The file little_women.txt has about 189079 words.

10.3.8 静默失败

再前面一个示例中,我们告诉用户有一个文件找不到。但并非每次捕获到异常都需要告诉用户,有时候我们希望程序在发生异常时保持静默,就像什么都没有发生一样运行。要让程序静默失败,可像通常那样编写 try 代码块,但在 except 代码块中明确地告诉 Python 什么都不要做。Python 有一个 pass 语句,可用于让 Python 在代码块中什么都不要做,所以相比于上一个代码,只需要改动一行代码即可:

except FileNotFoundError:
	pass

可以想象到,输出也就是没了原来的第二行罢了,预料之中。另外,pass 语句还充当了占位符,提醒我们在程序中的某个地方什么都没有做,并且以后也许要在这里做些什么。例如,在这个程序中,我们可能决定将所有找不到的文件的名称写入文件 missing_files.txt 中。用户看不到这个文件,但我们可以读取它,进而处理所有找不到文件的问题。

10.3.9 决定报告哪些错误

该在什么情况下向用户报告错误?又该在什么情况下静默失败呢?如果用户知道要分析哪些文件,他们可能希望在有文件却没有分析出现时出现一条消息来告知原因。如果用户只想看到结果,并不知道要分析哪些文件,可能就无需再有些文件不存在时告知他们。向用户显示他不想看到的信息可能会降低程序的可用性。Python 的错误处理结构可以让我们能够细致地控制与用户分享错误的程度,要分享多少信息都由我们来决定。

10.4 存储数据

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

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

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

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

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

import json

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

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

这个程序,先导入模块 json ,再创建了一个数字列表。倒数第三行制定了要将该数字列表存储到哪个文件中。通常使用文件扩展名 .json 来指出文件存储的数据为 JSON 格式。接下来,以写入模式打开这个文件,让 json 能够将数据写入其中。最后在最后一行,使用函数 json.dump() 将数字列表存储到文件 numbers.json 中。

这个程序没有输出,但可以打开文件 numbers.json 来看看内容。数据的存储格式与 Python 中一样:

[2, 3, 7, 11, 13]

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

import json

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

print(numbers)

首先我们在第二行代码处确保读取的是前面写入的文件。这次以读取方式打开该文件,然后在倒数第二行代码处使用函数 json.load() 加载存储在 numbers.json 中的信息,并将其赋给变量 numbers 。最后,打印恢复的数字列表,发现与上一个程序中创建的数字列表相同:

[2, 3, 7, 11, 13]

这就是一种在程序之间共享数据的简单方式。

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

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

先来存储用户的名字:

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 rember you when you come back, {username}!")

运行过后就是这样:

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

现在再编写一个程序,向已经存储了名字的用户发出问候:

import json

filename = 'username.json'

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

这样就可以再恢复用户名后欢迎用户回来了:

Welcome back, Eric!

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

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 rember you when you come back, {username}!")
else:
    print(f"Welcome back, {username}!")

这里没有任何新的代码,只是将前两个示例的代码合并到了一个程序中。无论执行的是 except 还是 else 代码块,都将显示用户名和合适的问候语。

如果这个程序是首次运行,输出将如下:

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

否则,输出为:

Welcome back, Eric!

这是程序之前至少运行了一次时的输出。

10.4.3 重构

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

要重构一个程序,可将大部分逻辑放在一个或多个函数中。例如上一个程序的重点是问候用户,因此可以将所有代码都放在一个名为 greet_user() 的函数中:

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 rember 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 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 rember you when you come back, {username}!")
        
greet_user()

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


总结

在本章中,我们学习了:

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值