实际开发中常常会遇到对数据进行持久化的场景,所谓持久化是指将数据从无法长久保存数据的存储介质(通常是内存)转移到可以长久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的方式就是通过文件系统将数据保存到文件中。

计算机的文件系统是一种存储和组织计算机数据的方法,它使得对数据的访问和查找变得容易,文件系统使用文件树形目录的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,用户使用文件系统来保存数据时,不必关心数据实际保存在硬盘的哪个数据块上,只需要记住这个文件的路径和文件名。在写入新数据之前,用户不必关心硬盘上的哪个数据块没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。

打开和关闭文件

有了文件系统,我们可以非常方便的通过文件来读写数据;在Python中要实现文件操作是非常简单的。我们可以使用Python内置的open函数来打开文件,在使用open函数时,我们可以通过函数的参数指定文件名操作模式字符编码等信息,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件或二进制文件)以及做什么样的操作(读、写或追加),具体如下表所示。

操作模式 具体含义
'r' 读取 (默认)
'w' 写入(会先截断之前的内容)
'x' 写入,如果文件已经存在会产生异常
'a' 追加,将内容写入到已有文件的末尾
'b' 二进制模式
't' 文本模式(默认)
'+' 更新(既可以读又可以写)

下图展示了如何根据程序的需要来设置open函数的操作模式。
file-open-mode.png
在使用open函数时,如果打开的文件是字符文件(文本文件),可以通过encoding参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解,可以看看《字符集和字符编码》一文,此处不再进行赘述。如果没有指定该参数,则使用系统默认的编码作为读写文件的编码。当前系统默认的编码可以通过下面的代码获得。

  1. import sys
  2. print(sys.getdefaultencoding())

使用open函数打开文件成功后会返回一个文件对象,通过这个对象,我们就可以实现对文件的读写操作;如果打开文件失败,open函数会引发异常,稍后会对此加以说明。如果要关闭打开的文件,可以使用文件对象的close方法,这样可以在结束文件操作时释放掉这个文件。

读写文本文件

open函数打开文本文件时,需要指定文件名并将文件的操作模式设置为'r',如果不指定,默认值也是'r';如果需要指定字符编码,可以传入encoding参数,如果不指定,默认值是None,那么在读取文件时使用的是操作系统默认的编码。需要提醒大家,如果不能保证保存文件时使用的编码方式与encoding参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取文件失败。

下面的例子演示了如何读取一个纯文本文件(一般指只有字符原生编码构成的文件,与富文本相比,纯文本不包含字符样式的控制元素,能够被最简单的文本编辑器直接读取)。

  1. file = open('致橡树.txt', 'r', encoding='utf-8')
  2. print(file.read())
  3. file.close()

除了使用文件对象的read方法读取文件之外,还可以使用for-in循环逐行读取或者用readlines方法将文件按行读取到一个列表容器中,代码如下所示。

  1. import time
  2. file = open('致橡树.txt', 'r', encoding='utf-8')
  3. for line in file:
  4. print(line, end='')
  5. time.sleep(0.5)
  6. file.close()
  7. file = open('致橡树.txt', 'r', encoding='utf-8')
  8. lines = file.readlines()
  9. for line in lines:
  10. print(line, end='')
  11. time.sleep(0.5)
  12. file.close()

如果要向文件中写入内容,可以在打开文件时使用w或者a作为操作模式,前者会截断之前的文本内容写入新的内容,后者是在原来内容的尾部追加新的内容。

  1. file = open('致橡树.txt', 'a', encoding='utf-8')
  2. file.write('\n标题:《致橡树》')
  3. file.write('\n作者:舒婷')
  4. file.write('\n时间:1977年3月')
  5. file.close()

也可以使用下面的代码来完成相同的操作。

  1. lines = ['标题:《致橡树》', '作者:舒婷', '时间:1977年3月']
  2. file = open('致橡树.txt', 'a', encoding='utf-8')
  3. for line in lines:
  4. file.write(f'\n{line}')
  5. file.close()

异常处理机制

