Python学习记录 第十章文件和异常

第十章 文件和异常

10.1 从文件中读取数据

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

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

10.1.1 读取整个文件

要读取文件,需要一个包含几行文本的文件。下面首先创建一个文件。下面首先创建一个文件,它包含精确到小数点后30位的圆周率值,且在小数点后每10位处换行:

pi_digits.txt

3.1415926535
  8979323846
  2643383279

要动手尝试后续示例,可在编辑器中输入这些数据行,再将文件保存为 pi_digits.txt

下面的程序打开并读取这个文件,再将其显示到屏幕上:

file_reader.py

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

在这个程序中,第一行代码做了大量工作。我们先来看看函数 open( ) 。要以任何方式使用文件,哪怕仅仅是打印其内容,都得先打开文件,才能访问它。函数 open( ) 接受一个函数:要打开的文件 的名称。Python 在当前执行的文件所在的目录中查找指定的文件。在本例中,当前运行的是 file_reader.py ,因此 python 在 file_reader.py 所在的目录中查找 pi_digits.txt 。函数 open( ) 返回一个表示文件的对象。在这里,open(‘pi_digits.txt’) 返回一个表示文件 pi_digits.txt 的对象,python 将该对象赋给 file_object 供以后使用。

关键字 with 在不需要访问文件后将其关闭。在这个程序中,注意到我们调用了 open( ) ,但没用调用 close( ) 。也可以调用 open( ) 和 close( ) 来打开和关闭这个文件,但这样做时,如果程序存在 bug 导致方法 close( ) 未执行,文件将不会关闭。这看似微不足道,但未妥善关闭文件可能导致数据丢失或者受损。如果在程序中过早调用 close( ) ,你会发现需要使用文件时它已关闭(无法访问),这会导致更多错误。并非在任何情况下都能轻松确定关闭文件的恰当时机,但通过使用前面所示的的结构,可让python 去确定:你只管打开文件,并在需要时使用它,python 自会在合适的适合自动将其关闭。

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

3.1415926535
  8979323846
  2643383279

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

with open('pi_digits.txt') as file_object:
    contents = file_object.read()
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/filename.txt') as file_object:

这行代码让 python 到文件夹 python_work 下的文件夹 text_files 中去查找指定的 txt 文件。

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

还可以将文件在计算机中的准确位置告诉 python ,这样就不用关心当前运行的程序存储在什么地方了。这称为绝对文件路径。在相对路径行不通时,可使用绝对路径。例如,如果 text_files 并不在文件夹 python_work 中,而在文件夹 other_files 中,则向 open( ) 传递路径 ‘text_files/filename.txt’ 行不通,为明确指出希望 python 去哪里查找,需要提供完整的路径。

绝对路径通常比相对路径长,因此将其赋给一个变量,再将该变量传递给 open( ) 会有所帮助:

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

通过使用绝对路径,可读取系统中任何地方的文件。就目前而言,最简单的做法是,要么将数据文件存储在程序所在的目录中中,要么将其存储在程序文件所在目录下的一个文件夹中。

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

10.1.3 逐行读取

读取文件时,常常需要检查其中的每一行:可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。例如,你可能要遍历一个包含天气数据的文件,并使用天气描述中包含 sunny 字样的行。在新闻报道中,你可能会查找包含标签 < headline > 的行,并按特定的格式设置它。

要以每次一行的方式检查文件,可对文件对象使用 for 循环:

filename = 'pi_digits.txt'

with open(filename) as object:
    for line in object:
        print(line)

首先将要读取的文件名称赋给变量 filename。这是使用文件的一种常见做法。变量 filename表示的并非实际文件——它只是一个让 python 知道到哪里去查找文件的字符串。调用 open( ) 后,将一个表示文件及其内容的对象赋给了变量 object 。为查看文件的内容,通过对文件对象执行循环来遍历文件中的每一行。

打印每一行时,发现空白行更多了:

3.1415926535

  8979323846

  2643383279
  

为何会出现这些空白行呢?因为在这个文件中,每行的末尾都有一个看不见的换行符,而函数调用 print( ) 也会加上一个换行符,因此每行末尾都有两个换行符:一个来自文件,另一个来自函数调用 print( ) 。要消除这些多余的空白行,可在函数调用 print( ) 中使用 rstrip( ) :

filename = 'pi_digits.txt'

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

输出:

3.1415926535
  8979323846
  2643383279

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

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

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

filename = 'pi_digits.txt'

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

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

方法 readlines( ) 从文件中读取每一行,并将其存储在一个列表中。接下来,该列表被赋给变量 lines 。在 with 代码块外,依然可以使用这个变量。使用了一个简单的 for 循环来打印 lines 中的各行。因为列表 lines 的每个元素都对应文件中的一行,所以输出与文件内容完全一致。

10.1.5 使用文件的内容

将文件读取到内存中后,就能以任何方式使用这些数据了。下面以简单的方式使用圆周率的值。首先,创建一个字符串,它包含文件中存储的所有数字,且没用任何空格:

filename = 'pi_digits.txt'

with open(filename) as object:
    lines = 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( ) :

filename = 'pi_digits.txt'

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

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

print(pi_string)
print(len(pi_string))

这样就获得了一个字符串,其中包含准确到30位小数的圆周率值。这个字符串长32字符:

3.141592653589793238462643383279
32

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

10.2 写入文件

保存数据的最简单方式之一是将其写入文件中。通过将输出写入文件,即便关闭包含程序输出的终端窗口,这些输出也依然存在:可以在程序结束运行后查看这些输出,可以与别人分享输出文件,还可以编写程序来将这些输出读取到内存中并进行处理。

10.2.1 写入空文件

