# 第19章 动态属性和特性
"""
属性(attribute):
数据的属性和处理数据的方法统称属性,方法只是可调用的属性.
特性(property)
除此之外,我们海可以创建特性,在不改变类接口的前提下使用存取方法
(即读值方法和设值方法)修改数据属性,这与统一访问原则相符
统一访问原则:
不管服务是由存储还是计算实现的,一个模块提供的服务都应该通过统一的方式使用
python还提供了丰富的API,用于控制属性的权限,以及实现动态属性
使用点号访问属性时(obj.attr):
python解释器会调用特殊的方法(如__getattr__,__setattr__)计算属性
用户自己定义的类可以通过__getsttr__方法实现"虚拟属性",当访问不存在的属性时
即时计算属性的值
动态创建属性是一种元变成,在python中,相关基础技术十分简单,任何人都可以使用,甚至在
日常数据转换任务中也能用到.
"""
# 19.1 使用动态属性转换数据
# 示例19-2 osconfeed.py:下载osconfeed.json
# 19.1.1使用动态属性访问json数据
"""
读取这种嵌套的json数据时使用的feed['Schedule']['events'][40]['speakers'])语法很冗长
js中可以使用feed.Schedule.events[40].speakers表示python通过实现一个类似字典的类,能达到相同的效果
"""
# 示例19-5 explore0.py:把一个json对象装换成一个嵌套着FrozenJSON对象,列表.和简单类型的FrozenJSON对象
# 19.1.2处理无效的属性名
"""
类FrozenJSON的缺陷,如果属性名是python中的关键字,就无法读取属性值
如 grad = FrozenJSON({'name':'Jim','class':1982})
grad.class就不能正确读取属性值
"""
# 19-6 explore0.py:在名称为python关键字的属性后面加上_
# 19.1.3使用__new__方法以灵活的方式创建对象
"""
通常将__init__称为构造方法
实际上真正用于构建实例的方法是__new__,这是一个类方法
使用特殊方式处理,因此不用加@classmethod
它必须返回一个实例,返回的实例作为__init__的self参数传递给__init__
实际使用中几乎不用自己编写__new__方法,从object继承来的实现已经足够
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 explore2.py:使用__new__方法取代build方法
# 19.1.4 使用shelve模块调整OSCON数据源的结构
"""
shelve模块:
提供了对象序列化pickle存储方式
shelve.open高阶函数返回一个shelve.Shelf实例
这是简单的键值对象数据库,背后由dbm支持
shelve.Shelf是abc.MutableMapping的子类
shelve.Shelf还提供了几个管理I/O的方法,如sync和close,它也是一个上下文管理器
只要把新值赋予键,就会保存键值对
键必须是字符串
值必须是pickle模块能处理的对象
"""
# 示例19-9 schedule1.py:访问保存再shelve.Shelf对象里的OSCON数据
"""
对象的__dict__属性,存储着对象的属性,前提是类中没有申明__slot__属性
因此更新__dict__,把值设为一个映射,能快速地在那个实例中创建一堆属性
"""
# 19.1.5使用特性获取链接的记录
"""
特性经常用于把公开的属性变成使用读值方法和设值方法管理的属性
且在不影响客户端代码的前提下实施业务规则"""
# 19.2 使用特性验证属性
# 19.2.1 LineItem类第一版:表示订单中商品的类
"""
假设有个销售散装有机食品的电商应用,客户可以按重量订购坚果,干果或杂粮,
在这个系统中,每个订单都有一些列的商品,而每个商品都可以通过下例的类表示
"""
# 示例19-15 bulkfood_v1.py:最简单的LineItem类
# 19.2.2 LineItem类第二版:能验证值的特性
"""
使用特性以后,我们可以使用读值方法和设值方法,但是类的接口保持不变,即
设置LineItem对象的weight属性仍然写成raisins.weight = 12
"""
# 示例19-17 bulkfood_v2.py:定义了weight特性的LineItem类
"""
第二版把weight属性变成了特性,用户无法再给weight赋值负数或者0,
为防止工作人员出错,可以把price属性作相同的操作,但是就存在重复
去除重复的方法是抽象
抽象特性的方式:
使用特性工厂函数
使用描述符类
"""
# 19.3 特性的全面解析
"""
内置的property经常用作装饰器,但它其实是一个类
在python中,函数和类通常可以互换,因为二者都是可调用对象
而且没有实例化对象的new运算符,所有调用构造函数和调用工厂函数没有什么区别
此外只要能够返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器
property完整的构造方法:
property(fget = None,fset=None,fdel=None,doc=None)
所有的参数都是可选的
如果没有把函数传给某个参数,那么得到的特性对像就不允许执行相应的操作
"""
# 19.3.1特性会覆盖实例属性
"""
特性都是类属性,但是特性管理的其实是实例属性的存取
如果实例和所属的类有同名属性,那么实例属性会覆盖类的数据属性
"""
"""
# 示例 19-19 实例属性覆盖类的数据属性
class Class: # 定一个类,它有两个属性 数据属性data和特性prop
data = 'the class data attr'
@property
def prop(self):
return 'the prop value'
obj = Class()
# vars(obj)返回obj的__dict__属性,这里表明没有实例属性
print(vars(obj)) # {}
print(obj.data) # the class data attr
obj.data = 'bar' # 为obj.data赋值,创建一个实例属性
# 此时实例属性覆盖了类属性data
print(vars(obj)) # {'data': 'bar'}
print(obj.data) # bar
# 类属性data的值完好无损
print(Class.data) # the class data attr
# 实例属性不会覆盖类的特性 19-20
# 直接从类中读取prop,获取的是特性对象本身,不会运行特性的读值方法
print(Class.prop) # <property object at 0x0000017F837DC540>
# 读取obj.prop会执行特性的读值方法
print(obj.prop) # the prop value
# 尝试设置prop实例属性,结果失败
# obj.prop = 'foo' # AttributeError: can't set attribute
# 但是可以直接把'prop'存入obj.__dict__
obj.__dict__['prop'] = 'foo'
print(vars(obj)) # {'data': 'bar', 'prop': 'foo'} obj现在有两个实例属性
print(obj.prop) # the prop value 然而读取obj.prop任然会执行特性的读值方法
Class.prop = 'baz' # 覆盖Class.prop特性,销毁特性对象
print(obj.prop) # foo 现在obj.prop获取的是实例属性
print("==============================")
# 19-21 为class类新添一个特性,覆盖实例属性
print(obj.data) # bar 获取的是实例属性
print(Class.data) # the class data attr 获取的是类属性
Class.data = property(lambda self:'the "data" prop value') # 使用新特性覆盖Class.data
print(obj.data) # the "data" prop value 被特性覆盖了
del Class.data # 删除特性
print(obj.data) # bar 实例属性恢复
"""
# 总结:
# obj.attr这样的表达式不会从obj开始寻找attr,而是从obj.__class__开始,
# 仅当类中没有名为attr的特性时,python才会在obj的实例中找
# 这条规则不仅适用于特性,还适用于一整类描述符--覆盖型描述符(overrriding descriptor)
# 19.3.2特性的文档
# 控制台中的help()函数或IDE等工具需要显示特性的文档时,会从特性的__doc__中提取
# weight = property(........doc='weight in kilograms')
# 使用装饰器创建property对象时,读值方法的文档字符串作为一个整体,变成特性的文档
# 19.4定义一个特性工厂函数
# 就不用手动实现两队一模一样的读值方法和设值方法了
# 示例 19-24 bulkfood_v2prop.py:quantity特性工厂函数
# 19.5处理属性的删除操作
# 示例19-26 blackknight.py:定义特性删除方法
# 19.6 处理属性的重要属性和函数
# 19.6.1影响属性处理方式的特殊属性
"""
__class__:
对象所属类的引用(即obj.__class__与type(obj)的作用相同)
python的某些特殊方法只在对象的类中寻找,而不在实例中寻找,如__getattr__
__dict__:
一个映射,存储对象或类的可写属性,有__dict__属性的对象,任何时候都能随意设置新的属性
如果类有__slots__属性,它的实例可能没有__dict__属性
__slots__:
类可以定义这个属性,限制实例可以有哪些属性,其值是一个由字符串组成的元组,指明允许有的属性
如果__slots__中没有__dict__属性,那么该类的实例没有__dict__属性
"""
# 19.6.2处理属性的内置函数
# 以下5个内置函数对对象的属性做读/写/内省操作
"""
dir([object]):
列出对象的大多数属性
能审查有或者没有__dict__的对象
不会列出__dict__属性本身,但会列出其中的键
不会列出几个特殊的属性:__mro__,__bases__,__name__
如果没有指定可选参数object,返回当前作用域中的名称
getattr(object,name[,default]):
从obj获取name字符串对应的属性
获取的属性可能来自对象的类或者超类
如果没有指定的属性,抛出AttributeError,或者返回default参数的值(如果设置了的话)
hasattr(obj,name):
如果obj对象存在指定的属性,或者能以某种方式(例如继承)通过obj对象获取指定的属性,则返回True
setattr(obj,name,value):
把obj对象指定属性的值设置为value,前提是obj对象能接受这个值
这个函数可能会创建一个新属性,或者覆盖现有的属性
vars([object]):
返回obj对象的__dict__属性,如实例没有__dict__属性(设置了__slots__),那么不能处理,相反dir()可以处理
省略object效果和dir相同
"""
# 19.6.3处理属性的特殊方法
# 在用户自己定义的类中,下列特殊方法用于获取\设置\删除\列出属性
"""
使用.或者内置的getattr/setattr/hasattr函数存取属性都会触发下述列表中相应的特殊方法
但是直接通过__dict__读写属性不会触发,如果需要,通常使用这种方式跳过特殊方法
obj.attr和getattr(obj,'attr',42)都会触发 Class.__getattribute__(obj,'attr')特殊方法
__delattr__(self,name):
使用del 删除属性就会调用这个方法
del obj.attr 调用Class.__delattr__(obj,'attr')
__dir__(self)
把对象传给dir()时调用,列出属性
dir(obj)触发 Class.__dir__(obj)方法
__getattr__(self,name):
仅当获取指定的属性失败,搜索过obj,Class,和超类之后调用
__getattribute__(self,name):
尝试获取指定的属性时,总会调用这个方法,不过寻找的属性是特殊属性或特殊方法时除外
.号和getattr(),hasattr()会触发这个方法
调用这个方法触发AttributeError时,调用__getattr__()
为了在获取obj实例属性时不导致无限递归,__getattribute__方法的实现要使用super().__getattribute__(obj,name)
__srtsttr__(self,name,value):
尝试设置指定的属性时总会调用这个方法
obj.attr = 42和setattr(obj,'attr',42)都会触发 Class.__setattr__(obj,'attr',value)
"""
# 本章总结:
"""
本章的话题是动态属性编程"""
# 示例19-2 osconfeed.py:下载osconfeed.json
# 示例19-2 osconfeed.py:下载osconfeed.json
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,encoding='utf-8') as fp:
# 解析json文件,返回python原生对象,在这个数据源中,有几种数据类型:
# dict,list,str,int
return json.load(fp)
if __name__ == '__main__':
feed=load()
print(sorted(feed['Schedule'].keys()))
for key,value in sorted(feed['Schedule'].items()):
print('{:3} {}'.format(len(value),key))
print(feed['Schedule']['speakers'][-1]['name'])
print(feed['Schedule']['speakers'][-1]['serial'])
print(feed['Schedule']['events'][-1]['name'])
print(feed['Schedule']['events'][40]['speakers'])
# 示例19-5 explore0.py:把一个json对象装换成一个嵌套着FrozenJSON对象,列表.和简单类型的FrozenJSON对象
# 19-5 explore0.py:把一个json对象装换成一个嵌套着FrozenJSON对象,列表.和简单类型的FrozenJSON对象
from collections import abc
class FrozenJSON:
'''一个只读接口,使用属性表示法访问Json对象'''
def __init__(self,mapping):
# dict(mapping)确保传入的是字典,或者能被转换成字典的对象
# 安全起见,创建一个副本
self.__data = dict(mapping)
def __getattr__(self, name):
'''仅当没有指定名称(name)的属性时,才调用这个方法'''
if hasattr(self.__data,name):
# 如果name是__data的属性,则返回这个属性
return getattr(self.__data,name)
else:
# 否则,从self.__data中获取name键对应的元素,返回调用FrozenJSON.build
# 得到的结果
return FrozenJSON.build(self.__data[name])
@classmethod
def build(cls,obj): # 这是一个备选的构造方法
if isinstance(obj,abc.Mapping): # 如果obj是映射
return cls(obj)
elif isinstance(obj,abc.MutableSequence):# 如果是MutableSequence(列表)
return [cls.build(item) for item in obj]
else: return obj # 既不是字典也不是列表
if __name__ == '__main__':
from osconfeed import load
raw_feed = load()
# 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON对象
feed = FrozenJSON(raw_feed)
# FrozenJSON实例能使用属性表示法遍历嵌套的字典
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
for key,value in sorted(feed.Schedule.items()):
print('{:3} {}'.format(len(value),key))
print(feed.Schedule.speakers[-1].name)
talk = feed.Schedule.events[0]
print(type(talk))
print(talk.speakers)
print(talk.flavor) # KeyError: 'flavor'
# 19-6 explore1.py:在名称为python关键字的属性后面加上_
# 19-6 explore1.py:在名称为python关键字的属性后面加上_
import keyword
from collections import abc
class FrozenJSON:
'''一个只读接口,使用属性表示法访问Json对象'''
def __init__(self, mapping):
self.__data = {}
for key,value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value
def __getattr__(self, name):
'''仅当没有指定名称(name)的属性时,才调用这个方法'''
if hasattr(self.__data, name):
# 如果name是__data的属性,则返回这个属性
return getattr(self.__data, name)
else:
# 否则,从self.__data中获取name键对应的元素,返回调用FrozenJSON.build
# 得到的结果
return FrozenJSON.build(self.__data[name])
@classmethod
def build(cls, obj): # 这是一个备选的构造方法
if isinstance(obj, abc.Mapping): # 如果obj是映射
return cls(obj)
elif isinstance(obj, abc.MutableSequence): # 如果是MutableSequence(列表)
return [cls.build(item) for item in obj]
else:
return obj # 既不是字典也不是列表
if __name__ == '__main__':
from osconfeed import load
raw_feed = load()
# 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON对象
feed = FrozenJSON(raw_feed)
# FrozenJSON实例能使用属性表示法遍历嵌套的字典
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
for key, value in sorted(feed.Schedule.items()):
print('{:3} {}'.format(len(value), key))
print(feed.Schedule.speakers[-1].name)
talk = feed.Schedule.events[0]
print(type(talk))
print(talk.speakers)
print(talk.flavor) # KeyError: 'flavor'
# 实例19-7 explore2.py:使用__new__方法取代build方法
# 实例19-7 explore2.py:使用__new__方法取代build方法
import keyword
from collections import abc
class FrozenJSON:
def __new__(cls, arg):
if isinstance(arg, abc.Mapping): # 如果obj是映射
return super().__new__(cls)
elif isinstance(arg, abc.MutableSequence): # 如果是MutableSequence(列表)
return [cls(item) for item in arg]
else:
return arg # 既不是字典也不是列表
def __init__(self, mapping):
self.__data = {}
for key,value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value
def __getattr__(self, name):
'''仅当没有指定名称(name)的属性时,才调用这个方法'''
if hasattr(self.__data, name):
# 如果name是__data的属性,则返回这个属性
return getattr(self.__data, name)
else:
# 否则,从self.__data中获取name键对应的元素,返回调用FrozenJSON.build
# 得到的结果
return FrozenJSON(self.__data[name])
if __name__ == '__main__':
from osconfeed import load
raw_feed = load()
# 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON对象
feed = FrozenJSON(raw_feed)
# FrozenJSON实例能使用属性表示法遍历嵌套的字典
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
for key, value in sorted(feed.Schedule.items()):
print('{:3} {}'.format(len(value), key))
print(feed.Schedule.speakers[-1].name)
talk = feed.Schedule.events[0]
print(type(talk))
print(talk.speakers)
print(talk.flavor) # KeyError: 'flavor'
# 示例19-9 schedule1.py:访问保存再shelve.Shelf对象里的OSCON数据
# 示例19-9 schedule1.py:访问保存在shelve.Shelf对象里的OSCON数据
import warnings
import osconfeed
DB_NAME = 'data/schedule_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)
if __name__ == '__main__':
import shelve
db = shelve.open(DB_NAME)
if CONFERENCE not in db:
load_db(db)
speaker = db['speaker.3471']
print(speaker)
print(speaker.name, speaker.twitter)
db.close()
# schedule2.py 使用特性获取链接的记录
# schedule2.py 使用特性获取链接的记录
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
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:
raise
def __repr__(self):
if hasattr(self,'serial'):
cls_name = self.__class__.__name__
return '<{} serial = {!r}>'.format(cls_name,self.serial)
else:
return super().__repr__()
# 重要的event类
class Event(DbRecord):
@property
def venue(self):
key = 'venue.{}'.format(self.venue_serial)
return self.__class__.fetch(key)
@property
def speakers(self):
if not hasattr(self,'_speaker_objs'):
spkr_serial = self.__dict__['speakers']
fetch = self.__class__.fetch
self._speaker_objs = [fetch('speaker.{}'.format(key))
for key in spkr_serial]
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__()
# 重写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'] = key
db[key] = factory(**record)
# 19.2.1 LineItem类第一版:表示订单中商品的类
# 示例19-15 bulkfood_v1.py:最简单的LineItem类
class LineItem:
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
if __name__ == '__main__':
raisins = LineItem('Golden raisins',10,6.95)
print(raisins.subtotal())
# 这个简单的类存在的问题
raisins.weight = -20 # 无效输入...
print(raisins.subtotal()) # 无效输出...
# 示例19-17 bulkfood_v2.py:定义了weight特性的LineItem类
# 示例19-17 bulkfood_v2.py:定义了weight特性的LineItem类
class LineItem:
def __init__(self,description,weight,price):
self.description = description
self.weight = weight # ①
self.price = price
def subtotal(self):
return self.weight * self.price
@property # @property装饰读值方法
def weight(self): # 实现特性的方法,其名称与 ①处的公开属性名称一样--weight
return self.__weight # 真正的值存储在私有属性__weight中
@weight.setter # 被装饰的读值方法有个setter属性,这个属性也是一个装饰器,这个装饰器把读值方法和设值方法绑定在一起
def weight(self,value):
if value>0:
self.__weight = value
else:
raise ValueError('value must be > 0 ')
if __name__ == '__main__':
raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.subtotal())
raisins.weight = -20 # 抛出异常
# 示例19-17 bulkfood_v2b.py:不使用装饰器
# 示例19-17 bulkfood_v2b.py:不使用装饰器
class LineItem:
def __init__(self,description,weight,price):
self.description = description
self.weight = weight # ①
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self): # 实现特性的方法,其名称与 ①处的公开属性名称一样--weight
return self.__weight # 真正的值存储在私有属性__weight中
def set_weight(self,value):
if value>0:
self.__weight = value
else:
raise ValueError('value must be > 0 ')
weight = property(get_weight,set_weight) # 构建property对象,然后赋值给公开的类属性
if __name__ == '__main__':
raisins = LineItem('Golden raisins', 10, 6.95)
print(raisins.subtotal())
raisins.weight = -20 # 抛出异常
# 示例 19-24 bulkfood_v2prop.py:quantity特性工厂函数
# 示例 19-24 bulkfood_v2prop.py:quantity特性工厂函数
def quantity(storage_name):
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance,value):
if value > 0 :
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be >0')
return property(qty_getter,qty_setter)
class LineItem:
weight = quantity('weight')
price = quantity('price')
def __init__(self,description,weight,price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
if __name__ == '__main__':
nutmeg = LineItem('Moluccan nutmeg',8,13.95)
print(nutmeg.weight, nutmeg.price)
print(sorted(vars(nutmeg).items()))
# 示例19-26 blackknight.py:定义特性删除方法
# 示例19-26 blackknight.py:定义特性删除方法
class Blackknight:
def __init__(self):
self.members = ['an arm','another arm','a leg','another leg']
self.phrases = ["'Tis but a scratch.",
"'It's just a flesh wound.",
"I'm invincible",
"All right,we'll call it a draw."]
@property
def member(self):
print('next member is:')
return self.members[0]
@member.deleter
def member(self):
text = 'BLACK KNIGHT (loses {})\n--{}'
print(text.format(self.members.pop(0),self.phrases.pop(0)))
if __name__ == '__main__':
knight = Blackknight()
print(knight.member)
del knight.member
del knight.member
del knight.member
del knight.member
35岁学python,也不知道为了啥?