请注意上面的代码,如果open函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以使用Python的异常机制对可能在运行时发生状况的代码进行适当的处理。Python中和异常相关的关键字有五个,分别是tryexceptelsefinallyraise,我们先看看下面的代码,再来为大家介绍这些关键字的用法。

  1. file = None
  2. try:
  3. file = open('致橡树.txt', 'r', encoding='utf-8')
  4. print(file.read())
  5. except FileNotFoundError:
  6. print('无法打开指定的文件!')
  7. except LookupError:
  8. print('指定了未知的编码!')
  9. except UnicodeDecodeError:
  10. print('读取文件时解码错误!')
  11. finally:
  12. if file:
  13. 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中内置了大量的异常类型,除了上面代码中用到的异常类型以及之前的课程中遇到过的异常类型外,还有许多的异常类型,其继承结构如下所示。

  1. BaseException
  2. +-- SystemExit
  3. +-- KeyboardInterrupt
  4. +-- GeneratorExit
  5. +-- Exception
  6. +-- StopIteration
  7. +-- StopAsyncIteration
  8. +-- ArithmeticError
  9. | +-- FloatingPointError
  10. | +-- OverflowError
  11. | +-- ZeroDivisionError
  12. +-- AssertionError
  13. +-- AttributeError
  14. +-- BufferError
  15. +-- EOFError
  16. +-- ImportError
  17. | +-- ModuleNotFoundError
  18. +-- LookupError
  19. | +-- IndexError
  20. | +-- KeyError
  21. +-- MemoryError
  22. +-- NameError
  23. | +-- UnboundLocalError
  24. +-- OSError
  25. | +-- BlockingIOError
  26. | +-- ChildProcessError
  27. | +-- ConnectionError
  28. | | +-- BrokenPipeError
  29. | | +-- ConnectionAbortedError
  30. | | +-- ConnectionRefusedError
  31. | | +-- ConnectionResetError
  32. | +-- FileExistsError
  33. | +-- FileNotFoundError
  34. | +-- InterruptedError
  35. | +-- IsADirectoryError
  36. | +-- NotADirectoryError
  37. | +-- PermissionError
  38. | +-- ProcessLookupError
  39. | +-- TimeoutError
  40. +-- ReferenceError
  41. +-- RuntimeError
  42. | +-- NotImplementedError
  43. | +-- RecursionError
  44. +-- SyntaxError
  45. | +-- IndentationError
  46. | +-- TabError
  47. +-- SystemError
  48. +-- TypeError
  49. +-- ValueError
  50. | +-- UnicodeError
  51. | +-- UnicodeDecodeError
  52. | +-- UnicodeEncodeError
  53. | +-- UnicodeTranslateError
  54. +-- Warning
  55. +-- DeprecationWarning
  56. +-- PendingDeprecationWarning
  57. +-- RuntimeWarning
  58. +-- SyntaxWarning
  59. +-- UserWarning
  60. +-- FutureWarning
  61. +-- ImportWarning
  62. +-- UnicodeWarning
  63. +-- BytesWarning
  64. +-- ResourceWarning

从上面的继承结构可以看出,Python中所有的异常都是BaseException的子类型,它有四个直接的子类,分别是:SystemExitKeyboardInterruptGeneratorExitException。其中,SystemExit表示解释器请求退出,KeyboardInterrupt是用户中断程序执行(按下Ctrl+c),GeneratorExit表示生成器发生异常通知退出。值得一提的是Exception类,它是常规异常类型的父类型,很多的异常都是直接或间接的继承自Exception类。如果Python内置的异常类型不能满足应用程序的需要,我们可以自定义异常类型,而自定义的异常类型也应该直接或间接继承自Exception类,当然还可以根据需要重写或添加方法。

在Python中,可以使用raise关键字来引发异常(抛出异常对象),而调用者可以通过try...except...结构来捕获并处理异常。例如在函数中,当函数的执行条件不满足时,可以使用抛出异常的方式来告知调用者问题的所在,而调用者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。

  1. class InputError(ValueError):
  2. """自定义异常类型"""
  3. pass
  4. def fac(num):
  5. """求阶乘"""
  6. if type(num) != int or num < 0:
  7. raise InputError('只能计算非负整数的阶乘!!!')
  8. if num in (0, 1):
  9. return 1
  10. return num * fac(num - 1)

调用求阶乘的函数fac,通过try...except...结构捕获输入错误的异常并打印异常对象(显示异常信息),如果输入正确就计算阶乘并结束程序。

  1. flag = True
  2. while flag:
  3. num = int(input('n = '))
  4. try:
  5. print(f'{num}! = {fac(num)}')
  6. flag = False
  7. except InputError as err:
  8. print(err)

上下文语法

对于open函数返回的文件对象,还可以使用with上下文语法在文件操作完成后自动执行文件对象的close方法,这样可以让代码变得更加简单,因为不需要再写finally代码块来执行关闭文件释放资源的操作。需要提醒大家的是,并不是所有的对象都可以放在with上下文语法中,只有符合上下文管理器协议(有__enter____exit__魔术方法)的对象才能使用这种语法,Python标准库中的contextlib模块也提供了对with上下文语法的支持,后面再为大家进行讲解。

  1. try:
  2. with open('致橡树.txt', 'r', encoding='utf-8') as file:
  3. print(file.read())
  4. except FileExistsError:
  5. print("无法打开指定的文件!")
  6. except LookupError:
  7. print('指定了未知的编码!')
  8. except UnicodeDecodeError:
  9. print('读取文件时解码错误!')

