Skip to content

Latest commit

 

History

History
1136 lines (816 loc) · 57.1 KB

File metadata and controls

1136 lines (816 loc) · 57.1 KB

七、文件和数据持久性

"Persistence is the key to the adventure we call life." – Torsten Alexander Lange

在前面的章节中,我们探讨了 Python 的几个不同方面。由于这些示例具有教学目的,我们在一个简单的 Python shell 中运行它们,或者以 Python 模块的形式运行它们。他们运行,可能在控制台上打印了一些东西,然后终止,没有留下他们短暂存在的痕迹。

然而,现实世界的应用程序通常有很大的不同。当然,它们仍然在内存中运行,但它们与网络、磁盘和数据库交互。它们还使用适合情况的格式与其他应用程序和设备交换信息。

在本章中,我们将通过探索以下内容开始接近现实世界:

  • 文件和目录
  • 压缩
  • 网络和流
  • JSON 数据交换格式
  • 使用标准库中的 pickle 和 shelve 实现数据持久化
  • 使用 SQLAlchemy 实现数据持久化

像往常一样,我将尝试平衡广度和深度,以便在本章结束时,您将牢牢掌握基本原理,并知道如何在 web 上获取更多信息。

使用文件和目录

谈到文件和目录,Python 提供了大量有用的工具。特别是在以下示例中,我们将利用osshutil模块。当我们在磁盘上读写时,我将使用一个文件fear.txt,其中包含 Thich Nhat Hanh 的恐惧摘录,作为我们的一些例子的实验对象。

打开文件

用 Python 打开文件非常简单直观。实际上,我们只需要使用open函数。让我们看一个简单的例子:

# files/open_try.py
fh = open('fear.txt', 'rt')  # r: read, t: text

for line in fh.readlines():
    print(line.strip())  # remove whitespace and print

fh.close()

前面的代码非常简单。我们调用open,传递文件名,并告诉open我们希望以文本模式读取它。文件名前没有路径信息;因此,open将假定该文件位于运行脚本的同一文件夹中。这意味着如果我们从files文件夹外运行此脚本,则不会找到fear.txt

一旦文件被打开,我们将获得一个文件对象fh,我们可以使用它处理文件的内容。在本例中,我们使用readlines()方法迭代文件中的所有行,并打印它们。我们在每行上调用strip(),以消除内容周围的任何额外空格,包括结尾的行终止字符,因为print已经为我们添加了一个空格。在本例中,这是一个快速而肮脏的解决方案,但如果文件内容包含需要保留的有意义的空格,则在清理数据时必须稍微小心一点。在脚本结束时,我们刷新并关闭流。

关闭一个文件是非常重要的,因为我们不想冒着无法释放该文件句柄的风险。因此,我们需要采取一些预防措施,并将前面的逻辑封装在一个try/finally块中。这样做的结果是,无论在我们尝试打开和读取文件时发生什么错误,我们都可以放心,close()将被调用:

# files/open_try.py
try:
    fh = open('fear.txt', 'rt')
    for line in fh.readlines():
        print(line.strip())
finally:
    fh.close()

逻辑完全相同,但现在也安全了。

Don't worry if you don't understand try/finally for now. We will explore how to deal with exceptions in the next chapter. For now, suffice to say that putting code within the body of a try block adds a mechanism around that code that allows us to detect errors (which are called exceptions) and decide what to do if they happen. In this case, we don't really do anything in case of errors, but by closing the file within the finally block, we make sure that line is executed whether or not any error has happened.

我们可以通过以下方式简化前面的示例:

# files/open_try.py
try:
    fh = open('fear.txt')  # rt is default
    for line in fh:  # we can iterate directly on fh
        print(line.strip())
finally:
    fh.close()

如您所见,rt是打开文件的默认模式,因此我们不需要指定它。此外,我们可以简单地迭代fh,而无需显式调用readlines()。Python 非常好,它为我们提供了使代码更短、更易于阅读的速记。

前面的所有示例都会在控制台上打印文件(请查看源代码以阅读全部内容):

An excerpt from Fear - By Thich Nhat Hanh

The Present Is Free from Fear

When we are not fully present, we are not really living. Were not really there, either for our loved ones or for ourselves. If were not there, then where are we? We are running, running, running, even during our sleep. We run because were trying to escape from our fear.
...

使用上下文管理器打开文件

让我们承认:必须用try/finally块来传播我们的代码的前景不是最好的。和往常一样,Python 为我们提供了一种更安全的方式来打开文件:使用 c上文本管理器。让我们先看看代码:

# files/open_with.py
with open('fear.txt') as fh:
    for line in fh:
        print(line.strip())

前面的示例与前面的示例相同,但读起来要好得多。with语句支持上下文管理器定义的运行时上下文的概念。这是使用一对方法__enter____exit__实现的,允许用户定义类定义在执行语句体之前输入的运行时上下文,并在语句结束时退出。open函数能够在上下文管理器调用时生成文件对象,但它的真正优点在于fh.close()将自动为我们调用,即使在出现错误的情况下。

