文件

数据持久化最简单的类型是普通文件,有时也叫平面文件(flat file)。它仅仅是在一个文件名下的字节流,把数据从一个文件读入内存,然后从内存写入文件。Python很容易实现这些文件操作,它模仿熟悉和流行的 unix 系统的操作。

读写一个文件之前需要打开它:

1
data = open(filename, mode)

下面是对该 open() 调用的简单解释:

  • data 是 open() 返回的文件对象;
  • filename 是该文件的字符串名;
  • mode 是指明文件类型和操作的字符串。

mode 的第一个字母表明对其的操作。

  • r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
  • r+ 可读可写,不会创建不存在的文件。如果直接写文件,则从顶部开始写,覆盖之前此位置的内容,如果先读后写,则会在文件最后追加内容。
  • w 表示写模式。若果文件不存在则新创建,如果该文件已存在则将其覆盖。
  • w+ 打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
  • x 表示在文件不存在的情况下新创建并写文件。
  • a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
  • a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。

mode 的第二个字母是文件类型:

  • t(或者省略) 代表文本类型。
  • b 代表二进制文件。

打开文件之后就可以调用函数来读写数据,最后需要关闭文件。

使用 write() 写文本文件

1
2
3
>>> poem = "日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。"
>>> len(poem)
32

将整首诗写到libai.txt中:

1
2
3
4
>>> fout = open('libai.txt', 'wt')
>>> fout.write(poem)
32
fout.close()

函数 write() 返回写入文件的字节数。和 print() 一样, 他没有增加空格或者换行符。同样,你也可以在一个文本文件中使用 print()

1
2
3
>>> fout = open('libai.txt', 'w')
>>> print(poem, file=fout)
>>> fout.close()

这就产生了一个问题:到底是用是 write() 还是 print()? print() 默认会在每个参数后面添加空格,在每行结束处添加换行。在之前的例子中, libai.txt 中默认添加了一个换行。为了使 print() 与 write() 有同样的输出,传入下面两个参数:

  • sep 分隔符:默认是一个空格 ' '
  • end 结束字符:默认是一个换行符 ‘\n’

除非自定义参数,否则 print() 会使用默认参数。在这里,我们通过空字符串替换 print() 添加的所有多余输出:

1
2
3
>>> fout = open('libai.txt', 'w')
>>> print(poem, file=fout, sep='', end='')
>>> fout.close()

如果字符串非常大,可以将数据分块,直到所有字符被写入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> fout = open('libai.txt', 'w')
>>> size = len(poem)
>>> size
32
>>> offset = 0
>>> chunk = 10
>>> while True:
...     if offset > size:
...         break
...     fout.write(poem[offset:offset+chunk])
...     offset += chunk
...
>>> fout.close()

第一次写 10 个字符,4次写完,32个字符。

如果 libai.txt 文件已经存在,使用模式 x 可以避免重写文件:

1
2
3
4
5
6
>>> font = open('libai.txt', 'xt')
Traceback (most recent call last):
  File "<ipython-input-108-c2dbf65c7612>", line 1, in <module>
    font = open('libai.txt', 'xt')
FileExistsError: [Errno 17] File exists: 'libai.txt'
>>>

使用 read()、readline() 或者 readlines() 读取文本文件

你可以按照下面的示例那样,使用不带参数的 read() 函数一次读入文件的所有内容。但在读入文件时要格外注意,1GB的文件会用到相同大小的内存。

1
2
3
4
5
6
7
>>> fin = open('libai.txt', 'rt')
>>> poem = fin.read()
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'
>>> fin.close()
>>> len(poem)
32

同样也可以设置最大的读入字符数限制 read() 函数一次返回的大小。下面一次读入10个字符,然后把每一快拼接成原来的字符串 poem

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> chunk = 10
>>> while True:
...     fragment = fin.read(chunk)
...     if not fragment:
...         break
...     poem += fragment
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

读到结尾之后,再次调用 read() 会返回空字符串(''), if not fragment 条件被判断为 False。此时会跳出 while True 的循环。 当然, 你也能使用 readline() 每次读入文件的一行。 在下面的例子中,通过追加每一行拼接成原来的字符串 poem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> while True:
...     line = fin.readline()
...     if not line:
...         break
...     poem += line
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

对于一个文本文件,即使空行也有1字符长度(换行符’\n’),自然会返回 True。当文件读取结束后,readline() (类似 read() ) 同样会返回空字符串,也被 while True 判断为 False。

读取文本文件最简单的方式是使用一个迭代器(iterator),它会每次返回一行。这和之前的例子类似,但代码会更短:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> for line in fin:
...     poem += line
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

前面所有的示例最终都返回单个字符串 poem。 函数 readlines() 调用时每次读取一行,并返回单行字符串的列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> poem = '''日照香炉生紫烟,
... 遥看瀑布挂前川。
... 飞流直下三千尺,
... 疑是银河落九天。
... '''
>>> fin = open('libai.txt', 'wt')
>>> fin.write(poem)
36
>>> fin.close()

