实际开发中常常会遇到对数据进⾏持久化的场景,所谓持久化是指将数据从⽆法⻓久保存数据的存储介质(通常是内存)转移到可以⻓久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的⽅式就是通过⽂件系统
将数据保存到
⽂件
中。
计算机的
⽂件系统
是⼀种存储和组织计算机数据的⽅法,它使得对数据的访问和查找变得容易,⽂件系统使⽤⽂件
和
树形⽬录
的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,⽤户使⽤⽂件系统来保存数据时,不必关⼼数据实际保存在硬盘的哪个数据块上,只需要记住这个⽂件的路径和⽂件名。在写⼊新数据之前,⽤户不必关⼼硬盘上的哪个数据块没有被使⽤,硬盘上的存储空间管理(分配和释放)功能由⽂件系统⾃动完成,⽤户只需要记住数据被写⼊到了哪个⽂件中。
打开和关闭⽂件
有了⽂件系统,我们可以⾮常⽅便的通过⽂件来读写数据;在
Python
中要实现⽂件操作是⾮常简单的。我们可以使⽤Python
内置的
open
函数来打开⽂件,在使⽤
open
函数时,我们可以通过函数的参数指定
⽂件名
、
操作模式
和
字符编码
等信息,接下来就可以对⽂件进⾏读写操作了。这⾥所说的操作模式是指要打开什么样的⽂件(字符⽂件或⼆进制⽂件)以及做什么样的操作(读、写或追加),具体如下表所示。
操作模式
|
具体含义
|
'r'
|
读取 (默认)
|
'w'
|
写⼊(会先截断之前的内容)
|
'x'
|
写⼊,如果⽂件已经存在会产⽣异常
|
'a'
|
追加,将内容写⼊到已有⽂件的末尾
|
'b'
|
⼆进制模式
|
't'
|
⽂本模式(默认)
|
'+'
|
更新(既可以读⼜可以写)
|
下图展示了如何根据程序的需要来设置
open
函数的操作模式。
![](https://img-blog.csdnimg.cn/d2535217425f4e64a2395ac76f9ebc57.png)
在使⽤
open
函数时,如果打开的⽂件是字符⽂件(⽂本⽂件),可以通过
encoding
参数来指定读写⽂件使⽤的字符编码。如果对字符编码和字符集这些概念不了解,可以看看
《字符集和字符编码》
⼀⽂, 此处不再进⾏赘述。如果没有指定该参数,则使⽤系统默认的编码作为读写⽂件的编码。当前系统默认的编码可以通过下⾯的代码获得。
import sys
print(sys.getdefaultencoding())
使⽤
open
函数打开⽂件成功后会返回⼀个⽂件对象,通过这个对象,我们就可以实现对⽂件的读写操作;如果打开⽂件失败, open
函数会引发异常,稍后会对此加以说明。如果要关闭打开的⽂件,可以使⽤⽂件对象的 close
⽅法,这样可以在结束⽂件操作时释放掉这个⽂件。
读写⽂本⽂件
⽤
open
函数打开⽂本⽂件时,需要指定⽂件名并将⽂件的操作模式设置为
'r'
,如果不指定,默认值也是 'r'
;如果需要指定字符编码,可以传⼊
encoding
参数,如果不指定,默认值是
None
,那么在读取⽂件时使⽤的是操作系统默认的编码。需要提醒⼤家,如果不能保证保存⽂件时使⽤的编码⽅式与 encoding
参数指定的编码⽅式是⼀致的,那么就可能因⽆法解码字符⽽导致读取⽂件失败。
下⾯的例⼦演示了如何读取⼀个纯⽂本⽂件(⼀般指只有字符原⽣编码构成的⽂件,与富⽂本相⽐,纯⽂本不包含字符样式的控制元素,能够被最简单的⽂本编辑器直接读取)。
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
file.close()
说明
:
《致橡树》
是舒婷⽼师在
1977
年
3
⽉创建的爱情诗,也是我最喜欢的现代诗之⼀。
除了使⽤⽂件对象的
read
⽅法读取⽂件之外,还可以使⽤
for-in
循环逐⾏读取或者⽤
readlines
⽅法将⽂件按⾏读取到⼀个列表容器中,代码如下所示。
import time
file = open('致橡树.txt', 'r', encoding='utf-8')
for line in file:
print(line, end='')
time.sleep(0.5)
file.close()
file = open('致橡树.txt', 'r', encoding='utf-8')
lines = file.readlines()
for line in lines:
print(line, end='')
time.sleep(0.5)
file.close()
如果要向⽂件中写⼊内容,可以在打开⽂件时使⽤
w
或者
a
作为操作模式,前者会截断之前的⽂本内容写⼊新的内容,后者是在原来内容的尾部追加新的内容。
file = open('致橡树.txt', 'a', encoding='utf-8')
file.write('\n标题:《致橡树》')
file.write('\n作者:舒婷')
file.write('\n时间:1977年3⽉')
file.close()
也可以使⽤下⾯的代码来完成相同的操作。
lines = ['标题:《致橡树》', '作者:舒婷', '时间:1977年3⽉']
file = open('致橡树.txt', 'a', encoding='utf-8')
for line in lines:
file.write(f'\n{line}')
file.close()
异常处理机制
请注意上⾯的代码,如果
open
函数指定的⽂件并不存在或者⽆法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以使⽤
Python
的异常机制对可能在运⾏时发⽣状况的
代码进⾏适当的处理
。
Python
中和异常相关的关键字有五个,分别是
try
、
except
、
else
、
finally和 raise
,我们先看看下⾯的代码,再来为⼤家介绍这些关键字的⽤法。
file = None
try:
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
except FileNotFoundError:
print('⽆法打开指定的⽂件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取⽂件时解码错误!')
finally:
if file:
file.close()
在
Python
中,我们可以将运⾏时会出现状况的代码放在
try
代码块中,在
try
后⾯可以跟上⼀个或多 个 except
块来捕获异常并进⾏相应的处理。例如,在上⾯的代码中,⽂件找不到会引发FileNotFoundError ,指定了未知的编码会引发
LookupError
,⽽如果读取⽂件时⽆法按指定编码⽅式解码⽂件会引发 UnicodeDecodeError
,所以我们在
try
后⾯跟上了三个
except
分别处理这三种不同的异常状况。在 except
后⾯,我们还可以加上
else
代码块,这是
try
中的代码没有出现异常时会执⾏的代码,⽽且 else
中的代码不会再进⾏异常捕获,也就是说如果遇到异常状况,程序会因异常⽽终⽌并报告异常信息。最后我们使⽤ finally
代码块来关闭打开的⽂件,释放掉程序中获取的外部资源。由于finally 块的代码不论程序正常还是异常都会执⾏,甚⾄是调⽤了
sys
模块的
exit
函数终⽌
Python
程序, finally
块中的代码仍然会被执⾏(因为
exit
函数的本质是引发了
SystemExit
异常),因此我们把 finally
代码块称为
“
总是执⾏代码块
”
,它最适合⽤来做释放外部资源的操作。
Python
中内置了⼤量的异常类型,除了上⾯代码中⽤到的异常类型以及之前的课程中遇到过的异常类型 外,还有许多的异常类型,其继承结构如下所示。
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
从上⾯的继承结构可以看出,
Python
中所有的异常都是
BaseException
的⼦类型,它有四个直接的⼦类,分别是: SystemExit
、
KeyboardInterrupt
、
GeneratorExit
和
Exception
。其中,
SystemExit 表示解释器请求退出, KeyboardInterrupt
是⽤户中断程序执⾏(按下
Ctrl+c
),
GeneratorExit
表示⽣成器发⽣异常通知退出。值得⼀提的是 Exception
类,它是常规异常类型的⽗类型,很多的异常都是直接或间接的继承⾃ Exception
类。如果
Python
内置的异常类型不能满⾜应⽤程序的需要,我们可以⾃定义异常类型,⽽⾃定义的异常类型也应该直接或间接继承自Exception
类,当然还可以根据需要重写或添加⽅法。
在
Python
中,可以使⽤
raise
关键字来引发异常(抛出异常对象),⽽调⽤者可以通过try...except... 结构来捕获并处理异常。例如在函数中,当函数的执⾏条件不满⾜时,可以使⽤抛出
异常的⽅式来告知调⽤者问题的所在,⽽调⽤者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。
class InputError(ValueError):
"""⾃定义异常类型"""
pass
def fac(num):
"""求阶乘"""
if type(num) != int or num < 0:
raise InputError('只能计算⾮负整数的阶乘!!!')
if num in (0, 1):
return 1
return num * fac(num - 1)
调⽤求阶乘的函数
fac
,通过
try...except...
结构捕获输⼊错误的异常并打印异常对象(显示异常信
息),如果输⼊正确就计算阶乘并结束程序。
flag = True
while flag:
num = int(input('n = '))
try:
print(f'{num}! = {fac(num)}')
flag = False
except InputError as err:
print(err)
上下⽂语法
对于
open
函数返回的⽂件对象,还可以使⽤
with
上下⽂语法在⽂件操作完成后⾃动执⾏⽂件对象的close ⽅法,这样可以让代码变得更加简单,因为不需要再写
finally
代码块来执⾏关闭⽂件释放资源的操作。需要提醒⼤家的是,并不是所有的对象都可以放在 with
上下⽂语法中,只有符合
上下⽂管理
器协议
(有
__enter__
和
__exit__
魔术⽅法)的对象才能使⽤这种语法,
Python
标准库中的contextlib 模块也提供了对
with
上下⽂语法的⽀持,后⾯再为⼤家进⾏讲解。
try:
with open('致橡树.txt', 'r', encoding='utf-8') as file:
print(file.read())
except FileNotFoundError:
print('⽆法打开指定的⽂件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取⽂件时解码错误!')
读写⼆进制⽂件
读写⼆进制⽂件跟读写⽂本⽂件的操作类似,但是需要注意,在使⽤
open
函数打开⽂件时,如果要进⾏读操作,操作模式是 'rb'
,如果要进⾏写操作,操作模式是
'wb'
。还有⼀点,读写⽂本⽂件
时,
read
⽅法的返回值以及
write
⽅法的参数是
str
对象(字符串),⽽读写⼆进制⽂件时,
read
⽅法的返回值以及 write
⽅法的参数是
bytes-like
对象(字节串)。下⾯的代码实现了将当前路径下名为 guido.jpg
的图⽚⽂件复制到
吉多
.jpg
⽂件中的操作。
try:
with open('guido.jpg', 'rb') as file1:
data = file1.read()
with open('吉多.jpg', 'wb') as file2:
file2.write(data)
except FileNotFoundError:
print('指定的⽂件⽆法打开.')
except IOError:
print('读写⽂件时出现错误.')
print('程序执⾏结束.')
如果要复制的图⽚⽂件很⼤,⼀次将⽂件内容直接读⼊内存中可能会造成⾮常⼤的内存开销,为了减少对内存的占⽤,可以为 read
⽅法传⼊
size
参数来指定每次读取的字节数,通过循环读取和写⼊的⽅式来完成上⾯的操作,代码如下所示。
try:
with open('guido.jpg', 'rb') as file1, \
open('吉多.jpg', 'wb') as file2:
data = file1.read(512)
while data:
file2.write(data)
data = file1.read()
except FileNotFoundError:
print('指定的⽂件⽆法打开.')
except IOError:
print('读写⽂件时出现错误.')
print('程序执⾏结束.')
简单的总结
通过读写⽂件的操作,我们可以实现数据持久化。在
Python
中可以通过
open
函数来获得⽂件对象,可以通过⽂件对象的 read
和
write
⽅法实现⽂件读写操作。程序在运⾏时可能遭遇⽆法预料的异常状况,可以使⽤Python
的异常机制来处理这些状况。
Python
的异常机制主要包括try 、
except
、
else
、
finally
和
raise
这五个核⼼关键字。
try
后⾯的
except
语句不是必须的, finally
语句也不是必须的,但是⼆者必须要有⼀个;
except
语句可以有⼀个或多个,多个 except
会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进⼊后续的
except
语句; except
语句中还可以通过元组同时指定多个异常类型进⾏捕获;
except
语句后⾯如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使⽤ raise
要再次抛出,但是不建议捕获并抛出同⼀个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。最后强调⼀点,不
要使⽤异常机制来处理正常业务逻辑或控制程序流程,简单的说就是不要滥⽤异常机制。