上下文管理器用于几种不同的场景,例如线程同步、文件或其他对象的关闭以及网络和数据库连接的管理。您可以在contextlib文档页面(中找到关于它们的信息 https://docs.python.org/3.7/library/contextlib.html

读写文件

现在我们已经知道了如何打开文件,让我们看看读写文件的几种不同方式:

# files/print_file.py
with open('print_example.txt', 'w') as fw:
    print('Hey I am printing into a file!!!', file=fw)

第一种方法使用print函数,您在前面的章节中已经多次看到该函数。获取文件对象后,这一次指定我们打算写入它(“w”),我们可以告诉对print的调用将其效果直接作用于文件,而不是默认的sys.stdout,当在控制台上执行时,默认的sys.stdout被映射到该文件。

前面的代码的作用是,如果不存在print_example.txt文件,则创建该文件;如果存在,则截断该文件,并将第Hey I am printing into a file!!!行写入该文件。

这一切都很好,也很简单,但不是我们通常在想要写入文件时所做的。让我们看一个更常见的方法:

# files/read_write.py
with open('fear.txt') as f:
    lines = [line.rstrip() for line in f]

with open('fear_copy.txt', 'w') as fw:
    fw.write('\n'.join(lines))

在前面的示例中,我们首先打开fear.txt并将其内容逐行收集到一个列表中。请注意,这一次,我调用了一个更精确的方法,rstrip()作为示例,以确保只去掉每行右侧的空白。

在代码片段的第二部分中,我们创建了一个新文件fear_copy.txt,并将原始文件中的所有行写入其中,并用一个新行\n连接起来。Python 是优雅的,默认情况下与通用换行符一起工作,这意味着即使原始文件可能有一个不同于\n的换行符,它也会在返回该行之前为我们自动翻译。当然,这种行为是可定制的,但通常它正是您想要的。说到新词,你能想出其中一个可能在副本中丢失的吗?

二进制模式下的读写

请注意,通过在选项中打开一个通过t的文件(或者省略它,因为它是默认的),我们将以文本模式打开该文件。这意味着文件的内容被视为文本并被解释为文本。如果希望将字节写入文件,可以以二进制模式打开它。这是处理不包含原始文本的文件时的常见要求,例如图像、音频/视频,以及通常任何其他专有格式。

为了以二进制模式处理文件,只需在打开文件时指定b标志,如下例所示:

# files/read_write_bin.py
with open('example.bin', 'wb') as fw:
    fw.write(b'This is binary data...')

with open('example.bin', 'rb') as f:
    print(f.read())  # prints: b'This is binary data...'

在本例中,我仍然使用文本作为二进制数据,但它可以是您想要的任何内容。通过在输出中获得b'This ...'前缀,可以看出它被视为二进制。

防止覆盖现有文件

Python 使我们能够打开文件进行编写。通过使用w标志,我们打开一个文件并截断其内容。这意味着该文件将被一个空文件覆盖,原始内容将丢失。如果您只希望在文件不存在的情况下打开文件进行写入,则可以使用x标志,如下例所示:

# files/write_not_exists.py
with open('write_x.txt', 'x') as fw:
    fw.write('Writing line 1')  # this succeeds

with open('write_x.txt', 'x') as fw:
    fw.write('Writing line 2')  # this fails

如果运行前面的代码段,您将在目录中找到一个名为write_x.txt的文件,其中只包含一行文本。事实上,代码片段的第二部分无法执行。这是我在控制台上得到的输出:

$ python write_not_exists.py
Traceback (most recent call last):
 File "write_not_exists.py", line 6, in <module>
 with open('write_x.txt', 'x') as fw:
FileExistsError: [Errno 17] File exists: 'write_x.txt'

检查文件和目录是否存在

如果您想确保某个文件或目录存在(或不存在),则需要使用os.path模块。让我们看一个小例子:

# files/existence.py
import os

filename = 'fear.txt'
path = os.path.dirname(os.path.abspath(filename))

print(os.path.isfile(filename))  # True
print(os.path.isdir(path))  # True
print(path)  # /Users/fab/srv/lpp/ch7/files

前面的片段非常有趣。在使用相对引用声明文件名(因为它缺少路径信息)后,我们使用abspath计算文件的完整绝对路径。然后,我们通过调用dirname来获取路径信息(通过删除末尾的文件名)。如您所见,结果打印在最后一行。还要注意我们如何通过调用isfileisdir来检查文件和目录的存在性。在os.path模块中,您可以找到使用路径名所需的所有函数。

Should you ever need to work with paths in a different way, you can check out pathlib. While os.path works with strings, pathlib offers classes representing filesystem paths with semantics appropriate for different operating systems. It is beyond the scope of this chapter, but if you're interested, check out PEP428 (https://www.python.org/dev/peps/pep-0428/), and its page in the standard library.

操作文件和目录

让我们看几个关于如何操作文件和目录的快速示例。第一个示例操纵内容:

# files/manipulation.py
from collections import Counter
from string import ascii_letters

chars = ascii_letters + ' '

def sanitize(s, chars):
    return ''.join(c for c in s if c in chars)

def reverse(s):
    return s[::-1]

with open('fear.txt') as stream:
    lines = [line.rstrip() for line in stream]

with open('raef.txt', 'w') as stream:
    stream.write('\n'.join(reverse(line) for line in lines))

# now we can calculate some statistics
lines = [sanitize(line, chars) for line in lines]
whole = ' '.join(lines)
cnt = Counter(whole.lower().split())
print(cnt.most_common(3))

前面的示例定义了两个函数:sanitizereverse。它们是简单的函数,其目的是从字符串中删除任何非字母或空格的内容,并分别生成字符串的反向副本。

我们打开fear.txt并将其内容读入列表。然后我们创建一个新文件raef.txt,它将包含原始文件的水平镜像版本。我们通过一次操作写入lines的所有内容,在新行字符上使用join。也许更有趣的是,最后的一点。首先,通过列表理解,我们将lines重新分配给自身的净化版本。然后我们将它们放在whole字符串中,最后,我们将结果传递给Counter。请注意,我们将字符串拆分并用小写字母表示。这样,无论大小写如何,每个单词都将被正确计数,而且,由于split,我们不需要担心任何额外的空格。当我们打印出三个最常见的单词时,我们意识到 Thich Nhat Hanh 真正关注的是其他单词,因为we是文本中最常见的单词:

$ python manipulation.py
[('we', 17), ('the', 13), ('were', 7)]

现在我们来看一个更面向磁盘操作的操作示例,其中我们使用了shutil模块:

# files/ops_create.py
import shutil
import os

BASE_PATH = 'ops_example'  # this will be our base path
os.mkdir(BASE_PATH)

path_b = os.path.join(BASE_PATH, 'A', 'B')
path_c = os.path.join(BASE_PATH, 'A', 'C')
path_d = os.path.join(BASE_PATH, 'A', 'D')

os.makedirs(path_b)
os.makedirs(path_c)

for filename in ('ex1.txt', 'ex2.txt', 'ex3.txt'):
    with open(os.path.join(path_b, filename), 'w') as stream:
        stream.write(f'Some content here in {filename}\n')

shutil.move(path_b, path_d)

shutil.move(
    os.path.join(path_d, 'ex1.txt'),
    os.path.join(path_d, 'ex1d.txt')
)

在前面的代码中,我们首先声明一个基本路径,它将安全地包含我们要创建的所有文件和文件夹。然后我们使用makedirs创建两个目录:ops_example/A/Bops_example/A/C。(你能想出一种使用map创建这两个目录的方法吗?)。

我们使用os.path.join来连接目录名,因为使用/将专门化代码,使其在目录分隔符为/的平台上运行,但随后代码将在使用不同分隔符的平台上失败。让我们将任务委托给join来确定哪个是合适的分隔符。

创建目录后,在一个简单的for循环中,我们将一些代码放在目录B中创建三个文件。然后,我们将文件夹B及其内容移动到另一个名称:D。最后,我们将ex1.txt重命名为ex1d.txt。如果打开该文件,您将看到它仍然包含来自for循环的原始文本。对结果调用tree会产生以下结果:

$ tree ops_example/
ops_example/
└── A
 ├── C
 └── D
 ├── ex1d.txt
 ├── ex2.txt
 └── ex3.txt 

操纵路径名

让我们通过一个简单的例子来进一步探讨os.path的能力:

# files/paths.py
import os

filename = 'fear.txt'
path = os.path.abspath(filename)

print(path)
print(os.path.basename(path))
print(os.path.dirname(path))
print(os.path.splitext(path))
print(os.path.split(path))

readme_path = os.path.join(
    os.path.dirname(path), '..', '..', 'README.rst')
print(readme_path)
print(os.path.normpath(readme_path))

对于这个简单的例子,阅读结果可能是一个很好的解释:

/Users/fab/srv/lpp/ch7/files/fear.txt           # path
fear.txt                                        # basename
/Users/fab/srv/lpp/ch7/files                    # dirname
('/Users/fab/srv/lpp/ch7/files/fear', '.txt')   # splitext
('/Users/fab/srv/lpp/ch7/files', 'fear.txt')    # split
/Users/fab/srv/lpp/ch7/files/../../README.rst   # readme_path
/Users/fab/srv/lpp/README.rst                   # normalized

临时文件和目录

有时,在运行某些代码时能够创建临时目录或文件非常有用。例如,在编写影响磁盘的测试时,可以使用临时文件和目录来运行逻辑并断言它是正确的,并且确保在测试运行结束时,测试文件夹没有剩余内容。让我们看看如何在 Python 中实现这一点:

# files/tmp.py
import os
from tempfile import NamedTemporaryFile, TemporaryDirectory

with TemporaryDirectory(dir='.') as td:
    print('Temp directory:', td)
    with NamedTemporaryFile(dir=td) as t:
        name = t.name
        print(os.path.abspath(name))

前面的示例非常简单:我们在当前目录(“.”)中创建一个临时目录,并在其中创建一个命名的临时文件。我们打印文件名及其完整路径:

$ python tmp.py
Temp directory: ./tmpwa9bdwgo
/Users/fab/srv/lpp/ch7/files/tmpwa9bdwgo/tmp3d45hm46 

每次运行此脚本都会产生不同的结果。毕竟,这是我们在这里创建的临时随机名称,对吗?

目录内容

使用 Python,您还可以检查目录的内容。我将向您展示两种方法:

# files/listing.py
import os

with os.scandir('.') as it:
    for entry in it:
        print(
            entry.name, entry.path,
            'File' if entry.is_file() else 'Folder'
        )

此代码段使用在当前目录上调用的os.scandir。我们对结果进行迭代,每个结果都是os.DirEntry的一个实例,这是一个很好的类,公开了有用的属性和方法。在代码中,我们访问其中的一个子集:namepathis_file()。运行代码会产生以下结果(为了简洁起见,我省略了一些结果):

$ python listing.py
fixed_amount.py ./fixed_amount.py File
existence.py ./existence.py File
...
ops_example ./ops_example Folder
...

os.walk为我们提供了一种更强大的目录树扫描方法。让我们看一个例子:

# files/walking.py
import os

for root, dirs, files in os.walk('.'):
    print(os.path.abspath(root))
    if dirs:
        print('Directories:')
        for dir_ in dirs:
            print(dir_)
        print()
    if files:
        print('Files:')
        for filename in files:
            print(filename)
        print()

运行前面的代码段将生成当前代码段中所有文件和目录的列表,并对每个子目录执行相同的操作。

文件和目录压缩

在我们结束本节之前,让我给您一个如何创建压缩文件的示例。在本书的源代码中,我有两个示例:一个创建 ZIP 文件,另一个创建tar.gz文件。Python 允许您以几种不同的方式和格式创建压缩文件。在这里,我将向您展示如何创建最常见的一个 ZIP:

# files/compression/zip.py
from zipfile import ZipFile

with ZipFile('example.zip', 'w') as zp:
    zp.write('content1.txt')
    zp.write('content2.txt')
    zp.write('subfolder/content3.txt')
    zp.write('subfolder/content4.txt')

with ZipFile('example.zip') as zp:
    zp.extract('content1.txt', 'extract_zip')
    zp.extract('subfolder/content3.txt', 'extract_zip')

在前面的代码中,我们导入ZipFile,然后在上下文管理器中,我们向其中写入四个虚拟上下文文件(其中两个位于子文件夹中,以显示 ZIP 保留了完整路径)。然后,作为一个例子,我们打开压缩文件并从中提取几个文件,放入extract_zip目录。如果您有兴趣了解有关数据压缩的更多信息,请务必查看标准库(上的数据压缩和归档部分 https://docs.python.org/3.7/library/archiving.html ),在这里您可以了解有关此主题的所有信息。

数据交换格式

现代软件体系结构倾向于将一个应用程序分成几个组件。无论您采用面向服务的体系结构范式,还是将其进一步推向微服务领域,这些组件都必须交换数据。但是,即使您正在编写一个单片应用程序,其代码库包含在一个项目中,也有可能仍然需要与 API、其他程序交换数据,或者只是处理网站前端和后端部分之间的数据流,这很可能不会讲同一种语言。

选择正确的信息交换格式至关重要。特定于语言的格式的优点是,语言本身很可能为您提供使序列化和反序列化变得轻而易举的所有工具。但是,您将无法与使用同一语言的不同版本或完全使用不同语言编写的其他组件进行对话。不管未来会是什么样子,只有在特定情况下唯一可能的选择是使用特定语言的格式时,才应该这样做。

更好的方法是选择一种与语言无关的格式,并且可以被所有(或至少大多数)语言使用。在我领导的团队中,我们有来自英格兰、波兰、南非、西班牙、希腊、印度、意大利的人,仅举几例。我们都说英语,所以不管我们的母语是什么,我们都能相互理解(嗯……大部分是!)。

在软件世界,近年来一些流行的格式已经成为事实上的标准。最著名的可能是 XML、YAML 和 JSON。Python 标准库具有xmljson模块,并且在 PyPI(上)https://docs.python.org/3.7/library/archiving.html ),您可以找到一些不同的软件包与 YAML 一起使用。

在 Python 环境中,JSON 可能是最常用的。因为它是标准库的一部分,而且简单,所以它胜过其他两个。如果您曾经使用过 XML,您就知道它可能是一场噩梦。

使用 JSON

JSONJavaScript 对象表示法的首字母缩写,是 JavaScript 语言的子集。它已经存在了近二十年,所以它是众所周知的,基本上被所有语言广泛采用,尽管它实际上是独立于语言的。您可以在其网站(上阅读相关信息 https://www.json.org/ ),但我现在就给大家简单介绍一下。

JSON 基于两种结构:名称/值对的集合和值的有序列表。您将立即意识到这两个对象分别映射到 Python 中的 dictionary 和 list 数据类型。作为数据类型,它提供字符串、数字、对象和值,如 true、false 和 null。让我们看一个快速示例来开始:

# json_examples/json_basic.py
import sys
import json

data = {
    'big_number': 2 ** 3141,
    'max_float': sys.float_info.max,
    'a_list': [2, 3, 5, 7],
}

json_data = json.dumps(data)
data_out = json.loads(json_data)
assert data == data_out  # json and back, data matches

我们首先导入sysjson模块。然后我们创建一个简单的字典,里面有一些数字和一个列表。我想用非常大的数字来测试序列化和反序列化,包括intfloat,所以我把*23141*和我的系统能处理的最大浮点数放在一起。

我们使用json.dumps进行序列化,它获取数据并将其转换为 JSON 格式的字符串。然后,这些数据被输入到json.loads,而json.loads的作用正好相反:从 JSON 格式的字符串,它将数据重建为 Python。在最后一行,我们通过 JSON 确保原始数据和序列化/反序列化的结果匹配。

让我们看看,在下一个示例中,如果我们打印 JSON 数据,它会是什么样子:

# json_examples/json_basic.py
import json

info = {
    'full_name': 'Sherlock Holmes',
    'address': {
        'street': '221B Baker St',
        'zip': 'NW1 6XE',
        'city': 'London',
        'country': 'UK',
    }
}

print(json.dumps(info, indent=2, sort_keys=True))

在本例中,我们创建了一个包含福尔摩斯数据的字典。如果你像我一样,是福尔摩斯的粉丝,并且在伦敦,你会在那个地址找到他的博物馆(我建议你去参观,它虽小但很不错)。

注意我们如何称呼json.dumps。我们已经告诉它用两个空格缩进,并按字母顺序排列键。结果是:

$ python json_basic.py
{
 "address": {
 "city": "London",
 "country": "UK",
 "street": "221B Baker St",
 "zip": "NW1 6XE"
 },
 "full_name": "Sherlock Holmes"
}

与 Python 的相似性是巨大的。一个区别是,如果在字典中的最后一个元素上放置逗号,就像我在 Python 中所做的那样(按照惯例),JSON 会抱怨。

让我给你看一些有趣的东西:

# json_examples/json_tuple.py
import json

data_in = {
    'a_tuple': (1, 2, 3, 4, 5),
}

json_data = json.dumps(data_in)
print(json_data)  # {"a_tuple": [1, 2, 3, 4, 5]}
data_out = json.loads(json_data)
print(data_out)  # {'a_tuple': [1, 2, 3, 4, 5]}

在本例中,我们放置了一个元组,而不是列表。有趣的是,从概念上讲,元组也是一个有序的项列表。它没有列表的灵活性,但从 JSON 的角度来看,它仍然被认为是相同的。因此,正如第一个print所示,在 JSON 中,元组被转换为列表。很自然,元组的信息丢失了,当反序列化发生时,data_outa_tuple中的内容实际上是一个列表。在处理数据时,请务必记住这一点,因为在进行转换过程时,所涉及的格式仅包含可使用的数据结构的子集,这意味着将丢失信息。在本例中,我们丢失了有关类型的信息(元组与列表)。

这实际上是一个常见的问题。例如,您无法将所有 Python 对象序列化为 JSON,因为不清楚 JSON 是否应该(或如何)还原。比如,想想datetime。该类的一个实例是 JSON 不允许序列化的 Python 对象。如果我们将其转换为一个字符串,如2018-03-04T12:00:30Z,它是带有时间和时区信息的日期的 ISO 8601 表示形式,那么 JSON 在反序列化时应该做什么?如果它说的是,这实际上是反序列化到一个 DATETIME 对象,所以我最好做它?那么可以用多种方式解释的数据类型呢?

答案是,在处理数据交换时,我们通常需要先将对象转换为更简单的格式,然后再使用 JSON 序列化它们。这样,我们将知道在反序列化它们时如何正确地重构它们。

不过,在某些情况下,能够序列化自定义对象是很有用的,而且主要用于内部使用,因此,为了好玩,我将用两个示例向您展示如何序列化自定义对象:复数(因为我喜欢数学)和日期时间对象。

使用 JSON 自定义编码/解码

在 JSON Word 中,我们可以考虑像编码/解码这样的术语作为序列化/反序列化的同义词。它们基本上都意味着在 JSON 之间来回转换。在以下示例中,我将向您展示如何对复数进行编码:

# json_examples/json_cplx.py
import json

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, complex):
            return {
                '_meta': '_complex',
                'num': [obj.real, obj.imag],
            }
        return json.JSONEncoder.default(self, obj)

data = {
    'an_int': 42,
    'a_float': 3.14159265,
    'a_complex': 3 + 4j,
}

json_data = json.dumps(data, cls=ComplexEncoder)
print(json_data)

def object_hook(obj):
    try:
        if obj['_meta'] == '_complex':
            return complex(*obj['num'])
    except (KeyError, TypeError):
        return obj

data_out = json.loads(json_data, object_hook=object_hook)
print(data_out)

我们首先定义一个ComplexEncoder类,它需要实现default方法。此方法被传递给所有必须序列化的对象,一次一个,在obj变量中。在某个时刻,obj将是我们的复数,3+4j。如果这是真的,我们将返回一个包含一些自定义元信息的字典,以及一个包含数字的实部和虚部的列表。这就是我们需要做的,以避免丢失复数的信息。

然后我们调用json.dumps,但这次我们使用cls参数来指定自定义编码器。结果打印出来:

{"an_int": 42, "a_float": 3.14159265, "a_complex": {"_meta": "_complex", "num": [3.0, 4.0]}}

工作完成了一半。对于反序列化部分,我们可以编写另一个继承自JSONDecoder的类,但是,为了好玩,我使用了另一种更简单的技术并使用了一个小函数:object_hook

object_hook的体内,我们发现了另一个try块,但暂时不要担心。我将在下一章详细解释它。重要的部分是try块体内部的两条线。该函数接收一个对象(注意,只有当obj是一个字典时才会调用该函数),如果元数据与我们的复数约定相匹配,我们将实数部分和虚数部分传递给complex函数。try/except块只是为了防止格式错误的 JSON 破坏一方(如果发生这种情况,我们只需按原样返回对象)。

最后一次打印返回:

{'an_int': 42, 'a_float': 3.14159265, 'a_complex': (3+4j)}

您可以看到a_complex已正确反序列化。

现在让我们看一个稍微复杂一点的例子(没有双关语的意思):处理datetime对象。我将把代码分成两个部分,序列化部分和反序列化部分:

# json_examples/json_datetime.py
import json
from datetime import datetime, timedelta, timezone

now = datetime.now()
now_tz = datetime.now(tz=timezone(timedelta(hours=1)))

class DatetimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            try:
                off = obj.utcoffset().seconds
            except AttributeError:
                off = None

            return {
                '_meta': '_datetime',
                'data': obj.timetuple()[:6] + (obj.microsecond, ),
                'utcoffset': off,
            }
        return json.JSONEncoder.default(self, obj)

data = {
    'an_int': 42,
    'a_float': 3.14159265,
    'a_datetime': now,
    'a_datetime_tz': now_tz,
}

json_data = json.dumps(data, cls=DatetimeEncoder)
print(json_data)

这个例子稍微复杂一点的原因在于 Python 中的datetime对象可以是时区感知的,也可以不是;因此,我们需要更加小心。流程基本上与以前相同,只是它处理的是不同的数据类型。我们从获取当前日期和时间信息开始,在没有(now)和(now_tz)时区感知的情况下进行操作,以确保脚本正常工作。然后,我们继续像以前一样定义自定义编码器,并再次实现default方法。该方法中的重要部分是如何以秒为单位获取时区偏移量(off)信息,以及如何构造返回数据的字典。这一次,元数据表示这是一个日期时间信息,然后我们保存时间元组中的前六项(年、月、日、小时、分钟和秒),再加上data键中的微秒,以及之后的偏移量。你能告诉我data的值是元组的串联吗?如果可以的话,干得好!

当我们有了自定义编码器后,我们继续创建一些数据,然后进行序列化。print语句返回(在我做了一些修饰之后):

{
 "a_datetime": {
 "_meta": "_datetime",
 "data": [2018, 3, 18, 17, 57, 27, 438792],
 "utcoffset": null
 },
 "a_datetime_tz": {
 "_meta": "_datetime",
 "data": [2018, 3, 18, 18, 57, 27, 438810],
 "utcoffset": 3600
 },
 "a_float": 3.14159265,
 "an_int": 42
}

有趣的是,我们发现None被翻译成null,它的 JavaScript 等价物。此外,我们可以看到,我们的数据似乎已被正确编码。让我们进入脚本的第二部分:

# json_examples/json_datetime.py
def object_hook(obj):
    try:
        if obj['_meta'] == '_datetime':
            if obj['utcoffset'] is None:
                tz = None
            else:
                tz = timezone(timedelta(seconds=obj['utcoffset']))
            return datetime(*obj['data'], tzinfo=tz)
    except (KeyError, TypeError):
        return obj

data_out = json.loads(json_data, object_hook=object_hook)

再一次,我们首先验证元数据是否告诉我们它是一个datetime,然后继续获取时区信息。一旦我们有了它,我们将 7 元组(使用*解压调用中的值)和时区信息传递给datetime调用,返回原始对象。我们打印data_out进行验证:

{
 'a_datetime': datetime.datetime(2018, 3, 18, 18, 1, 46, 54693),
 'a_datetime_tz': datetime.datetime(
 2018, 3, 18, 19, 1, 46, 54711,
 tzinfo=datetime.timezone(datetime.timedelta(seconds=3600))),
 'a_float': 3.14159265,
 'an_int': 42
}

正如你所看到的,我们把所有东西都找对了。作为练习,我想挑战您编写相同的逻辑,但对于date对象,它应该更简单。

在我们进入下一个话题之前,请注意一点。也许这是违反直觉的,但使用datetime对象可能是最棘手的事情之一,因此,尽管我非常确定这段代码正在做它应该做的事情,但我想强调的是,我只是非常轻松地测试了它。所以,如果你打算抓住它并使用它,请彻底测试它。测试不同的时区,测试夏令时的开启和关闭,测试纪元之前的日期,等等。您可能会发现本节中的代码需要进行一些修改以适合您的情况。

现在让我们转到下一个主题,IO。

IO、流和请求

IO代表输入/输出,泛指计算机与外界的通信。有几种不同类型的 IO,对它们的解释超出了本章的范围,但我仍然想给大家举几个例子。

使用内存中的流

第一个将向您展示io.StringIO类,它是文本 IO 的内存流。第二种方法将避开计算机的本地性,并向您展示如何执行 HTTP 请求。让我们来看第一个例子:

# io_examples/string_io.py
import io

stream = io.StringIO()
stream.write('Learning Python Programming.\n')
print('Become a Python ninja!', file=stream)

contents = stream.getvalue()
print(contents)

stream.close()

在前面的代码片段中,我们从标准库导入了io模块。这是一个非常有趣的模块,具有许多与流和 IO 相关的工具。其中一个是StringIO,这是一个内存缓冲区,我们将在其中使用两种不同的方法编写两个句子,就像我们在本章第一个示例中对文件所做的那样。我们可以调用StringIO.write或使用print,并告诉它将数据定向到我们的流。

通过调用getvalue,我们可以获取流的内容(并打印它),最后关闭它。对close的调用导致文本缓冲区立即被丢弃。

有一种更优雅的方法来编写前面的代码(在看之前,你能猜到吗?)

# io_examples/string_io.py
with io.StringIO() as stream:
    stream.write('Learning Python Programming.\n')
    print('Become a Python ninja!', file=stream)
    contents = stream.getvalue()
    print(contents)

是的,它还是一个上下文管理器。与open类似,io.StringIO在上下文管理器块中工作良好。注意与open的相似之处:在这种情况下,我们也不需要手动关闭流。

内存中的对象在多种情况下都很有用。内存比磁盘快得多,对于少量数据来说,它是一个完美的选择。

运行脚本时,输出为:

$ python string_io.py
Learning Python Programming.
Become a Python ninja!

发出 HTTP 请求

现在,让我们研究几个关于 HTTP 请求的示例。对于这些示例,我将使用requests库,您可以使用pip进行安装。我们将对httpbin.orgAPI 执行 HTTP 请求,有趣的是,该 API 由requests库本身的创建者 Kenneth Reitz 开发。该库是世界上使用最广泛的库之一:

import requests

urls = {
    'get': 'https://httpbin.org/get?title=learn+python+programming',
    'headers': 'https://httpbin.org/headers',
    'ip': 'https://httpbin.org/ip',
    'now': 'https://now.httpbin.org/',
    'user-agent': 'https://httpbin.org/user-agent',
    'UUID': 'https://httpbin.org/uuid',
}

def get_content(title, url):
    resp = requests.get(url)
    print(f'Response for {title}')
    print(resp.json())

for title, url in urls.items():
    get_content(title, url)
    print('-' * 40)

前面的代码片段应该简单易懂。我声明了一个 URL 字典,我想对其执行requests。我已经将执行请求的代码封装到一个小函数中:get_content。正如您所看到的,非常简单,我们执行一个 GET 请求(通过使用requests.get),然后打印响应主体的标题和 JSON 解码版本。让我花点时间谈谈这最后一点。

当我们对网站或 API 执行请求时,我们会返回一个响应对象,很简单,就是我们执行请求的服务器返回的对象。来自httpbin.org的所有响应的主体恰好是 JSON 编码的,因此,我们不需要获取主体(通过获取resp.text并手动解码,调用json.loads,而是通过在响应对象上利用json方法将两者结合起来。requests套餐之所以被如此广泛地采用,有很多原因,其中之一就是它的易用性。

现在,当您在应用程序中执行请求时,您需要一种更健壮的方法来处理错误等,但在本章中,一个简单的示例就可以了。别担心,我会在第 14 章网站开发中更全面地介绍 HTTP 请求。

回到我们的代码,最后,我们运行一个for循环并获取所有 URL。当您运行它时,您将在控制台上看到每个调用的结果,如下所示(为了简洁起见,经过修饰和修剪):

$ python reqs.py
Response for get
{
  "args": {
    "title": "learn python programming"
  },
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "close",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.19.0"
  },
  "origin": "82.47.175.158",
  "url": "https://httpbin.org/get?title=learn+python+programming"
}
... rest of the output omitted ... 

请注意,您可能会在版本号和 IP 方面获得稍微不同的输出,这很好。现在,GET 只是 HTTP 动词之一,它肯定是最常用的。第二种是无处不在的 POST,这是您需要向服务器发送数据时发出的请求类型。每次你在网上提交一份表格,你基本上就是在提出一个 POST 请求。因此,让我们尝试以编程方式制作一个:

# io_examples/reqs_post.py
import requests

url = 'https://httpbin.org/post'
data = dict(title='Learn Python Programming')

resp = requests.post(url, data=data)
print('Response for POST')
print(resp.json())

前面的代码与我们之前看到的代码非常相似,只是这次我们不调用get,而是调用post,因为我们想发送一些数据,所以我们在调用中指定了它。requests库提供的远不止这些,它还因其公开的漂亮 API 而受到社区的赞扬。这是一个项目,我鼓励你检查和探索,因为你最终将使用它的所有时间,无论如何。

运行前面的脚本(并对输出应用一些美化魔法)会产生以下结果:

$ python reqs_post.py
Response for POST
{ 'args': {},
 'data': '',
 'files': {},
 'form': {'title': 'Learn Python Programming'},
 'headers': { 'Accept': '*/*',
 'Accept-Encoding': 'gzip, deflate',
 'Connection': 'close',
 'Content-Length': '30',
 'Content-Type': 'application/x-www-form-urlencoded',
 'Host': 'httpbin.org',
 'User-Agent': 'python-requests/2.7.0 CPython/3.7.0b2 '
 'Darwin/17.4.0'},
 'json': None,
 'origin': '82.45.123.178',
 'url': 'https://httpbin.org/post'}

注意现在的头是如何不同的,我们在响应主体的form键/值对中找到了发送的数据。

我希望这些简短的例子足以让您开始,尤其是在请求方面。网络每天都在变化,因此值得学习基础知识,然后时不时地复习。

现在让我们转到本章的最后一个主题:以不同格式在磁盘上持久化数据。

在磁盘上持久化数据

在本章的最后一节中,我们将探讨如何以三种不同的格式在磁盘上保存数据。我们将探讨pickleshelve以及一个简短的示例,该示例将涉及使用 SQLAlchemy(Python 生态系统中最广泛采用的 ORM 库)访问数据库。

用 pickle 序列化数据

来自 Python 标准库的pickle模块提供了将 Python 对象转换为字节流的工具,反之亦然。尽管picklejson公开的 API 中存在部分重叠,但两者却截然不同。正如我们在本章前面所看到的,JSON 是一种文本格式,可读性强,与语言无关,并且只支持 Python 数据类型的有限子集。另一方面,pickle模块不是人类可读的,可以转换为字节,是特定于 Python 的,并且,由于出色的 Python 内省功能,它支持大量的数据类型。

不管这些差异是什么,当你考虑是否使用一个或另一个时,你应该知道,我认为最重要的关注在于,当你使用它时,暴露在你身上的安全威胁。因此,如果您决定在应用程序中采用它,您需要格外小心。

也就是说,让我们通过一个简单的例子来看看它的实际效果:

# persistence/pickler.py
import pickle
from dataclasses import dataclass

@dataclass
class Person:
    first_name: str
    last_name: str
    id: int

    def greet(self):
        print(f'Hi, I am {self.first_name} {self.last_name}'
              f' and my ID is {self.id}'
        )

people = [
    Person('Obi-Wan', 'Kenobi', 123),
    Person('Anakin', 'Skywalker', 456),
]

# save data in binary format to a file
with open('data.pickle', 'wb') as stream:
    pickle.dump(people, stream)

# load data from a file
with open('data.pickle', 'rb') as stream:
    peeps = pickle.load(stream)

for person in peeps:
    person.greet()

在前面的示例中,我们使用dataclass装饰器创建了一个Person类,如第 6 章OOP、装饰器和迭代器中所示。我用一个数据类编写这个示例的唯一原因是向您展示pickle如何轻松地处理它,而不需要我们做任何对于更简单的数据类型不会做的事情。

该类有三个属性:first_namelast_nameid。它还公开了一个greet方法,该方法只需打印一条包含数据的 hello 消息。

我们创建一个实例列表,然后将其保存到一个文件中。为了做到这一点,我们使用了pickle.dump,我们将要腌制的内容输入到pickle.dump,以及我们要写入的流。紧接着,我们从同一个文件中读取,并通过使用pickle.load,将该流的全部内容转换回 Python。为了确保对象已正确转换,我们对它们都调用了greet方法。结果如下:

$ python pickler.py
Hi, I am Obi-Wan Kenobi and my ID is 123
Hi, I am Anakin Skywalker and my ID is 456 

pickle模块还允许您通过dumpsloads函数(注意两个名称末尾的s来转换字节对象)。在日常应用程序中,pickle通常在我们需要持久化不应该与其他应用程序交换的 Python 数据时使用。我最近偶然发现的一个例子是flask插件中的会话管理,它在将会话对象发送到 Redis 之前对其进行 pickle 处理。但是,实际上,您不太可能经常处理这个库。

另一个可能使用更少,但在资源短缺时非常有用的工具是shelve

使用搁置保存数据

shelf是一个持久的类似字典的对象。它的美妙之处在于,您保存到shelf中的值可以是您可以保存到pickle中的任何对象,因此您不会像使用数据库那样受到限制。shelve模块虽然有趣且有用,但在实践中很少使用。为了完整起见,让我们看一个简单的例子来说明它是如何工作的:

# persistence/shelf.py
import shelve

class Person:
    def __init__(self, name, id):
        self.name = name
        self.id = id

with shelve.open('shelf1.shelve') as db:
    db['obi1'] = Person('Obi-Wan', 123)
    db['ani'] = Person('Anakin', 456)
    db['a_list'] = [2, 3, 5]
    db['delete_me'] = 'we will have to delete this one...'

    print(list(db.keys()))  # ['ani', 'a_list', 'delete_me', 'obi1']

    del db['delete_me']  # gone!

    print(list(db.keys()))  # ['ani', 'a_list', 'obi1']

    print('delete_me' in db)  # False
    print('ani' in db)  # True

    a_list = db['a_list']
    a_list.append(7)
    db['a_list'] = a_list
    print(db['a_list'])  # [2, 3, 5, 7]

除了布线和周围的样板之外,前面的示例类似于一个使用字典的练习。我们创建一个简单的Person类,然后在上下文管理器中打开一个shelve文件。如您所见,我们使用字典语法存储四个对象:两个Person实例、一个列表和一个字符串。如果我们打印keys,,我们会得到一个包含我们使用的四个键的列表。打印完成后,我们立即从书架上删除(恰当命名的)delete_me键/值对。再次打印keys表示删除成功。然后,我们测试了几个成员资格密钥,最后,我们将编号7附加到a_list。请注意,我们必须从工具架中提取列表,修改它,然后再次保存它。

如果这种行为不受欢迎,我们可以采取以下措施:

# persistence/shelf.py
with shelve.open('shelf2.shelve', writeback=True) as db:
    db['a_list'] = [11, 13, 17]
    db['a_list'].append(19)  # in-place append!
    print(db['a_list'])  # [11, 13, 17, 19]

通过使用writeback=True打开工具架,我们启用了writeback功能,这允许我们简单地附加到a_list,就好像它实际上是一个普通字典中的值一样。默认情况下,此功能不处于活动状态的原因是,您需要为内存消耗和较慢的书架关闭速度付出代价。

现在我们已经向与数据持久性相关的标准库模块表示敬意,让我们来看看 Python 生态系统中最广泛采用的 ORM:{ ToLt0} SqLalChany OutT1。

将数据保存到数据库

在本例中,我们将使用内存中的数据库,这将使事情变得更简单。在本书的源代码中,我留下了一些注释,向您展示了如何生成 SQLite 文件,因此我希望您也能探索这个选项。

You can find a free database browser for SQLite at sqlitebrowser.org. If you are not satisfied with it, you will be able to find a wide range of tools, some free, some not free, that you can use to access and manipulate a database file.

在深入研究代码之前,请允许我简要介绍关系数据库的概念。

关系数据库是一种允许您按照 Edgar F.Codd 于 1969 年发明的关系模型保存数据的数据库。在此模型中,数据存储在一个或多个表中。每个表都有行(也称为记录元组,每个行表示表中的一个条目。表也有列(也称为属性,每个列表示记录的一个属性。每个记录都通过一个唯一的键来标识,通常称为主键,它是表中一个或多个列的并集。举个例子:想象一个名为Users的表,其中有idusernamepasswordnamesurname列。这样一个表格将非常适合容纳我们系统的用户。每一行代表一个不同的用户。例如,值为3gianchubmy_wonderful_pwdFabrizioRomano的行将代表我在系统中的用户。

该模型之所以被称为关系型,是因为您可以在表之间建立关系。例如,如果您将一个名为PhoneNumbers的表添加到我们虚构的数据库中,您可以在其中插入电话号码,然后通过关系确定哪个电话号码属于哪个用户。

为了查询关系数据库,我们需要一种特殊的语言。主要标准称为SQL,代表结构化查询语言。它诞生于所谓的关系代数,这是一个非常好的代数家族,用于根据关系模型对存储的数据进行建模,并对其执行查询。您可以执行的最常见操作通常包括对行或列进行筛选、连接表、根据某些条件聚合结果等。以英语为例,对我们想象中的数据库的查询可以是:获取用户名以“m”开头的所有用户(用户名、姓名、姓氏),这些用户最多有一个电话号码。在这个查询中,我们要求得到User表中列的子集。我们只对用户名以字母m开头的用户进行过滤,更进一步,只对最多有一个电话号码的用户进行过滤。

Back in the days when I was a student in Padova, I spent a whole semester learning both the relational algebra semantics, and the standard SQL (amongst other things). If it wasn't for a major bicycle accident I had the day of the exam, I would say that this was one of the most fun exams I ever had to prepare.

现在,每个数据库都有自己的风格的SQL。他们都在一定程度上尊重这一标准,但没有一个完全尊重这一标准,而且他们在某些方面都有所不同。这在现代软件开发中提出了一个问题。如果我们的应用程序包含 SQL 代码,那么很可能如果我们决定使用不同的数据库引擎,或者同一引擎的不同版本,我们会发现我们的 SQL 代码需要修改。

这可能会非常痛苦,特别是因为 SQL 查询可能会很快变得非常复杂。为了稍微减轻这种痛苦,计算机科学家(保佑他们)已经创建了将特定语言的对象映射到关系数据库表的代码。毫不奇怪,这些工具的名称是对象关系映射ORMs)。

在现代应用程序开发中,您通常会使用 ORM 开始与数据库交互,如果您发现自己无法通过 ORM 执行需要执行的查询,那么您将直接使用 SQL。这是一个很好的折衷方案,既不使用 SQL,也不使用 ORM,这最终意味着专门化与数据库交互的代码,但有上述缺点。

在本节中,我将展示一个利用 SQLAlchemy(最流行的 Python ORM)的示例。我们将定义两个模型(PersonAddress,分别映射到一个表,然后我们将填充数据库并对其执行一些查询。

让我们从模型声明开始:

# persistence/alchemy_models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
    Column, Integer, String, ForeignKey, create_engine)
from sqlalchemy.orm import relationship

首先,我们导入一些函数和类型。我们需要做的第一件事就是创建一个引擎。此引擎告诉 SQLAlchemy 我们为示例选择的数据库类型:

# persistence/alchemy_models.py
engine = create_engine('sqlite:///:memory:')
Base = declarative_base()

class Person(Base):
    __tablename__ = 'person'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    age = Column(Integer)

    addresses = relationship(
        'Address',
        back_populates='person',
        order_by='Address.email',
        cascade='all, delete-orphan'
    )

    def __repr__(self):
        return f'{self.name}(id={self.id})'

class Address(Base):
    __tablename__ = 'address'

    id = Column(Integer, primary_key=True)
    email = Column(String)
    person_id = Column(ForeignKey('person.id'))
    person = relationship('Person', back_populates='addresses')

    def __str__(self):
        return self.email
    __repr__ = __str__

Base.metadata.create_all(engine)

然后,每个模型继承自Base表,在本例中,该表仅由declarative_base()返回的默认值组成。我们定义了Person,它映射到一个名为person的表,并公开了属性idnameage。我们还声明了与Address模型的关系,通过声明访问addresses属性将获取address表中与我们正在处理的特定Person实例相关的所有条目。cascade选项会影响创建和删除的工作方式,但这是一个更高级的概念,因此我建议您暂时使用它,以后可能会进行更多的研究。

我们声明的最后一件事是__repr__方法,它为我们提供了对象的正式字符串表示形式。这应该是一种可以用来完全重建对象的表示形式,但在本例中,我只是使用它在输出中提供一些内容。Python 将repr(obj)重定向到对obj.__repr__()的调用。

我们还声明了Address模型,该模型将包含电子邮件地址以及对其所属人员的引用。您可以看到,person_idperson属性都是关于设置AddressPerson实例之间的关系。注意我是如何在Address上声明__str__方法的,然后为其分配了一个别名,名为__repr__。这意味着在Address对象上同时调用reprstr将最终导致调用__str__方法。这是 Python 中非常常见的技术,因此我借此机会在这里向您展示了它。

在最后一行,我们告诉引擎根据我们的模型在数据库中创建表。

深入理解这段代码需要的空间远远超出我的承受能力,因此我鼓励您阅读数据库管理系统DBMS)、SQL、关系代数和 SQLAlchemy。

现在我们有了模型,让我们用它们来保存一些数据!

让我们来看看下面的例子:

# persistence/alchemy.py
from alchemy_models import Person, Address, engine
from sqlalchemy.orm import sessionmaker

Session = sessionmaker(bind=engine)
session = Session()

首先,我们创建会话,它是我们用来管理数据库的对象。接下来,我们继续创建两个人:

anakin = Person(name='Anakin Skywalker', age=32)
obi1 = Person(name='Obi-Wan Kenobi', age=40)

然后,我们使用两种不同的技术将电子邮件地址添加到这两个地址中。一个将它们分配给列表,另一个只是将它们添加到列表中:

obi1.addresses = [
    Address(email='obi1@example.com'),
    Address(email='wanwan@example.com'),
]

anakin.addresses.append(Address(email='ani@example.com'))
anakin.addresses.append(Address(email='evil.dart@example.com'))
anakin.addresses.append(Address(email='vader@example.com'))

我们还没有接触数据库。只有当我们使用 session 对象时,它才会发生实际的事情:

session.add(anakin)
session.add(obi1)
session.commit()

添加两个Person实例就足以添加它们的地址(这要感谢级联效应)。调用commit实际上是告诉 SQLAlchemy 提交事务并将数据保存在数据库中。事务是在数据库上下文中提供类似沙箱的操作。只要事务尚未提交,我们就可以回滚对数据库所做的任何修改,并通过这样做,恢复到启动事务之前的状态。SQLAlchemy 提供了更复杂、更精细的处理事务的方法,您可以在其官方文档中研究,因为这是一个相当高级的话题。现在,我们使用like查询所有名字以Obi开头的人,它与 SQL*:*中的LIKE操作符挂钩

obi1 = session.query(Person).filter(
    Person.name.like('Obi%')
).first()
print(obi1, obi1.addresses)

我们获取该查询的第一个结果(我们知道我们只有欧比万),然后打印它。然后,我们通过在他的名字上使用精确匹配来获取anakin(只是为了向您展示一种不同的过滤方式):

anakin = session.query(Person).filter(
    Person.name=='Anakin Skywalker'
).first()
print(anakin, anakin.addresses)

然后我们捕获阿纳金的 ID,并从全局帧中删除anakin对象:

anakin_id = anakin.id
del anakin

我们这样做的原因是,我想向您展示如何通过 ID 获取对象。在这样做之前,我们编写了display_info函数,我们将使用它来显示数据库的全部内容(从地址开始获取,以便演示如何通过使用 SQLAlchemy 中的关系属性获取对象):

def display_info():
    # get all addresses first
    addresses = session.query(Address).all()

    # display results
    for address in addresses:
        print(f'{address.person.name} <{address.email}>')

    # display how many objects we have in total
    print('people: {}, addresses: {}'.format(
        session.query(Person).count(),
        session.query(Address).count())
    )

display_info函数打印所有地址以及相应人员的姓名,最后生成关于数据库中对象数量的最后一条信息。我们调用函数,然后获取并删除anakin(想想达斯维德,删除他你就不会难过了),然后我们再次显示信息,以验证他是否真的从数据库中消失了:

display_info()

anakin = session.query(Person).get(anakin_id)
session.delete(anakin)
session.commit()

display_info()

所有这些代码段一起运行的输出如下(为了方便起见,我将输出分为四个块,以反映实际生成该输出的四个代码块):

$ python alchemy.py
Obi-Wan Kenobi(id=2) [obi1@example.com, wanwan@example.com] 
Anakin Skywalker(id=1) [ani@example.com, evil.dart@example.com, vader@example.com]
 Anakin Skywalker <ani@example.com>
Anakin Skywalker <evil.dart@example.com>
Anakin Skywalker <vader@example.com>
Obi-Wan Kenobi <obi1@example.com>
Obi-Wan Kenobi <wanwan@example.com>
people: 2, addresses: 5
 Obi-Wan Kenobi <obi1@example.com>
Obi-Wan Kenobi <wanwan@example.com>
people: 1, addresses: 2

从最后两个块中可以看到,删除anakin已经删除了一个Person对象,以及与之相关的三个地址。同样,这是由于我们删除anakin时发生了级联。

我们对数据持久性的简要介绍到此结束。这是一个广阔的、有时是复杂的领域,我鼓励你们尽可能多地探索学习理论。当涉及到数据库系统时,缺乏知识或正确的理解确实会造成伤害。

总结

在本章中,我们探讨了如何使用文件和目录。我们已经学习了如何打开文件进行读写,以及如何通过使用上下文管理器更优雅地进行读写。我们还探讨了目录:如何以递归方式和非递归方式列出其内容。我们还了解了路径名,它是访问文件和目录的网关。

然后,我们简要了解了如何创建 ZIP 存档并提取其内容。本书的源代码还包含一个不同压缩格式的示例:tar.gz

我们讨论了数据交换格式,并深入探讨了 JSON。我们在为特定 Python 数据类型编写自定义编码器和解码器时玩得很开心。

然后我们研究了 IO,包括内存流和 HTTP 请求。

最后,我们看到了如何使用pickleshelve和 SQLAlchemy ORM 库持久化数据。

您现在应该对如何处理文件和数据持久性有了很好的了解,我希望您能花时间亲自深入探讨这些主题。

下一章将介绍测试、分析和异常处理