Python 动态属性和特性(使用动态属性转换数据)
使用动态属性转换数据
在接下来的几个示例中,我们要使用动态属性处理 O’Reilly 为 OSCON
2014 大会提供的 JSON 格式数据源。示例 19-1 是那个数据源中的 4 个
记录。
示例 19-1 osconfeed.json 文件中的记录示例;节略了部分字段的
内容
{ "Schedule":
{ "conferences": [{"serial": 115 }],
"events": [
{ "serial": 34505,
"name": "Why Schools Don´t Use Open Source to Teach Programming",
"event_type": "40-minute conference session",
"time_start": "2014-07-23 11:30:00",
"time_stop": "2014-07-23 12:10:00",
"venue_serial": 1462,
"description": "Aside from the fact that high school programming...",
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
"speakers": [157509],
"categories": ["Education"] }
],
"speakers": [
{ "serial": 157509,
"name": "Robert Lefkowitz",
"photo": null,
"url": "http://sharewave.com/",
"position": "CTO",
"affiliation": "Sharewave",
"twitter": "sharewaveteam",
"bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." }
],
"venues": [
{ "serial": 1462,
"name": "F151",
"category": "Conference Venues" }
]
}
}
那个 JSON 源中有 895 条记录,示例 19-1 只列出了 4 条。可以看出,整
个数据集是一个 JSON 对象,里面有一个键,名为 “Schedule”;这个
键对应的值也是一个映像,有 4 个键:
“conferences”、“events”、“speakers” 和 “venues”。这 4 个键对
应的值都是一个记录列表。在示例 19-1 中,各个列表中只有一条记
录。然而,在完整的数据集中,列表中有成百上千条记录。不
过,“conferences” 键对应的列表中只有一条记录,如上述示例所
示。这 4 个列表中的每个元素都有一个名为 “serial” 的字段,这是元
素在各个列表中的唯一标识符。
我编写的第一个脚本只用于下载那个 OSCON 数据源。为了避免浪费流
量,我会先检查本地有没有副本。这么做是合理的,因为 OSCON 2014
大会已经结束,数据源不会再更新。
示例 19-2 没用到元编程,几乎所有代码的作用可以用这一个表达式概
括:json.load(fp)。不过,这样足以处理那个数据集
了。osconfeed.load 函数会在后面几个示例中用到。
示例 19-2 osconfeed.py:下载 osconfeed.json(doctest 在示例 19-3
中)
from urllib.request import urlopen
import warnings
import os
import json
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'data/osconfeed.json'
def load():if not os.path.exists(JSON):msg = 'downloading {} to {}'.format(URL, JSON)warnings.warn(msg) ➊with urlopen(URL) as remote, open(JSON, 'wb') as local: ➋local.write(remote.read())with open(JSON) as fp:return json.load(fp) ➌
❶ 如果需要下载,就发出提醒。
❷ 在 with 语句中使用两个上下文管理器(从 Python 2.7 和 Python 3.1
起允许这么做),分别用于读取和保存远程文件。
❸ json.load 函数解析 JSON 文件,返回 Python 原生对象。在这个数
据源中有这几种数据类型:dict、list、str 和 int。
有了示例 19-2 中的代码,我们可以审查数据源中的任何字段,如示例
19-3 所示。
示例 19-3 osconfeed.py:示例 19-2 的 doctest
>>> feed = load() ➊
>>> sorted(feed['Schedule'].keys()) ➋
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed['Schedule'].items()):
... print('{:3} {}'.format(len(value), key)) ➌
...
1 conferences
494 events
357 speakers
53 venues
>>> feed['Schedule']['speakers'][-1]['name'] ➍
'Carina C. Zona'
>>> feed['Schedule']['speakers'][-1]['serial'] ➎
141590
>>> feed['Schedule']['events'][40]['name']
'There *Will* Be Bugs'
>>> feed['Schedule']['events'][40]['speakers'] ➏
[3471, 5199]
❶ feed 的值是一个字典,里面嵌套着字典和列表,存储着字符串和整
数。
❷ 列出 “Schedule” 键中的 4 个记录集合。
❸ 显示各个集合中的记录数量。
❹ 深入嵌套的字典和列表,获取最后一个演讲者的名字。
❺ 获取那位演讲者的编号。
❻ 每个事件都有一个 ‘speakers’ 字段,列出 0 个或多个演讲者的编
号。
使用动态属性访问JSON类数据
示例 19-2 十分简单,不过,feed[‘Schedule’][‘events’][40]
[‘name’] 这种句法很冗长。在 JavaScript 中,可以使用
feed.Schedule.events[40].name 获取那个值。在 Python 中,可以
实现一个近似字典的类(网上有大量实现) ,达到同样的效果。我自
己实现了 FrozenJSON 类,比大多数实现都简单,因为只支持读取,即
只能访问数据。不过,这个类能递归,自动处理嵌套的映射和列表。
示例 19-4 演示 FrozenJSON 类的用法,源代码在示例 19-5 中。
示例 19-4 示例 19-5 定义的 FrozenJSON 类能读取属性,如
name,还能调用方法,如 .keys() 和 .items()
>>> from osconfeed import load
>>> raw_feed = load()
>>> feed = FrozenJSON(raw_feed) ➊
>>> len(feed.Schedule.speakers) ➋
357
>>> sorted(feed.Schedule.keys()) ➌
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed.Schedule.items()): ➍
... print('{:3} {}'.format(len(value), key))
...
1 conferences
494 events
357 speakers
53 venues
>>> feed.Schedule.speakers[-1].name ➎
'Carina C. Zona'
>>> talk = feed.Schedule.events[40]
>>> type(talk) ➏
<class 'explore0.FrozenJSON'>
>>> talk.name
'There *Will* Be Bugs'
>>> talk.speakers ➐
[3471, 5199]
>>> talk.flavor ➑
Traceback (most recent call last):
...
KeyError: 'flavor'
❶ 传入嵌套的字典和列表组成的 raw_feed,创建一个 FrozenJSON 实
例。
❷ FrozenJSON 实例能使用属性表示法遍历嵌套的字典;这里,我们获
取演讲者列表的元素数量。
❸ 也可以使用底层字典的方法,例如 .keys(),获取记录集合的名
称。
❹ 使用 items() 方法获取各个记录集合及其内容,然后显示各个记录
集合中的元素数量。
❺ 列表,例如 feed.Schedule.speakers,仍是列表;但是,如果里
面的元素是映射,会转换成 FrozenJSON 对象。
❻ events 列表中的 40 号元素是一个 JSON 对象,现在则变成一个
FrozenJSON 实例。
❼ 事件记录中有一个 speakers 列表,列出演讲者的编号。
❽ 读取不存在的属性会抛出 KeyError 异常,而不是通常抛出的
AttributeError 异常。
FrozenJSON 类的关键是 __getattr__
方法。我们在 10.5 节的 Vector
示例中用过这个方法,那时用于通过字母获取 Vector 对象的分量(例
如 v.x、v.y、v.z)。我们要记住重要的一点,仅当无法使用常规的方式获取属性(即在实例、类或超类中找不到指定的属性),解释器才会
调用特殊的 __getattr__
方法。
示例 19-4 的最后一行揭露了这个实现的一个小问题:理论上,尝试读
取不存在的属性应该抛出 AttributeError 异常。其实,一开始我对这
个异常做了处理,但是 __getattr__
方法的代码量增加了一倍,而且
偏离了我最想展示的重要逻辑,因此为了教学,后来我把那部分代码去
掉了。
如示例 19-5 所示,FrozenJSON 类只有两个方法(init 和
getattr)和一个实例属性 __data。因此,尝试获取其他属性会
触发解释器调用 __getattr__
方法。这个方法首先查看 self.__data
字典有没有指定名称的属性(不是键),这样 FrozenJSON 实例便可以
处理字典的所有方法,例如把 items 方法委托给
self.__data.items() 方法。如果 self.__data 没有指定名称的属
性,那么 __getattr__
方法以那个名称为键,从 self.__data 中获取
一个元素,传给 FrozenJSON.build 方法。这样就能深入 JSON 数据的
嵌套结构,使用类方法 build 把每一层嵌套转换成一个 FrozenJSON
实例。
示例 19-5 explore0.py:把一个 JSON 数据集转换成一个嵌套着
FrozenJSON 对象、列表和简单类型的 FrozenJSON 对象
from collections import abc
class FrozenJSON:
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __init__(self, mapping):self.__data = dict(mapping) ➊
def __getattr__(self, name): ➋if hasattr(self.__data, name):return getattr(self.__data, name) ➌else:return FrozenJSON.build(self.__data[name]) ➍
@classmethod
def build(cls, obj): ➎if isinstance(obj, abc.Mapping): ➏return cls(obj)elif isinstance(obj, abc.MutableSequence): ➐return [cls.build(item) for item in obj]else: ➑return obj
❶ 使用 mapping 参数构建一个字典。这么做有两个目的:(1) 确保传入
的是字典(或者是能转换成字典的对象);(2) 安全起见,创建一个副
本。
❷ 仅当没有指定名称(name)的属性时才调用 __getattr__
方法。
❸ 如果 name 是实例属性 __data 的属性,返回那个属性。调用 keys
等方法就是通过这种方式处理的。
❹ 否则,从 self.__data 中获取 name 键对应的元素,返回调用
FrozenJSON.build() 方法得到的结果。
❺ 这是一个备选构造方法,@classmethod 装饰器经常这么用。
❻ 如果 obj 是映射,那就构建一个 FrozenJSON 对象。
❼ 如果是 MutableSequence 对象,必然是列表, 因此,我们把 obj
中的每个元素递归地传给 .build() 方法,构建一个列表。
❽ 如果既不是字典也不是列表,那么原封不动地返回元素。
注意,我们没有缓存或转换原始数据源。在迭代数据源的过程中,嵌套
的数据结构不断被转换成 FrozenJSON 对象。这么做没问题,因为数据
集不大,而且这个脚本只用于访问或转换数据。
从随机源中生成或仿效动态属性名的脚本都必须处理一个问题:原始数
据中的键可能不适合作为属性名。下一节处理这个问题。
处理无效属性名
FrozenJSON 类有个缺陷:没有对名称为 Python 关键字的属性做特殊处
理。比如说像下面这样构建一个对象:
>>> grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
此时无法读取 grad.class 的值,因为在 Python 中 class 是保留字:
>>> grad.class
File "<stdin>", line 1
grad.class
^
SyntaxError: invalid syntax
当然,可以这么做:
>>> getattr(grad, 'class')
1982
但是,FrozenJSON 类的目的是为了便于访问数据,因此更好的方法是
检查传给 FrozenJSON.__init__
方法的映射中是否有键的名称为关键
字,如果有,那么在键名后加上 _,然后通过下述方式读取:
>>> grad.class_
1982
为此,我们可以把示例 19-5 中只有一行代码的 __init__
方法改成示
例 19-6 中的版本。
示例 19-6 explore1.py:在名称为 Python 关键字的属性后面加上 _
def __init__(self, mapping):self.__data = {}for key, value in mapping.items():if keyword.iskeyword(key): ➊key += '_'
self.__data[key] = value
➊ keyword.iskeyword(…) 正是我们所需的函数;为了使用它,必
须导入 keyword 模块;这个代码片段没有列出导入语句。
如果 JSON 对象中的键不是有效的 Python 标识符,也会遇到类似的问
题:
>>> x = FrozenJSON({'2be':'or not'})
>>> x.2be
File "<stdin>", line 1
x.2be
^
SyntaxError: invalid syntax
这种有问题的键在 Python 3 中易于检测,因为 str 类提供的
s.isidentifier() 方法能根据语言的语法判断 s 是否为有效的 Python
标识符。但是,把无效的标识符变成有效的属性名却不容易。对此,有
两个简单的解决方法,一个是抛出异常,另一个是把无效的键换成通用
名称,例如 attr_0、attr_1,等等。为了简单起见,我将忽略这个问
题。
对动态属性的名称做了一些处理之后,我们要分析 FrozenJSON 类的另
一个重要功能——类方法 build 的逻辑。这个方法把嵌套结构转换成
FrozenJSON 实例或 FrozenJSON 实例列表,因此__getattr__
方法
使用这个方法访问属性时,能为不同的值返回不同类型的对象。
除了在类方法中实现这样的逻辑之外,还可以在特殊的 __new__
方法
中实现,如下一节所述。
使用 __new__
方法以灵活的方式创建对象
我们通常把 __init__
称为构造方法,这是从其他语言借鉴过来的术
语。其实,用于构建实例的是特殊方法 __new__
:这是个类方法(使用
特殊方式处理,因此不必使用 @classmethod 装饰器),必须返回一个
实例。返回的实例会作为第一个参数(即 self)传给 __init__
方法。因为调用 __init__
方法时要传入实例,而且禁止返回任何值,所
以 __init__
方法其实是“初始化方法”。真正的构造方法是 __new__
。
我们几乎不需要自己编写 __new__
方法,因为从 object 类继承的实现
已经足够了。
刚才说明的过程,即从 __new__
方法到 __init__
方法,是最常见
的,但不是唯一的。__new__
方法也可以返回其他类的实例,此时,解
释器不会调用 __init__
方法。
也就是说,Python 构建对象的过程可以使用下述伪代码概括:
# 构建对象的伪代码
def object_maker(the_class, some_arg):new_object = the_class.__new__(some_arg)if isinstance(new_object, the_class):the_class.__init__(new_object, some_arg)return new_object# 下述两个语句的作用基本等效x = Foo('bar')x = object_maker(Foo, 'bar')
示例 19-7 是 FrozenJSON 类的另一个版本,把之前在类方法 build 中
的逻辑移到了 __new__
方法中。
示例 19-7 explore2.py:使用 __new__
方法取代 build 方法,构
建可能是也可能不是 FrozenJSON 实例的新对象
from collections import abc
class FrozenJSON:
"""一个只读接口,使用属性表示法访问JSON类对象
"""
def __new__(cls, arg): ➊if isinstance(arg, abc.Mapping):return super().__new__(cls) ➋elif isinstance(arg, abc.MutableSequence): ➌return [cls(item) for item in arg]else:return arg
def __init__(self, mapping):self.__data = {}for key, value in mapping.items():if iskeyword(key):key += '_'self.__data[key] = value
def __getattr__(self, name):if hasattr(self.__data, name):return getattr(self.__data, name)else:return FrozenJSON(self.__data[name]) ➍
❶ __new__
是类方法,第一个参数是类本身,余下的参数与 __init__
方法一样,只不过没有 self。
❷ 默认的行为是委托给超类的 __new__
方法。这里调用的是 object
基类的 __new__
方法,把唯一的参数设为 FrozenJSON。
❸ __new__
方法中余下的代码与原先的 build 方法完全一样。
❹ 之前,这里调用的是 FrozenJSON.build 方法,现在只需调用
FrozenJSON 构造方法。
__new__
方法的第一个参数是类,因为创建的对象通常是那个类的实
例。所以,在 FrozenJSON.__new__
方法
中,super().__new__(cls)
表达式会调用
object.__new__(FrozenJSON)
,而 object 类构建的实例其实是
FrozenJSON 实例,即那个实例的 __class__
属性存储的是
FrozenJSON 类的引用。不过,真正的构建操作由解释器调用 C 语言实
现的object.__new__
方法执行。
OSCON 的 JSON 数据源有一个明显的缺点:索引为 40 的事件,即名为
‘There Will Be Bugs’ 的那个,有两位演讲者,3471 和 5199,
但却不容易找到他们,因为提供的是编号,而 Schedule.speakers 列
表没有使用编号建立索引。此外,每条事件记录中都有 venue_serial
字段,存储的值也是编号,但是如果想找到对应的记录,那就要线性搜
索 Schedule.venues 列表。接下来的任务是,调整数据结构,以便自
动获取所链接的记录。
使用shelve模块调整OSCON数据源的结构
标准库中有个 shelve(架子)模块,这名字听起来怪怪的,可是如果
知道 pickle(泡菜)是 Python 对象序列化格式的名字,还是在那个格
式与对象之间相互转换的某个模块的名字,就会觉得以 shelve 命名是
合理的。泡菜坛子摆放在架子上,因此 shelve 模块提供了 pickle 存
储方式。
- shelve.open 高阶函数返回一个 shelve.Shelf 实例,这是简单的键
- 值对象数据库,背后由 dbm 模块支持,具有下述特点。
- shelve.Shelf 是 abc.MutableMapping 的子类,因此提供了处
理映射类型的重要方法。 - 此外,shelve.Shelf 类还提供了几个管理 I/O 的方法,如 sync
和 close;它也是一个上下文管理器。 - 只要把新值赋予键,就会保存键和值。
- 键必须是字符串。
- 值必须是 pickle 模块能处理的对象。
shelve(https://docs.python.org/3/library/shelve.html)、dbm(https://docs.python.org/3/和 pickle 模块(https://docs.python.org/3/library/pickle.html)的详细用
法和注意事项参见文档。现在值得关注的是,shelve 模块为识别
OSCON 的日程数据提供了一种简单有效的方式。我们将从 JSON 文件
中读取所有记录,将其存在一个 shelve.Shelf 对象中,键由记录类型
和编号组成(例如,‘event.33950’ 或 ‘speaker.3471’),而值是
我们即将定义的 Record 类的实例。
实例 19-8 是 schedule1.py 脚本的 doctest,使用 shelve 模块处理数据
源。若想以交互式方式测试,要执行 python -i schedule1.py 命令
运行脚本,启动加载了 schedule1 模块的控制台。主要工作由load_db 函数完成:调用 osconfeed.load 方法(在示例 19-2 中定
义)读取 JSON 数据,把通过 db 传入的 Shelf 对象中的各条记录存储
为一个个 Record 实例。这样处理之后,获取演讲者的记录就容易了,
例如 speaker = db[‘speaker.3471’]。
示例 19-8 测试 schedule1.py 脚本(见示例 19-9)提供的功能
>>> import shelve
>>> db = shelve.open(DB_NAME) ➊
>>> if CONFERENCE not in db: ➋
... load_db(db) ➌
...
>>> speaker = db['speaker.3471'] ➍
>>> type(speaker) ➎
<class 'schedule1.Record'>
>>> speaker.name, speaker.twitter ➏
('Anna Martelli Ravenscroft', 'annaraven')
>>> db.close() ➐
❶ shelve.open 函数打开现有的数据库文件,或者新建一个。
❷ 判断数据库是否填充的简便方法是,检查某个已知的键是否存在;
这里检查的键是 conference.115,即 conference 记录(只有一个)
的键。
❸ 如果数据库是空的,那就调用 load_db(db),加载数据。
❹ 获取一条 speaker 记录。
❺ 它是示例 19-9 中定义的 Record 类的实例。
❻ 各个 Record 实例都有一系列自定义的属性,对应于底层 JSON 记录
里的字段。
❼ 一定要记得关闭 shelve.Shelf 对象。如果可以,使用 with 块确保
Shelf 对象会关闭。
schedule1.py 脚本的代码在示例 19-9 中。
示例 19-9 schedule1.py:访问保存在 shelve.Shelf 对象里的
OSCON 日程数据
import warnings
import osconfeed ➊
DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):self.__dict__.update(kwargs) ➋
def load_db(db):raw_data = osconfeed.load() ➌warnings.warn('loading ' + DB_NAME)for collection, rec_list in raw_data['Schedule'].items(): ➍record_type = collection[:-1] ➎for record in rec_list:key = '{}.{}'.format(record_type, record['serial']) ➏record['serial'] = key ➐db[key] = Record(**record) ➑
❶ 加载示例 19-2 中的 osconfeed.py 模块。
❷ 这是使用关键字参数传入的属性构建实例的常用简便方式(详情参
见下文)。
❸ 如果本地没有副本,从网上下载 JSON 数据源。
❹ 迭代集合(例如 ‘conferences’、‘events’,等等)。
❺ record_type 的值是去掉尾部 ‘s’ 后的集合名(即把 ‘events’ 变
成 ‘event’)。
❻ 使用 record_type 和 ‘serial’ 字段构成 key。
❼ 把 ‘serial’ 字段的值设为完整的键。
❽ 构建 Record 实例,存储在数据库中的 key 键名下。
Record.__init__
方法展示了一个流行的 Python 技巧。我们知道,对
象的 __dict__
属性中存储着对象的属性——前提是类中没有声明
__slots__
属性,如 9.8 节所述。因此,更新实例的 __dict__
属性,
把值设为一个映射,能快速地在那个实例中创建一堆属性。
示例 19-9 中定义的 Record 类太简单了,因此你可能会问,为什么之前
没用,而是使用更复杂的 FrozenJSON 类。原因有两个。第
一,FrozenJSON 类要递归转换嵌套的映射和列表;而 Record 类不需
要这么做,因为转换好的数据集中没有嵌套的映射和列表,记录中只有
字符串、整数、字符串列表和整数列表。第二,FrozenJSON 类要访问
内嵌的 __data 属性(值是字典,用于调用 keys 等方法),而现在我
们也不需要这么做了。
像上面那样调整日程数据集之后,我们可以扩展 Record 类,让它提供
一个有用的服务:自动获取 event 记录引用的 venue 和 speaker 记
录。这与 Django ORM 访问 models.ForeignKey 字段时所做的事类
似:得到的不是键,而是链接的模型对象。在下一个示例中,我们要使
用特性来实现这个服务。
使用特性获取链接的记录
下一个版本的目标是,对于从 Shelf 对象中获取的 event 记录来说,
读取它的 venue 或 speakers 属性时返回的不是编号,而是完整的记录
对象。用法如示例 19-10 中的交互代码片段所示。
示例 19-10 摘自 schedule2.py 脚本的 doctest
>>> DbRecord.set_db(db) ➊
>>> event = DbRecord.fetch('event.33950') ➋
>>> event ➌
<Event 'There *Will* Be Bugs'>
>>> event.venue ➍
<DbRecord serial='venue.1449'>
>>> event.venue.name ➎
'Portland 251'
>>> for spkr in event.speakers: ➏
... print('{0.serial}: {0.name}'.format(spkr))
...
speaker.3471: Anna Martelli Ravenscroft
speaker.5199: Alex Martelli
❶ DbRecord 类扩展 Record 类,添加对数据库的支持:为了操作数据
库,必须为 DbRecord 提供一个数据库的引用。
❷ DbRecord.fetch 类方法能获取任何类型的记录。
❸ 注意,event 是 Event 类的实例,而 Event 类扩展 DbRecord 类。
❹ event.venue 返回一个 DbRecord 实例。
❺ 现在,想找出 event.venue 的名称就容易了。这种自动取值是这个
示例的目标。
❻ 还可以迭代 event.speakers 列表,获取表示各位演讲者的
DbRecord 对象。
图 19-1 绘出了本节要分析的几个类。
Record__init__
方法与 schedule1.py 脚本(见示例 19-9)中的一样;为
了辅助测试,增加了 __eq__
方法。
DbRecord
Record 类的子类,添加了 __db 类属性,用于设置和获取 __db 属
性的 set_db 和 get_db 静态方法,用于从数据库中获取记录的 fetch
类方法,以及辅助调试和测试的 __repr__
实例方法。
Event
DbRecord 类的子类,添加了用于获取所链接记录的 venue 和
speakers 属性,以及特殊的 __repr__
方法。
DbRecord.__db 类属性的作用是存储打开的 shelve.Shelf 数据库引
用,以便在需要使用数据库的 DbRecord.fetch 方法及 Event.venue
和 Event.speakers 属性中使用。我把 __db 设为私有类属性,然后定义了普通的读值方法和设值方法,以防不小心覆盖 __db 属性的值。基
于一个重要的原因,我没有使用特性去管理 __db 属性:特性是用于管
理实例属性的类属性。
本节的代码在本书仓库(https://github.com/fluentpython/example-code)里
的 schedule2.py 模块中。这个模块有 100 多行,因此我会分成几部分分
析。
schedule2.py 脚本的前几个语句在示例 19-11 中。
示例 19-11 schedule2.py:导入模块,定义常量和增强的 Record
类
import warnings
import inspect ➊
import osconfeed
DB_NAME = 'data/schedule2_db' ➋
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):self.__dict__.update(kwargs)
def __eq__(self, other): ➌if isinstance(other, Record):return self.__dict__ == other.__dict__else:return NotImplemented
➊ inspect 模块在 load_db 函数中使用(参见示例 19-14)。
➋ 因为要存储几个不同类的实例,所以我们要创建并使用不同的数据
库文件;这里不用示例 19-9 中的 ‘schedule1_db’,而是使用’schedule2_db’。
➌ __eq__
方法对测试有重大帮助。
在 Python 2 中,只有“新式”类支持特性。在 Python 2 中定义新式类
的方法是,直接或间接继承 object 类。示例 19-11 中的 Record
类是一个继承体系的基类,用到了特性;因此,在 Python 2 中声明
Record 类时,开头要这么写:
class Record(object):
# 余下的代码……
接下来,schedule2.py 脚本定义了两个类——一个自定义的异常类型和
DbRecord 类,参见示例 19-12。
示例 19-12 schedule2.py:MissingDatabaseError 类和
DbRecord 类
class MissingDatabaseError(RuntimeError):
"""需要数据库但没有指定数据库时抛出。""" ➊class DbRecord(Record): ➋__db = None ➌@staticmethod ➍def set_db(db):DbRecord.__db = db ➎@staticmethod ➏def get_db():return DbRecord.__db@classmethod ➐def fetch(cls, ident):db = cls.get_db()try:return db[ident] ➑except TypeError:if db is None: ➒msg = "database not set; call '{}.set_db(my_db)'"
raise MissingDatabaseError(msg.format(cls.__name__))else: ➓raisedef __repr__(self):if hasattr(self, 'serial'): ⓫cls_name = self.__class__.__name__return '<{} serial={!r}>'.format(cls_name, self.serial)else:return super().__repr__() ⓬
❶ 自定义的异常通常是标志类,没有定义体。写一个文档字符串,说
明异常的用途,比只写一个 pass 语句要好。
❷ DbRecord 类扩展 Record 类。
❸ __db 类属性存储一个打开的 shelve.Shelf 数据库引用。
❹ set_db 是静态方法,以此强调不管调用多少次,效果始终一样。
❺ 即使调用 Event.set_db(my_db),__db 属性仍在 DbRecord 类中
设置。
❻ get_db 也是静态方法,因为不管怎样调用,返回值始终是
DbRecord.__db 引用的对象。
❼ fetch 是类方法,因此在子类中易于定制它的行为。
❽ 从数据库中获取 ident 键对应的记录。
❾ 如果捕获到 TypeError 异常,而且 db 变量的值是 None,抛出自定
义的异常,说明必须设置数据库。
❿ 否则,重新抛出 TypeError 异常,因为我们不知道怎么处理。
⓫ 如果记录有 serial 属性,在字符串表示形式中使用。
⓬ 否则,调用继承的__repr__
方法。
现在到这个示例的重要部分了——Event 类,如示例 19-13 所示。
示例 19-13 schedule2.py:Event 类
class Event(DbRecord): ➊@propertydef venue(self):key = 'venue.{}'.format(self.venue_serial)return self.__class__.fetch(key) ➋@propertydef speakers(self):if not hasattr(self, '_speaker_objs'): ➌spkr_serials = self.__dict__['speakers'] ➍fetch = self.__class__.fetch ➎self._speaker_objs = [fetch('speaker.{}'.format(key))for key in spkr_serials] ➏return self._speaker_objs ➐def __repr__(self):if hasattr(self, 'name'): ➑cls_name = self.__class__.__name__return '<{} {!r}>'.format(cls_name, self.name)else:return super().__repr__() ➒
❶ Event 类扩展 DbRecord 类。
❷ 在 venue 特性中使用 venue_serial 属性构建 key,然后传给继承
自 DbRecord 类的 fetch 类方法(详情参见下文)。
❸ speakers 特性检查记录是否有 _speaker_objs 属性。
❹ 如果没有,直接从 __dict__
实例属性中获取 ‘speakers’ 属性的值,防止无限递归,因为这个特性的公开名称也是 speakers。
❺ 获取 fetch 类方法的引用(稍后会说明这么做的原因)。
❻ 使用 fetch 获取 speaker 记录列表,然后赋值给
self._speaker_objs。
❼ 返回前面获取的列表。
❽ 如果记录有 name 属性,在字符串表示形式中使用。
❾ 否则,调用继承的 __repr__
方法。
在示例 19-13 中的 venue 特性里,最后一行返回的是
self.__class__.fetch(key)
,为什么不直接使用
self.fetch(key) 呢?对这个 OSCON 数据源来说,可以使用后者,
因为事件记录都没有 ‘fetch’ 键。哪怕只有一个事件记录有名为
‘fetch’ 的键,那么在那个 Event 实例中,self.fetch 获取的是
fetch 字段的值,而不是 Event 继承自 DbRecord 的 fetch 类方法。
这个缺陷不明显,很容易被测试忽略;在生产环境中,如果会场或演讲
者记录链接到那个事件记录,获取事件记录时才会暴露出来。
如果 Record 类的行为更像映射,可以把动态的 __getattr__
方法换
成动态的 __getitem__
方法,这样就不会出现由于覆盖或遮盖而引起
的缺陷了。使用映射实现 Record 类或许更符合 Python 风格。可是,如
果我采用那种方式,就发掘不了动态属性编程的技巧和陷阱了。这个示例最后的代码是重写的 load_db 函数,如示例 19-14。
示例 19-14 schedule2.py:load_db 函数
def load_db(db):raw_data = osconfeed.load()warnings.warn('loading ' + DB_NAME)for collection, rec_list in raw_data['Schedule'].items():record_type = collection[:-1] ➊cls_name = record_type.capitalize() ➋cls = globals().get(cls_name, DbRecord) ➌if inspect.isclass(cls) and issubclass(cls, DbRecord): ➍factory = cls ➎else:factory = DbRecord ➏for record in rec_list: ➐key = '{}.{}'.format(record_type, record['serial'])record['serial'] = keydb[key] = factory(**record) ➑
❶ 目前,与 schedule1.py 脚本(见示例 19-9)中的 load_db 函数一
样。
❷ 把 record_type 变量的值首字母变成大写(例如,把 ‘event’ 变成
‘Event’),获取可能的类名。
❸ 从模块的全局作用域中获取那个名称对应的对象;如果找不到对
象,使用 DbRecord。
❹ 如果获取的对象是类,而且是 DbRecord 的子类……
❺ ……把对象赋值给 factory 变量。因此,factory 的值可能是
DbRecord 的任何一个子类,具体的类取决于 record_type 的值。
❻ 否则,把 DbRecord 赋值给 factory 变量。
❼ 这个 for 循环创建 key,然后保存记录,这与之前一样,不过……
❽ ……存储在数据库中的对象由 factory 构建,factory 可能是
DbRecord 类,也可能是根据 record_type 的值确定的某个子类。
注意,只有事件类型的记录有自定义的类——Event。不过,如果定义
了 Speaker 或 Venue 类,load_db 函数构建和保存记录时会自动使用
这两个类,而不会使用默认的 DbRecord 类。