要将文本写入文件,你在调用 open( ) 时需要提供另一个实参,告诉python你要写入打开的文件。为明白其中的工作原理,我们来将一条简单的消息存储到文件中,而不是将其打印到屏幕上:

write_message.py

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( ) 会自动创建它。然而,以写入模式打开文件时要小心,因为如果指定的文件已经存在,python 将在返回文件对象前清空该文件的内容。

使用文件对象的方法 write( ) 将一个字符串写入文件。这个程序没有终端输出,但如果打开文件 programming.txt ,将看到如下内容:

I love programming.

相比于计算机中的其它文件,这个文件没有什么不同。你可以打开它、在其中输入新文本、复制其内容、将内容粘贴到其中,等等。

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

10.2.2 写入多行

函数 write( ) 不会在写入文本末尾添加换行符,因此如果写入多行时没有指定换行符,文件看起来可能不是你希望的那样:

filename = 'programming.txt'

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

如果你打开文件,你会发现两行内容挤在一起:

I love programming.I love creating new games.

要让每个字符串都单独占一行,需要在方法调用 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')

现在输出出现在不同的行中:

I love programming.
I love creating new games.

像显示到终端的输出一样,还可以使用空格、制表符和空行来设置这些输出的格式。

10.2.3 附加到文件

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

下面来修改 write_message.py ,在既有文件 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 异常

10.3.1 处理ZeroDivisionError异常

10.3.2 使用try-except代码块

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 "C:\Users\Asserking\Desktop\py编辑\file_reader.py", line 11, in <module>
    answer = int(first_number)/int(second_number)
ZeroDivisionError: division by zero

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("\nSecond 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)

让 python 尝试执行 try 代码块中的除法运算,这个代码块只包含可能导致错误的代码。依赖 try 代码块成功执行的代码都放在 else 代码块中。在本例中,如果除法运算成功,就使用 else 代码块来打印结果。

except 代码块告诉 python,出现 ZeroDivisionError 异常时该如何办。如果 try 代码块因除零错误而失败,就打印一条友好的信息,告诉用户如何避免这种错误。程序继续运行,用户根本看不到 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 语句中。有时候,有一些仅在 try 代码块成功执行时才需要运行的代码,这些代码应放在 else 代码块中。except 代码块告诉 python ,如果尝试执行 try 代码块中的代码引发了指定的异常时该怎么办。

10.3.5 处理FileNotFoundError异常

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

我们来尝试读取一个不存在的文件。下面的程序尝试读取文件 alice.txt 的内容,但该文件没有存储在 alice.py 所在的目录中:

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 "C:\Users\Asserking\Desktop\py编辑\untitled0.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} doesn't esist.")

此时输出一条友好的信息,而不是 traceback:

Sorry,the file alice.txt doesn't esist.

如果文件不存在,这个程序就什么也做不了,错误处理代码也意义不大。下面来扩展这个示例,看看在你使用多个文件时,异常处理可提供什么样的帮助。

10.3.6 分析文本

你可以分析包含整本书的文本文件。下面来提取童话《爱丽丝漫游奇境》的文本,并尝试计算它包含多少个单词。我们使用的方法 split( ) ,它能根据一个字符串创建一个单词列表。下面是对只包含童话名 “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} doesn't esist.")
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.

这个数稍大一点,因为使用的文本文件包含出版商提供的额外信息,但还是成功估算出了篇幅。

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} doesn't esist.")
    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( ) 中,并增加了缩进量。修改程序的同时更新注释是个不错的习惯,因此我们将注释改成文档字符串,并稍微调整下措辞。

现在可以编写一个简单的循环,计算要分析的任何文本的单词数了。为此,我们将要分析的文件的名称存储在一个列表中,然后对列表中的每个文件调用 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} doesn't esist.")
    else:
        words = contents.split()
        num_words = len(words)
        print(f"The file {filename} has about {num_words} words.")

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

文件 siddhartha.txt 不存在,但不影响程序处理其它文件:

The file alice.txt has about 29465 words.
Sorry,the file siddhartha.txt doesn't esist.
The file moby_dick.txt has about 215830 words.
The file little_women has about 189079 words.

10.3.8 静默失败

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

def count_words(filename)
    """计算一个文件大致包含多少个单词"""
    try:
        --snip--
    except FileNotFoundError:
        pass
    else:
        --snip--

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

相比前一个程序,这个程序唯一的不同之处是 pass 语句。现在,出现 FileNotFoundError 异常时,将执行 except 代码块中的代码,但什么都不会发生。这种错误发生时,不会出现 traceback,也没有任何输出。用户将看到存在的每个文件包含多少单词,但没有任何迹象表明有一个文件未找到:

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

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

10.4 存储数据

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

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

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

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

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

函数 json.dump( ) 接受两个实参:要存储的数据,以及可用于存储数据的文件对象。下面演示了如何使用 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 格式。接下来,以写入模式打开这个文件,让 json 能够将数据写入其中。随后使用函数 json.dump( ) 将数字列表存储到文件中。

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

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

输出结果相同:

[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}!")

使用 json.load( ) 将存储在 username.json 中的信息读取到变量 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 username?")
    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 不存在,将执行 except 代码块,提示用户输入用户名,再使用 json.dump( ) 存储该用户名并打印一条问候语。因此首次执行将会输出:

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

否则,输出如下:

Welcome back,Eric!

10.4.3 重构

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

要重构 remember_me.py ,可将其大部分逻辑放到一个或多个函数中。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 username?")
        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 username?")
        with open(filename,'w') as f:
            json.dump(username,f)
            print(f"We'll remember you when you come back,{username}!")

greet_user()

新增的函数目标明确,如果存储了用户名,该函数就获取并返回它;如果文件不存在,该函数就返回 None。

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

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():
    filename = 'username.json'
    username = input("What is your username?")
    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元

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

抵扣说明:

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

余额充值