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()) 0raw_feed = load() sorted(raw_feed['Schedule'].keys()) 1 raw_feed = load() sorted(raw_feed['Schedule'].keys()) 21.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()) 5raw_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'] 71.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)) 31.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)) 7for 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 51.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 9raw_feed['Schedule']['speakers'][-1]['name'] 0 1 conferences 494 events 357 speakers 53 venues 91.6.3. 动态绑定静态方法
动态绑定静态方法更加简单了,只要直接在类名后面像添加元素一样添加即可
raw_feed['Schedule']['speakers'][-1]['name'] 2 raw_feed['Schedule']['speakers'][-1]['name'] 3 raw_feed['Schedule']['speakers'][-1]['name'] 4 



还没有评论,来说两句吧...