1. 动态属性
在Python
中,数据的属性和处理数据的方法统称属性attribute
.其实,方法只是可调用的属性.
Python提供了丰富的API,用于控制属性的访问权限,以及实现动态属性.
使用点号访问属性时(如obj.attr
),Python解释器会调用特殊的方法(如__getattr__
和__setattr__
)计算属性.用户自己定义的类可以通过__getattr__
方法实现"虚拟属性".当访问不存在的属性时(如obj.no_such_attribute
),即时计算属性的值.
动态创建属性照理说是一种元编程,框架的作者经常这么做.然而在Python中,相关的基础技术十分简单,任何人都可以使用,甚至在日常的数据转换任务中也能用到.下面以这种任务开启本章的话题.
本节需要的先验知识有:
1.1. 影响属性处理方式的特殊属性
__class__
对象所属类的引用(即
obj.__class__
与type(obj)
的作用相同).Python 的某些特殊方法,例如__getattr__
,只在对象的类中寻找,而不在实例中寻找.__dict__
一个映射,存储对象或类的可写属性.有
__dict__
属性的对象,任何时候都能随意设置新属性.如果类有__slots__
属性,它的实例可能没有__dict__
属性.参见下面对__slots__
属性的说明.__slots__
类可以定义这个这属性,限制实例能有哪些属性.
__slots__
属性的值是一个字符串组成的元组,指明允许有的属性.如果__slots__
中没有'__dict__'
,那么该类的实例没有__dict__
属性,实例只允许有指定名称的属性.
1.2. 处理属性的内置函数
下述5个内置函数对对象的属性做读、写和内省操作.
dir([object])
列出对象的大多数属性.dir 函数的目的是交互式使用,因此没有提供完整的属性列表,只列出一组"重要的"属性名.
dir
函数能审查有或没有__dict__
属性的对象.dir
函数不会列出__dict__
属性本身,但会列出其中的键.dir
函数也不会列出类的几个特殊属性,例如__mro__
、__bases__
和__name__
.如果没有指定可选的object
参数,dir
函数会列出当前作用域中的名称.getattr(object,name[, default])
从
object
对象中获取name字符串对应的属性.获取的属性可能来自对象所属的类或超类。如果没有指定的属性,getattr
函数抛出AttributeError
异常,或者返回default
参数的值(如果设定了这个参数的话).hasattr(object, name)
如果
object
对象中存在指定的属性,或者能以某种方式(例如继承)通过object
对象获取指定的属性,返回Truesetattr(object, name, value)
把
object
对象指定属性的值设为value
,前提是object
对象能接受那个值.这个函数可能会创建一个新属性,或者覆盖现有的属性.
vars([object])
返回
object
对象的__dict__
属性;如果实例所属的类定义了__slots__
属性,实例没有__dict__
属性,那么vars
函数不能处理那个实例(相反,dir
函数能处理这样的实例).如果没有指定参数,那么vars()
函数的作用与locals()
函数一样:返回表示本地作用域的字典.
1.3. 处理属性的特殊方法
在用户自己定义的类中,下述特殊方法用于获取,设置,删除和列出属性.
使用点号或内置的getattr
、hasattr
和setattr
函数存取属性都会触发下述列表中相应的特殊方法.但是直接通过实例的__dict__
属性读写属性不会触发这些特殊方法——如果需要,通常会使用这种方式跳过特殊方法.
对用户自己定义的类来说,如果隐式调用特殊方法,仅当特殊方法在对象所属的类型上定义,而不是在对象的实例字典中定义时,才能确保调用成功.
要假定特殊方法从类上获取,即便操作目标是实例也是如此.因此,特殊方法不会被同名实例属性遮盖.
__delattr__(self, name)
只要使用
del
语句删除属性,就会调用这个方法.例如,del obj.attr
语句触发Class.__delattr__(obj, 'attr')
方法.__dir__(self)
把对象传给
dir
函数时调用,列出属性.例如,dir(obj)
触发Class.__dir__(obj)
方法.__getattr__(self, name)
仅当获取指定的属性失败,搜索过
obj
、Class
和超类之后调用.表达式obj.no_such_attr
、getattr(obj, 'no_such_attr')
和hasattr(obj, 'no_such_attr')
可能会触发Class.__getattr__(obj, 'no_such_attr')
方法,但仅当在obj
、Class
和超类中找不到指定的属性时才会触发.__getattribute__(self, name)
尝试获取指定的属性时总会调用这个方法,不过寻找的属性是特殊属性或特殊方法时除外.点号与
getattr
和hasattr
内置函数会触发这个方法.调用__getattribute__
方法且抛出AttributeError
异常时,才会调用__getattr__
方法.为了在获取obj
实例的属性时不导致无限递归,__getattribute__
方法的实现要使用super().__getattribute__(obj, name)
__setattr__(self, name, value)
尝试设置指定的属性时总会调用这个方法.点号和
setattr
内置函数会触发这个方法.例如obj.attr = 42
和setattr(obj,'attr', 42)
都会触发Class.__setattr__(obj,attr’, 42)
方法
其实特殊方法__getattribute__
和__setattr__
不管怎样都会调用,几乎会影响每一次属性存取,因此比__getattr__
方法(只处理不存在的属性名)更难正确使用.与定义这些特殊方法相比,使用特性或描述符相对不易出错.
1.4. 例子
我们要使用动态属性处理"O’Reilly 为OSCON 2014 大会"提供的
JSON
格式数据源.那个JSON源中有895条记录,整个数据集是一个JSON 对象,里面有一个键,名为"Schedule";这个键对应的值也是一个映像,有4个键:"conferences"、"events"、"speakers" 和"venues".这4个键对应的值都是一个记录列表.列表中有成百上千条记录.不过,"conferences"键对应的列表中只有一条记录,如上述示例所示.这4个列表中的每个元素都有一个名为"serial"的字段,这是元素在各个列表中的唯一标识符.
第一个脚本只用于下载那个OSCON数据源.为了避免浪费流量,我会先检查本地有没有副本.这么做是合理的,因为OSCON 2014 大会已经结束,数据源不会再更新.
第一个例子没用到元编程,几乎所有代码的作用可以用这一个表达式概括:json.load(fp)
.不过,这样足以处理那个数据集了.osconfeed.load 函数会在后面几个示例中用到.
import requests import warnings import os import json URL = 'http://www.oreilly.com/pub/sc/osconfeed' JSON = 'osconfeed.json' def load(): if not os.path.exists(JSON): msg = 'downloading {} to {}'.format(URL, JSON) warnings.warn(msg) with open(JSON, 'w') as local: remote = requests.get(URL) json.dump(remote.json(),local) with open(JSON) as fp: return json.load(fp)
raw_feed = load() sorted(raw_feed['Schedule'].keys())
['conferences', 'events', 'speakers', 'venues']
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
1 conferences 494 events 357 speakers 53 venues
raw_feed['Schedule']['speakers'][-1]['name']
'Carina C. Zona'
raw_feed['Schedule']['speakers'][-1]['serial']
141590
raw_feed['Schedule']['events'][40]['name']
raw_feed = load() sorted(raw_feed['Schedule'].keys())
0
raw_feed = load() sorted(raw_feed['Schedule'].keys())
1
raw_feed = load() sorted(raw_feed['Schedule'].keys())
2
1.5. 使用动态属性访问JSON类数据
feed['Schedule']['events'][40]['name']
这种句法很冗长.在JavaScript中,可以使用feed.Schedule.events[40].name
获取那个值.在Python中可以实现一个近似字典的类(网上有大量实现)以达到同样的效果.我自己实现了FrozenJSON
类,比大多数实现都简单,因为只支持读取,即只能访问数据.不过这个类能递归,自动处理嵌套的映射和列表.
raw_feed = load() sorted(raw_feed['Schedule'].keys())
3
raw_feed = load() sorted(raw_feed['Schedule'].keys())
4
raw_feed = load() sorted(raw_feed['Schedule'].keys())
5
raw_feed = load() sorted(raw_feed['Schedule'].keys())
6
['conferences', 'events', 'speakers', 'venues']
raw_feed = load() sorted(raw_feed['Schedule'].keys())
8
'Carina C. Zona'
['conferences', 'events', 'speakers', 'venues']
0
['conferences', 'events', 'speakers', 'venues']
1
['conferences', 'events', 'speakers', 'venues']
2
raw_feed = load() sorted(raw_feed['Schedule'].keys())
0
['conferences', 'events', 'speakers', 'venues']
4
raw_feed = load() sorted(raw_feed['Schedule'].keys())
2
['conferences', 'events', 'speakers', 'venues']
6
['conferences', 'events', 'speakers', 'venues']
7
1.5.1. 处理无效属性名
FrozenJSON
类有个缺陷:没有对名称为Python关键字的属性做特殊处理.比如说像下面这 样构建一个对象:
['conferences', 'events', 'speakers', 'venues']
8
此时无法读取grad.class
的值,因为在Python中class是保留字:
['conferences', 'events', 'speakers', 'venues']
9
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
0
但是FrozenJSON
类的目的是为了便于访问数据,因此更好的方法是检查传给Frozen-JSON.__init__
方法的映射中是否有键的名称为关键字,如果有,那么在键名后加上_
.
这种有问题的键在Python3中易于检测,因为str类提供的s.isidentifier()
方法能根据语言的语法判断s
是否为有效的Python标识符.但是,把无效的标识符变成有效的属性名却不容易.对此,有两个简单的解决方法:
- 一个是抛出异常
- 另一个是把无效的键换成通用名称,例如
attr_0
、attr_1
,等等.
为了简单起见,我将忽略这个问题.
对动态属性的名称做了一些处理之后,我们要分析FrozenJSON
类的另一个重要功能——类方法build
的逻辑.这个方法把嵌套结构转换成FrozenJSON
实例或FrozenJSON
实例列表,因此__getattr__
方法使用这个方法访问属性时,能为不同的值返回不同类型的对象.
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
1
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
2
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
3
1.5.2. 使用__new__方法以灵活的方式创建对象
除了在类方法中实现这样的逻辑之外,还可以在特殊的__new__
方法中实现.
我们通常把__init__
称为构造方法,这是从其他语言借鉴过来的术语.其实,用于构建实例的是特殊方法__new__
--这是个类方法(使用特殊方式处理,因此不必使用@classmethod
装饰器),必须返回一个实例.返回的实例会作为第一个参数(即self
)传给__init__
方法.因为调用__init__
方法时要传入实例,而且禁止返回任何值,所以__init__
方法其实是"初始化方法".真正的构造方法是__new__
.我们几乎不需要自己编写__new__
方法,因为从object
类继承的实现已经足够了.
刚才说明的过程,即从__new__
方法到__init__
方法,是最常见的,但不是唯一的.
__new__
方法也可以返回其他类的实例,此时,解释器不会调用__init__
方法.
下面是FrozenJSON
类的另一个版本,把之前在类方法build
中的逻辑移到了__new__
方法中.
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
4
__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
列表.接下来的任务是,调整数据结构,以便自动获取所链接的记录.
1.5.3. 使用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
模块为识别OSCON的日程数据提供了一种简单有效的方式.我们将从JSON文件中读取所有记录,将其存在一个shelve.Shelf
对象中,键由记录类型和编号组成(例如,event.33950
或speaker.3471
),而值是我们即将定义的Record类的实例.
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
5
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
6
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
7
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
8
for key, value in sorted(raw_feed['Schedule'].items()): print('{:3} {}'.format(len(value), key))
9
1 conferences 494 events 357 speakers 53 venues
0
Record.__init__
方法展示了一个流行的Python技巧.我们知道,对象的__dict__
属性中存储着对象的属性——前提是类中没有声明__slots__
属性.因此,更新实例的__dict__
属性,把值设为一个映射,能快速地在那个实例中创建一堆属性.
示例中定义的Record
类太简单了,因此你可能会问,为什么之前没用,而是使用更复杂的FrozenJSON
类.原因有两个:
- 第一,
FrozenJSON
类要递归转换嵌套的映射和列表;而Record
类不需要这么做,因为转换好的数据集中没有嵌套的映射和列表,记录中只有字符串、整数、字符串列表和整数列表. - 第二.
FrozenJSON
类要访问内嵌的__data__
属性(值是字典,用于调用keys
等方法),而现在我们也不需要这么做
像上面那样调整日程数据集之后,我们可以扩展Record
类,让它提供一个有用的服务--自动获取event
记录引用的venue
和speaker
记录.这与Django ORM
访问models.ForeignKey
字段时所做的事类似--得到的不是键,而是链接的模型对象.
1.6. 动态绑定方法
python中方法只是可以调用的属性,因此方法也是可以动态绑定的.尤其实例方法的动态绑定尤其实用.
1.6.1. 动态绑定实例方法
动态绑定实例方法需要借助types.MethodType
1 conferences 494 events 357 speakers 53 venues
1
1 conferences 494 events 357 speakers 53 venues
2
1 conferences 494 events 357 speakers 53 venues
3
1 conferences 494 events 357 speakers 53 venues
4
1 conferences 494 events 357 speakers 53 venues
5
1.6.2. 动态绑定类方法
动态绑定类方法与前面类似,只是MethodType
的第一个参数改成了类名
1 conferences 494 events 357 speakers 53 venues
6
1 conferences 494 events 357 speakers 53 venues
7
1 conferences 494 events 357 speakers 53 venues
8
1 conferences 494 events 357 speakers 53 venues
9
raw_feed['Schedule']['speakers'][-1]['name']
0
1 conferences 494 events 357 speakers 53 venues
9
1.6.3. 动态绑定静态方法
动态绑定静态方法更加简单了,只要直接在类名后面像添加元素一样添加即可
raw_feed['Schedule']['speakers'][-1]['name']
2
raw_feed['Schedule']['speakers'][-1]['name']
3
raw_feed['Schedule']['speakers'][-1]['name']
4
还没有评论,来说两句吧...