特性至关重要的地方在于,特性的存在使得开发者可以非常安全并且确定可行地将公共数据属性作为类的公共接口的一部分开放出来。
在 Python 中,数据的属性和处理数据的方法统称属性(attribute)。其实,方法只是可调用的属性。除了这二者之外,我们还可以创建特性(property),在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。
除了特性,Python 还提供了丰富的 API,用于控制属性的访问权限,以 及实现动态属性。使用点号访问属性时(如 obj.attr),Python 解释 器会调用特殊的方法(如 __getattr__ 和 __setattr__)计算属性。 用户自己定义的类可以通过 __getattr__ 方法实现“虚拟属性”,当访 问不存在的属性时(如obj.no_such_attribute),即时计算属性的值。
仅当无法使用常规的方式获取属性(即在实例、类或超类中找不到指定的属性),解释器才会调用特殊的 __getattr__ 方法。
例子,使用动态属性访问JSON类数据
from urllib.request import urlopen
import warnings
import os
import json
from collections import abc
import keyword
from pprint import pprint
class FronzenJSON:
def __init__(self, mapping):
# 生成一个新的副本
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
if not key.isidentifier():
raise Exception('{}不是有效的python标识符'.format(key))
self.__data[key] = value
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FronzenJSON.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
grad = FronzenJSON({'2be': 'or not'})
# print(grad.2be)
注意事项: 1. 属性不能为关键字 2. 属性需要是有效的标识符
使用__new__方法创建对象
我们通常把 __init__ 称为构造方法,这是从其他语言借鉴过来的术 语。其实,用于构建实例的是特殊方法 __new__:这是个类方法(使用 特殊方式处理,因此不必使用 @classmethod 装饰器),必须返回一个 实例。返回的实例会作为第一个参数(即 self)传给 __init__ 方 法。因为调用 __init__ 方法时要传入实例,而且禁止返回任何值,所 以 __init__ 方法其实是“初始化方法”。真正的构造方法是 __new__。 我们几乎不需要自己编写 __new__ 方法,因为从 object 类继承的实现 已经足够了。
new方法也可以返回其他类的实例,此时,解释器不会调用 init方法。
伪代码表示如下
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')
shelve模块用来存放数据的
schedule1.py(包含一个重要的小技巧)
import warnings
import osconfeed
DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):
"""
Record.__init__ 方法展示了一个流行的 Python 技巧。我们知道,对
象的 __dict__ 属性中存储着对象的属性——前提是类中没有声明
__slots__ 属性,如 9.8 节所述。因此,更新实例的 __dict__ 属性,
把值设为一个映射,能快速地在那个实例中创建一堆属性。
"""
self.__dict__.update(kwargs)
def load_db(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)
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__()
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_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__()
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)