读写二进制文件

读写二进制文件跟读写文本文件的操作类似,但是需要注意,在使用open函数打开文件时,如果要进行读操作,操作模式是'rb',如果要进行写操作,操作模式是'wb'。还有一点,读写文本文件时,read方法的返回值以及write方法的参数是str对象(字符串),而读写二进制文件时,read方法的返回值以及write方法的参数是bytes-like对象(字节串)。下面的代码实现了将当前路径下名为guido.jpg的图片文件复制到吉多.jpg文件中的操作。

  1. try:
  2. with open('guido.jpg', 'rb') as file1:
  3. data = file1.read()
  4. with open('吉多.jpg', 'wb') as file2:
  5. file2.write(data)
  6. except FileNotFoundError:
  7. print('指定的文件无法打开.')
  8. except IOError:
  9. print('读写文件时出现错误.')
  10. print('程序执行结束.')

如果要复制的图片文件很大,一次将文件内容直接读入内存中可能会造成非常大的内存开销,为了减少对内存的占用,可以为read方法传入size参数来指定每次读取的字节数,通过循环读取和写入的方式来完成上面的操作,代码如下所示。

  1. try:
  2. with open('guido.jpg', 'rb') as file1, \
  3. open('吉多.jpg', 'wb') as file2:
  4. data = file1.read(512)
  5. while data:
  6. file2.write(data)
  7. data = file1.read()
  8. except FileNotFoundError:
  9. print('指定的文件无法打开.')
  10. except IOError:
  11. print('读写文件时出现错误.')
  12. print('程序执行结束.')

对象的序列化和反序列化

读写JSON格式的数据

通过上面的讲解,我们已经知道如何将文本数据和二进制数据保存到文件中,那么这里还有一个问题,如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢?在Python中,我们可以将程序中的数据以JSON格式进行保存。JSON是“JavaScript Object Notation”的缩写,它本来是JavaScript语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨语言跨平台的数据交换。使用JSON的原因非常简单,因为它结构紧凑而且是纯文本,任何操作系统和编程语言都能处理纯文本,这就是实现跨语言跨平台数据交换的前提条件。目前JSON基本上已经取代了XML(可扩展标记语言)作为异构系统间交换数据的事实标准。可以在JSON的官方网站找到更多关于JSON的知识,这个网站还提供了每种语言处理JSON数据格式可以使用的工具或三方库。

下面是JSON格式的一个简单例子,大家可能已经注意到了,它跟Python中的字典非常类似而且支持嵌套结构,就像Python字典中的值还可以是字典,如果我们把下面的代码输入到浏览器控制台中,它会创建出一个JavaScript中的对象。

  1. {
  2. "name": "程不懂",
  3. "age": 25,
  4. "friends": ["刘备", "曹操"],
  5. "cars": [
  6. {"brand": "BMW", "max_speed": 240},
  7. {"brand": "Benz", "max_speed": 280},
  8. {"brand": "Audi", "max_speed": 280}
  9. ]
  10. }

JSON格式的数据类型和Python中的数据类型也是很容易找到对应关系的,正如下面的两张表所示。

JSON Python
object dict
array list
string str
number (int / real) int / float
true / false True / False
null None
Python JSON
dict object
list, tuple array
str string
int, float, int- & float-derived Enums number
True / False true / false
None null

在Python中,我们可以使用json模块将字典或列表以JSON格式写入到文件中,代码如下所示。

  1. import json
  2. my_dict = {
  3. 'name': '程不懂',
  4. 'age': 40,
  5. 'friends': ['刘备', '曹操'],
  6. 'car': [
  7. {'brand': 'BMW', 'max_speed': 240},
  8. {'brand': 'Audi', 'max_speed': 280},
  9. {'brand': 'tesla', 'max_speed': 260}
  10. ]
  11. }
  12. with open('data.json', 'w') as file:
  13. json.dump(my_dict, file)
  14. print('字典已经保存到data.json文件中')

执行上面的代码,会创建data.json文件,文件的内容如下所示,中文是用Unicode编码书写的。

  1. {"name": "\u7a0b\u4e0d\u61c2", "age": 40, "friends": ["\u5218\u5907", "\u66f9\u64cd"], "car": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "tesla", "max_speed": 260}]}

json模块有四个比较重要的函数,分别是

  • dump - 将Python对象按照JSON格式序列化到文件中
  • dumps - 将Python对象处理成JSON格式的字符串
  • load - 将文件中的JSON数据反序列化成对象
  • loads - 将字符串的内容反序列化成Python对象

