Python3完全新手小白的学习手册 10 文件和异常

从第一到第十章,我们学习了编写程序的所需的基本技能。

在本章节中,我们将学习如何处理文件,以及如何使用异常处理错误。

读取文件

文本文件可以存储许多数据:天气数据、交通数据、社会经济数据、文学作品等。

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

读取文件的全部内容

我们先准备一个文件,名为pi_digits.txt,内容如下:

3.1415926535
  8979323846
  2643383279

然后我们开始编写第一个读取文件的代码file_reader.py:

from pathlib import Path

path = Path('pi_digits.txt')
contents = path.read_text()
print(contents)

要使用文件的内容,需要将其路径告知 Python。
路径(path)是文件或文件夹在系统中的准确位置。
Python 提供了 pathlib 模块,能处理各种操作系统中处理文件和目录。

首先中 pathlib 模块导入 Path 类,再使用 Path 对像指向一个文件。
这里创建了一个表示文件 pi_digits.txt 的 Path 对象,并将其赋给了变量 path由于这个文件与当前编写的 .py 文件位于同一个目录中,因此 Path 只需要知道其文件名就能访问它。

创建表示文件 pi_digits.txt 的 Path 对象后,使用 read_text() 方法来读取这个文件的全部内容。
**read_text()**将该文件的全部内容作为一个字符串返回。我们将这个字符串赋给了变量 contents,再将其打印出来。

相比原始原件,输出唯一不同的地方是末尾多了一个空行。
因为 read_text()在到达文件末尾时会返回一个空字符串,而这个空字符串会被显示一个空行。
要删除这个多出来的空行,可对字符串变量contents调用 strip() 方法。

contents = path.read_text().rstrip()

这种方式称为方法链式调用。

博主使用的 3.13版本已经没有了这个问题。

相对路径和绝对路径

指定路径的方式有两种:

  • 相对文件路径让 Python 到相对于当前运行的程序文件所在的目录去查找。
  • 绝对文件路径让 Python 去系统的准确位置去查找。

在相对路径行不通时,可使用绝对路径。

绝对路径通常比相对路径长,因为以系统的根文件夹为起点。
在 Linux 系统中,系统的根文件夹为 /,而在 Windows 系统中,系统的根文件夹为 C:\。


path = Path('/home/eric/data_files/text_files/filename.txt')

在 Windows 系统中,可使用反斜杠(\)而不是斜杠(/)来分隔路径中的文件夹。

访问文件中的各行

使用 splitlines() 方法将冗长的字符串转换为一系列行,再使用 for 循环以每次一行的方式检查文件中的各行

from pathlib import Path

path = Path('pi_digits.txt')
contents = path.read_text()

lines = contents.splitlines()
for line in lines:
    print(line)

和前面一样,读取文件的全部内容,由于没有修改行,因此输出与文件内容相同。

使用文件的内容

将文件的内容读取到内存中后,就可以以任何方式使用这些数据了。

首先,创建一个字符串,它包含文件中存储的所有数字。

from pathlib import Path

path = Path('pi_digits.txt')
contents = path.read_text()

lines = contents.splitlines()
# 创建变量 pi_string
pi_string = ''
for line in lines:
    pi_string += line

print(pi_string)
print(len(pi_string))

输出如下:

3.1415926535  8979323846  2643383279
32

变量pi_string存储的字符串包含原来位于每行左端的空格。要删除这些空格,可对每行调用lstrip()方法。

for line in lines:
    pi_string += line.lstrip()

输出如下:

3.141592653589793238462643383279
32

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

包含100万位的大型文件

如果一个文本文件包含精确到小数点后 1 000 000 位而不是 30 位的圆周率值,也可以创建一个包含所有这些数字的字符串。

在这个例子中,我们将使用一个包含 100 000 位的圆周率值的文本文件,该文件的第一行只包含前 50 位。

from pathlib import Path