>>> fin = open('libai.txt', 'rt')
>>> lines = fin.readlines()
>>> fin.close()
>>> lines
['日照香炉生紫烟,\n', '遥看瀑布挂前川。\n', '飞流直下三千尺,\n', '疑是银河落九天。\n']
>>> print(len(lines), 'lines read')
4 lines read
>>> for line in lines:
...     print(line, end='')
...
日照香炉生紫烟,
遥看瀑布挂前川。
飞流直下三千尺,
疑是银河落九天。

使用 write() 写二进制文件

如果文件模式字符串中包含 ‘b’, 那么文件会以二进制模式打开,这种情况下,读写的是字节而不是字符串。

我们手边没有二进制格式的诗,所以直接在 0~255 产生 256 字节的值:

1
2
3
>>> bdata = bytes(range(0, 256))
>>> len(bdata)
256

以二进制模式打开文件,并且一次写入所有的数据:

1
2
3
4
5
>>> fout = open('bfile', 'wb')
>>> fout.write(bdata)
256
>>> fout.close()
>>>

再次,write() 返回到写入的字节数。

对于文本,也可以分块写二进制数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> fout = open('bfile', 'wb')
>>> size = len(bdata)
>>> offset = 0
>>> chunk = 100
>>> while True:
...     if offset > size:
...         break
...     fout.write(bdata[offset:offset+chunk])
...     offset += chunk
...
100
100
56
>>> fout.close()

使用 read() 读取二进制文件

1
2
3
4
5
>>> fin = open('bfile', 'rb')
>>> bdata = fin.read()
>>> len(bdata)
256
>>> fin.close()

使用 with 自动关闭文件

如果你忘记关闭已经打开的一个文件,在该文件对象不再被引用之后 Python 会关掉此文件。这也意味着在一个函数中打开文件,并没有及时关闭它,但是在函数结束时会被关掉。然而你可能会在一直运行中的函数或者程序的主要部分打开一个文件,应该强制剩下的所有写操作完成后再关闭文件。

Python的上下文管理器(context manager)会清理一些资源,例如打开的文件。它的形式为 with expression as variable

1
2
3
>>> with open('libai.txt', 'wt') as fout:
>>> ... fout.write(poem)
...

完成上下文管理器的代码后,文件会自动关闭。

使用 seek() 改变位置

无论是读或者写文件,Python都会跟踪文件中的位置。函数 tell() 返回距离文件开始处的字节偏移量。函数 seek() 允许跳转到文件其他字节偏移量的位置。这意味着可以不用从头读取文件的没一个字节,直接跳转到最后位置并制度一个字节也是可行的。

对于这个例子,使用之前写过的 256 字节的二进制文件 ‘bfile’:

1
2
3
>>> fin = open('bfile', 'rb')
>>> fin.tell()
0

使用 seek() 读取文件结束前最后一个字节:

1
2
>>> fin.seek(255)
255

一直读到文件结束:

1
2
3
4
5
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

seek() 同样返回当前的偏移量。

用第二个参数调用函数 seek(): seek(offset, origin)

  • 如果 origin 等于0 (默认为0),从头偏移 offset 个字节;
  • 如果 origin 等于1,从当前位置处偏移 offset 个字节;
  • 如果 origin 等于2,距离最后结尾处偏移 offset 个字节。

这些值也在标准库 os 模块中被定义:

1
2
3
4
5
6
7
>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2

所以,我们可以用不通的方法读取最后一个字节:

1
>>> fin = open('bfile', 'rb')

文件结尾前的一个字节:

1
2
3
4
>>> fin.seek(-1, 2)
255
>>> fin.tell()
255

一直读到文件结尾:

1
2
3
4
5
>>> bddata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

在调用 seek() 函数时不需要额外调用 tell()。前面的例子只是想说明两个函数都可以返回同样的偏移量。

下面是从文件的当前位置寻找:

1
>>> fin = open('bfile', 'rb') 

接下来的例子返回最后两个字节:

1
2
3
4
>>> fin.seek(254, 0)
254
>>> fin.tell()
254

在此基础上前进一个字节:

1
2
3
4
>>> fin.seek(1, 1)
255
>>> fin.tell()
255

这些函数对于二进制文件都是极其重要的。当文件是 ASCII 编码(每个字符一个字节)时,也可以使用它们,但是计算偏移量会是一个麻烦事。其实,这些都取决于文件的编码格式,最流行的编码格式(例如 UTF-8)每个字符的字节数都不尽相同。

异常

Python使用被称为异常的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知 所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行; 如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。 异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告 诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行: 显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

使用 try-except 代码块

当你认为可能发生了错误时,可编写一个try-except代码来处理可能引发的异常。你让 Python尝试运行一些代码,并告诉它如果这些代码引发了指定的异常,该怎么办。