这里出现了两个概念,一个叫序列化,一个叫反序列化,维基百科上的解释是:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”

我们可以通过下面的代码,从上面创建的data.json文件中读取JSON格式的数据并还原成字典。

  1. import json
  2. with open('data.json', 'r') as file:
  3. my_dict = json.load(file)
  4. print(type(my_dict))
  5. print(my_dict)

包管理工具pip的使用

Python标准库中的json模块在数据序列化和反序列化时性能并不是非常理想,为了解决这个问题,可以使用三方库ujson来替换json。所谓三方库,是指非公司内部开发和使用的,也不是来自于官方标准库的Python模块,这些模块通常由其他公司、组织或个人开发,所以被称为三方库。虽然Python语言的标准库虽然已经提供了诸多模块来方便我们的开发,但是对于一个强大的语言来说,它的生态圈一定也是非常繁荣的。

之前安装Python解释器时,默认情况下已经勾选了安装pip,大家可以在命令提示符或终端中通过pip --version来确定是否已经拥有了pip。pip是Python的包管理工具,通过pip可以查找、安装、卸载、更新Python的三方库或工具,macOS和Linux系统应该使用pip3。例如要安装替代json模块的ujson,可以使用下面的命令。

  1. pip install ujson

在默认情况下,pip会访问[https://pypi.org/simple/](https://pypi.org/simple/)来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网提供的镜像来替代这个默认的下载源,操作如下所示。

  1. pip install ujson -i https://pypi.doubanio.com/simple

可以通过pip search命令根据名字查找需要的三方库,可以通过pip list命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用pip install -Upip install --upgrade;如果要删除某个三方库,可以使用pip uninstall命令。
查看已经安装的三方库。

  1. pip list
  2. Package Version
  3. ----------------------------- ----------
  4. aiohttp 3.5.4
  5. alipay 0.7.4
  6. altgraph 0.16.1
  7. amqp 2.4.2
  8. ... ...

更新ujson三方库。

  1. pip install -U ujson -i https://pypi.doubanio.com/simple

如果要更新pip本身,可以使用下面的命令。
macOS系统:

  1. pip3 install -U pip

Windows系统:

  1. python -m pip install -U pip

删除ujson三方库。

  1. pip uninstall -y ujson

使用网络API获取数据

如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络API)都是基于HTTP提供JSON格式的数据,在Python程序中,我们可以发送HTTP请求给指定的URL(统一资源定位符),这个URL就是所谓的网络API,如果请求成功,它会返回HTTP响应,而HTTP响应的消息体中就有我们需要的JSON格式的数据。关于HTTP的相关知识,可以看看阮一峰的《HTTP协议入门》一文。

国内有很多提供网络API接口的网站,例如聚合数据阿凡达数据等,这些网站上有免费的和付费的数据接口,国外的{API}Search网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用[requests](https://link.zhihu.com/?target=http%3A//docs.python-requests.org/zh_CN/latest/)库(基于HTTP进行网络资源访问的三方库)访问网络API获取国内新闻并显示新闻标题和链接,这个例子使用了天行数据提供的国内新闻数据接口,其中的APIKey需要自己到网站上注册申请。

安装requests库。

  1. pip install requests -i https://pypi.doubanio.com/simple/

获取国内新闻并显示新闻标题和链接。

  1. import requests
  2. resp = requests.get('http://api.tianapi.com/guonei/?key=APIKey&num=10')
  3. if resp.status_code == 200:
  4. data_model = resp.json()
  5. for news in data_model['newslist']:
  6. print(news['title'])
  7. print(news['url'])
  8. print('-' * 60)

注意:上面代码中的APIKey需要换成自己在天行数据网站申请的APIKey,同时还要申请开通国内新闻的接口才能获取到JSON格式的数据。这个网站上还有很多非常有意思的网络API接口,例如:垃圾分类、美女图片、周公解梦等等,大家可以仿照上面的代码来调用这些接口。

简单的总结

通过读写文件的操作,我们可以实现数据持久化。在Python中可以通过open函数来获得文件对象,可以通过文件对象的readwrite方法实现文件读写操作。程序在运行时可能遭遇无法预料的异常状况,可以使用Python的异常机制来处理这些状况。Python的异常机制主要包括tryexceptelsefinallyraise这五个核心关键字。try后面的except语句不是必须的,finally语句也不是必须的,但是二者必须要有一个;except语句可以有一个或多个,多个except会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进入后续的except语句;except语句中还可以通过元组同时指定多个异常类型进行捕获;except语句后面如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使用raise要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。