path = Path('pi_million_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
for line in lines:
    pi_string += line.lstrip()

print(f"{pi_string[:52]}...")
print(len(pi_string))

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

现在我们知道如何使用 Python 来读取文件,我们来编写一个程序,看看圆周率值中包含你的生日吗?

from pathlib import Path

path = Path('pi_million_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
for line in lines:
    pi_string += line.lstrip()
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.")

写入文件

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

写入一行

定义一个文件的路径后,就可使用 write_text() 将数据写入该文件了。

from pathlib import Path

path = Path("programming.txt")
# write_text() 方法接受单个实参,即要写入文件的字符串。
path.write_text("I love programming.\n")

。这个程序没有终端输出,但你如果打开文件 programming.txt。

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

写入多行

write_text() 方法会在幕后完成几项工作。

首先,如果 path 变量对应的路径指向的文件不存在,就创建它。
其次,将字符串写入文件后,它会确保文件得以妥善地关闭。

要将多行写入文件,需要先创建一个字符串(其中包含要写入文件的全部内容),再调用 write_text() 并将这个字符串传递给它。

from pathlib import Path
contents = "I love programming.\n"
contents += "I love creating new games.\n"
contents += "I also love working with data.\n"

path = Path('programming.txt')
path.write_text(contents)

在对 path 对象调用 write_text() 方法时,务必谨慎。如果指定的文件已存在, write_text() 将删除其内容,再将指定的文本写入其中。

异常

Python 使用称为异常(exception)的特殊对象来管理程序执行期间发生的错误。

异常是使用 try-except 代码块处理的。

处理ZeroDivisionError异常

print(5/0)

输出如下:

Traceback (most recent call last):
  File "C:\Users\Administrator\PycharmProjects\pythonProject\demo.py", line 1, in <module>
    print(5/0)  # 报错
ZeroDivisionError: division by zero

在上述 traceback 中,错误 ZeroDivisionError 是个异常对象。
Python 在无法按你的要求做时,就会创建这种对象。

使用try-except代码块

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

输出如下:

You can't divide by zero!

当你认为可能发生了错误时,可编写一个 try 语句来告诉 Python,你要执行的操作可能引发这种错误。

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 代码块来打印结果

处理FileNotFoundError异常

在使用文件时,一种常见的问题是找不到文件:要查找的文件可能在其他地方,文件名可能不正确,或者这个文件根本就不存在。

from pathlib import Path

path = Path('alice.txt')
contents = path.read_text(encoding='utf-8')

这里使用 read_text() 的方式与前面稍有不同。如果系统的默认编码与要读取的文件的编码不一致,参数 encoding 必不可少。

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

通常最好从 traceback 的末尾着手。从最后一行可知,引发了异常 FileNotFoundError。

from pathlib import Path

path = Path('alice.txt')
try:
    contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
    print(f"Sorry, the file {path} does not exist.")

这个示例中,try 代码块中的代码引发了 FileNotFoundError 异常,因此要编写一个与该异常匹配的 except 代码块。
这样,当找不到文件时,Python 将运行 except 代码块中的代码,从而显示一条友好的错误消息,而不是 traceback。

分析文本

from pathlib import Path

path = Path('alice.txt')
try:
    contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
    print(f"Sorry, the file {path} does not exist.")
else:
    # 计算文件大致包含多少个单词
    words = contents.split()
    num_words = len(words)
    print(f"The file {path} has about {num_words} words.")

仅当 try 代码块成功执行时才会执行它们。输出指出了文件 alice.txt 包含多少个单词。

使用多个文件

下面分析几本书

from pathlib import Path

def count_words(filename):
    """计算一个文件大致包含多少个单词。"""
    try:
        contents = Path(filename).read_text(encoding='utf-8')
    except FileNotFoundError:
        msg = f"Sorry, the file {filename} does not exist."
        print(msg)
    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)

先将文件名存储为简单字符串,然后将每个字符串转换为 Path 对象,再调用 count_words()。

在这个示例中,使用 try-except 代码块有两个重要的优点:一是避免用户看到 traceback,二是让程序可以继续分析能够找到的其他文件。

静默失败

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

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

当这种错误发生时,既不会出现 traceback,也没有任何输出。用户将看到存在的每个文件包含多少个单词,但没有任何迹象表明有一个文件未找到

决定报告哪些错误

只要程序依赖于外部因素,如用户输入、是否存在指定的文件、是否有网络连接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理块,以及出现错误时该向用户提供多少相关的信息。

存储数据

一种简单的方式是使用模块 json 来存储数据。

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

第一个程序将使用 json.dumps() 来存储这组数,而第二个程序将使用 json.loads() 来读取它们。

from pathlib import Path
import json

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

path = Path('numbers.json')
contents = json.dumps(numbers)
path.write_text(contents)

首先导入模块 json,并创建一个数值列表。然后选择一个文件名,指定要将该数值列表存储到哪个文件中。
接下来,使用 json.dumps() 函数生成一个字符串,将其存储到变量 contents 中。
最后,使用 Path 对象的 write_text() 方法将这个字符串写入到文件 numbers.json 中。

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

from pathlib import Path
import json

path = Path('numbers.json')
contents = path.read_text()
numbers = json.loads(contents)
print(numbers)

这个数据文件是使用特殊格式的文本文件,因此可使用 read_text() 方法来读取它。然后将这个文件的内容传递给 json.loads()。这个函数将一个 JSON 格式的字符串作为参数,并返回一个 Python 对象(这里是一个列表),而我们将这个对象赋给了变量 numbers。最后,打印恢复的数值列表

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

使用 json 保存用户生成的数据很有必要

from pathlib import Path
import json

# 首先,提示用户输入名字
username = input("请输入你的用户名:")

# 接下来,将收集到的数据写入文件 username.json
path = Path('username.json')
contents = json.dump(username)
path.write_text(contents)

# 然后,打印一条消息,指出存储了用户输入的信息
print(f"我们将记住你的用户名,{username}")

再编写一个程序,向名字已被存储的用户发出问候

from pathlib import Path
import json

path = Path('username.json')
contents = path.read_text()
username = json.loads(contents)

print(f"欢迎回来,{username}!")

我们读取数据文件的内容,并使用 json.loads() 将恢复的数据赋给变量 userusername

from pathlib import Path
import json

path = Path('username.json')
# 如果指定的文件或文件夹存在,exists() 方法返回 True,否则返回 False。
# 这里使用 path.exists() 来确定是否存储了用户名
if path.exists():
    # 如果文件 username.json 存在,就加载其中的用户名,并向用户发出个性化问候
    contents = path.read_text()
    username = json.loads(contents)
    print(f"欢迎回来,{username}!")
else:
    # 如果文件 username.json 不存在,就提示用户输入用户名,并存储用户输入的值。
    username = input("请输入你的用户名:")
    contents = json.dumps(username)
    path.write_text(contents)
    print(f"我们将记住你的用户名,{username}")

重构

虽然代码能够正确地运行,但还可以将其划分为一系列完成具体工作的函数来进行改进。这样的过程称为重构。

重构让代码更清晰、更易于理解、更容易扩展。

from pathlib import Path
import json

def greet_user():
    """问候用户,并指出其名字。"""
    path = Path('users.json')
    if path.exists():
        contents = path.read_text()
        username = json.loads(contents)
        print(f'欢迎回来{username}')
    else:
        username = input('请输入你的名字:')
        contents = json.dumps(username)
        path.write_text(contents)
        print(f'我们将记住你的名字{username}')
        
greet_user()

greet_user() 函数所做的不仅是问候用户,还在存储了用户名时获取它,在没有存储用户名时提示用户输入。
下面重构 greet_user(),不让它执行这么多任务。

from pathlib import Path
import json

# 如果存储了用户名,就获取并返回它;如果传递给 get_stored_username() 的路径不存在,就返回 None
def get_stored_username(path):
    """如果存储了用户名,就获取它"""
    if path.exists():
        contents = path.read_text()
        username = json.loads(contents)
        return username
    else:
        # 这是一种不错的做法:函数要么返回预期的值,要么返回 None。
        return None
    
def get_stored_username(path):
    """提示用户输入用户名"""
    username = input('请输入你的名字? ')
    contents= json.dumps(username)
    path.write_text(contents)
    
    return username


def greet_user():
    """问候用户,并指出其名字"""
    path = Path('username.json')
    username = get_stored_username(path)

    if username:
        print(f"Welcome back, {username}!")
    else:
        username = get_stored_username(path)

        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
发出的红包

打赏作者

qyydeep

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值