try 语句有两种主要形式: try-except 和 try-finally . 这两个语句是互斥的, 也就是说你 只能使用其中的一种. 一个 try 语句可以对应一个或多个 except 子句, 但只能对应一个 finally 子句, 或是一个 try-except-finally 复合语句.

处理ZeroDivisionError异常的try-except代码 类似于下面这样:

1
2
3
4
5
6
>>> try:
...     print(5/0)
... except ZeroDivisionError:
...     print("You can't divide by zero!")
...
You can't divide by zero!

我们将导致错误的代码行print(5/0)放在了一个try代码中。如果try代码中的代码运行起来没有问题,Python将跳过except代码; 如果try代码中的代码导致了错误,Python将查找这样的except代码块,并运行其中的代码,即其中指定的错误与引发的错误相同。 在这个示例中,try代码中的代码引发了ZeroDivisionError异常,因此Python指出了该如何解决问题的except代码块,并运行其中的代码。这样,用户看到的是一条 好的错误消息,而不是traceback:

1
You can't divide by zero!

如果try-except代码后面还有其他代码,程序将接着运行,因为已经告诉了Python如何处理这种错误。

使用异常避免崩溃

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 1 # coding=utf-8
 2 print("Give me two numbers, and I'll divide them.")
 3 print("Enter 'q' to quit.")
 4
 5 while True:
 6     first_number = input("\nFirst number: ")
 7     if first_number == 'q':
 8         break
 9     second_number = input("Second number: ")
10     if second_number == 'q':
11         break
12     answer = int(first_number) / int(second_number)
13     print(answer)

在第6行,这个程序提示用户输入一个数字,并将其存储到变量first_number中; 如果用户输入的不是表示退出的q,就再提示用户输入一个数字,并将其存储到变量second_number中(见第9行 )。 接下来,我们计算这两个数字的商 (即answer,见12行)。这个程序没有采取任何处理错误的措施,因此让它执行除数为0的除法运算时,它将崩溃:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 "<ipython-input-208-1e61f850d337>", line 11, in <module>
    answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
>>>

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

else 代码块

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  1 # coding=utf-8
  2 print("Give me two numbers, and I'll divide them.")
  3 print("Enter 'q' to quit.")
  4
  5 while True:
  6     first_number = input("\nFirst number: ")
  7     if first_number == 'q':
  8         break
  9     second_number = input("Second number: ")
 10     if second_number == 'q':
 11         break
 12     try:
 13         answer = int(first_number) / int(second_number)
 14     except ZeroDivisionError:
 15         print("You can't divide by 0!")
 16     else:
 17         print(answer)

我们让Python尝试执行try代码块中的除法运算(见12行),这个代码块只包含可能导致错误的代码。依赖于try代码块成功执行的代码都放在else代码中; 在这个示例中,如果除法运算成功,我们就使用else代码块来打印结果(见16行)。 except代码块告诉Python,出现ZeroDivisionError异常时该怎么办(见14行 )。如果try代码因除零错误而失败,我们就打印一条友好的消息,告诉用户如何避免这种错误。程序将继续运行,用户根本看不到traceback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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代码块中的代码时引发了指定的异常,该怎么办。 通过预测可能发生错误的代码,可编写健壮的程序,它们即便面临无效数据或缺少资源,也能继续运行,从而能够抵御无意的用户错误和恶意的攻击。

失败时不提示

使用 pass

决定报告那些错误

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

编写得很好且经过详尽测试的代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络连接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理 ,以及出现错误时该向用户提供多少相关的信息。

finally子句

finally 子句是无论异常是否发生,是否捕捉都会执行的一段代码. 你可以将 finally 仅仅配合 try 一起使用,也可以和 try-except(else 也是可选的)一起使用. 你可以用 finally 子句 与 try-except 或 try-except-else 一起使用.

下面是 try-except-else-finally 语法的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try: 
    A
except MyException1:
    B1
except MyException2:
    B2
else: 
    C
finally: 
    D

当然,无论如何,你都可以有不止一个的 except 子句,但最少有一个 except 语句,而 else 和 finally 都是可选的. A,B,C 和 D 是程序(代码块). 程序会按预期的顺序执行.(注意:可能的顺序是 A-C-D[正常]或 A-B-D[异常]).无论异常发生在 A,B,和/或 C 都将执行 finally 块.

常见的异常

异常 描述
AssertionError assert(断言)语句失败
AttributeError 试图访问一个对象没有的属性,比如foo.x ,但是foo没有x这个属性。
IOError 输入/输出异常,基本上是无法打开文件。
ImportError 无法引入模块或者包,基本上是路径问题
IndentationError 语法错误,代码没有正确对齐
IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]
KeyError 试图访问字典里不存在的键
KerboardInterrupt Ctrl + C 被按下
NameError 使用一个还未被赋值予对象的变量
SyntaxError Python代码非法,代码不能解释
TypeError 传入对象类型与要求的不符
UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另一个同名的全局变量,导致你以为正在访问它
ValueError 传入一个调用者不期望的值,即使值的类型是正确的