目录
前言
python中,数据的属性和处理数据的方法统称属性attribute,即方法只是可调用的属性。除了这二者之外,我们还可以创建特性property,在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。
python使用点号访问属性时,比如obj.attr,python解释器会调用特殊的方法(如__getattr__和__setattr__)计算属性。用户自定义的类可以通过__getattr__方法实现虚拟属性,当访问不存在的属性时,即时计算属性的值。
动态创建属性是一种元编程,框架的作者经常这么做。然而在python中相关的技术十分简单,任何人都可以使用。
一、使用动态属性转换数据
接下来示例中使用JSON格式数据源
图示JSON数据源中只列出了4条记录。整个数据集是一个JSON对象,里边有一个键"Schedule",这个键对应的值也是一个映像(相当于字典),有四个键:"conferences"、"events"、"speakers"和"venues"。这4个键对应的值都是一个记录列表,示例中各个列表中只有一条记录,每条记录都是一个字典。在完整的数据集中,列表中有成百上千条记录。不过"conferences"键对应的列表中只有一条记录。这四个列表中每条记录都有一个名为"serial"的字段,这是元素在各个列表中的唯一标识符。
第一个脚本:下载JSON数据源。
- 如果需要下载,就发出提醒信息。
- 在with语句中使用两个上下文管理器,分别用于读取和保存远程文件。即把url的远程内容读取出来然后写入本地的JSON文件。
- json.load函数解析JSON文件,返回python原生对象。在这个数据源中有这几种数据类型:dict、list、str、int。
有了以上代码,就可以审查JSON数据源中的任何字段:
- feed = json.load(fp),feed是JSON的解析结果,feed本质是python的原生类型对象,即一个字典,里边嵌套着字典和列表,存储着字符串和整数。
- 列出"Schedule"键中的4个记录集合。
- 显示4个记录集合中的记录数量。
- 深入嵌套的字典和列表,获取最后一个演讲者的名字。
- 获取那位演讲者的编号。
- 每个事件都有一个'speakers'字段,列出0个或多个演讲者的编号。
使用动态属性访问JSON类数据
上边示例中feed字典要用feed['Schedule']['events'][40]['name']来获取到具体的某个字段值,这种句法很冗长。JavaScript中,可以使用feed.Schedule.events[40].name获取那个值。下面用python实现了一个FrozenJson类可达到同样效果,但是只支持读取数据,该类能递归,自动处理嵌套的字典和列表。
- 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON实例。
- FrozenJSON实例能使用属性表示法遍历嵌套的字典,这里我们speakers列表中的元素数量,其中的元素都是字典。
- 使用底层字典的方法,.keys(),获取字典中所有的键。
- 使用items方法,获取字典中各个元素组成的列表 [key, value],然后显示各个集合中的元素数量,这里的value是一个列表,列表中的各个元素都是字典。
- feed.Schedule.speakers仍是列表,但如果列表里的元素是映射,即speakers[-1]是映射,则会被转换成FrozenJSON对象。
- events列表中的40号元素是一个字典,现在变成了一个FrozenJSON实例。
- talk是events列表中的第40个元素,talk本质是字典,其有一个speakers键,对应值是列表。
- 读取不存在的属性会抛出KeyError异常,而不是通常抛出的AttributeError异常。
下边是FrozenJSON对象的实现:
- 使用mapping参数创建一个字典,作为FrozenJSON类的实例属性。这样做目的有二:一是确保传入的是字典或者能转成成字典的对象,二是为安全起见创建了一个副本。
- 仅当按实例属性---类属性---继承树搜寻不到指定名称的属性(数据属性和方法统称为属性)时才调用__getattr__方法。
- 如果name是FrozenJSON类的实例属性__data的属性,返回那个属性。
- 否则,从self.__data中获取name键对应的元素,返回调用FrozenJSON.build()方法得到的结果。
- 用@classmethod修饰的方法是类方法,经常用这种方法来实现一个备选构造方法,比如这里的build就是一个备选构造方法。
- 如果obj是映射,那么就构建一个FrozenJSON对象,
- 如果是MutableSequence对象,这里必然是列表,因此把列表obj中的每个元素递归地传给build方法,列表推导式构建出一个列表。
- 如果既不是字典也不是列表,那么原封不动地返回元素。
处理无效属性名
FrozenJSON类有个缺陷:没有对名称为python关键字地属性做特殊处理,比如以如下方式构建一个对象:
此时用grad.class是无法读取对应的值的,因为在python中class是保留字:
这个时候可以用getattr(grad, 'class')来读取对应的属性:
但是Frozen类的目的是为了便于用 '.' 来访问数据,因此更好的方法是检查传给FrozenJSON.__init__方法的映射中是否有键的名称是关键字,如果有,那么在键名后加上_,然后就可以通过以下方式读取了:
FrozenJSON类的__init__方法改成如下所示:
- 先导入keyword模块,然后用keyword.iskeyword()来判断某个键是不是python保留的关键字。
除了JSON对象中的键是不是python保留的关键字外还有一个问题,传入的键可能不是合法的标识符:
这种不合法的标识符在python3中很容易检测,因为str类提供了s.isidentifier()方法能判断s是否是有效的python标识符,但是把标识符变成有效的属性名不容易,有两种解决方案,一是抛出异常,二是把无效的键换成通用的名称,例如attr_0、attr_1等等。
使用__new__方法以灵活的方式创建对象
我们通常把__init__成为构造方法,但实际上用于构建实例的是特殊方法__new__:这是个类方法(使用特殊方式处理,因此不必使用@classmethod装饰器),必须返回一个实例。__new__返回的实例会作为第一个参数self传给__init__方法,调用__init__方法要传入实例,而且禁止返回任何值,所以__init__方法其实是初始化方法,真正的构造方法是__new__。一般情况下不需要自己编写__new__,因为从object类继承的实现已经够用了。python构建对象的过程可以用下面伪代码表示:
__new__方法也可以返回其他类的实例,此时解释器不会调用__init__方法。可以把FrozenJSON类中的类方法build改成用__new__方法实现。
- __new__是类方法,第一个参数是类本身,余下的参数与__init__方法一样,只不过没有self。
- 默认的行为是委托给超类的__new__方法,这里调用的是object基类的__new__方法,唯一的参数cls是FrozenJSON。
- __new__方法中余下的代码与原来的build一样。
- 这里之前调用的是FrozenJSON.build方法,现在只需调用FrozenJSON构造方法即可。
使用shelve模块调整OSCON的JSON数据源的结构
shelve.open高阶函数返回一个shelve.Shelf实例,这是简单的键值对象数据库,背后由dbm模块支持,具有以下特点:
- shelve.Shelf是abc.MutableMapping的子类,因此提供了处理映射类型的重要方法。
- shelve.Shelf类提供了几个管理IO的方法,如sync和close;它也是一个上下文管理器。
- 只要把新值赋予键,就会保存键和值。
- 键必须是字符串。
- 值必须是pickle模块能处理的对象。
我们讲从JSON文件中读取所有记录,讲其存在一个shelve.Shelf对象中,键由记录类型和编号组成(例如'event.33590'或'speaker.3471'),而值是我们即将定义的Record类的实例。
以下是schedule1.py脚本的doctest,主要工作由load_db函数完成:调用osconfeed.load方法读取JSON数据,把通过db传入的Shelf对象中的各条记录存储为一个个Record实例,这样处理之后获取演讲者的内容:speaker = db['speaker.3471']
- shelve.open函数打开现有的数据库文件,或者新建一个。
- 判断数据库是否填充的简便方法是,检查某个已知的键是否存在,这里检查的是conference.115,即conference记录的键。
- 如果数据库是空的,就调用load_db(db),加载数据。
- 获取一条speaker记录。
- speaker是Record类的实例。
- 各个Record实例都有一系列自定义的属性,对应于底层JSON记录里的字段。
- 一定要关闭shelve.Shelf对象,如果可以,使用with块确保Shelve对象会关闭。
schedule1.py脚本的代码如下所示:
- 导入osconfeed模块,用于解析JSON文件,返回python原生对象。
- 这是使用关键字参数传入的属性构建实例的常用方式。因为__dict__属性中存储着实例的所有属性及对应值,这里直接传入映射对象来更新__dict__可以快速添加大量属性。
- 如果本地没有副本,从网上下载JSON数据源。
- 迭代Schedule关键字对应的值,该值也是个字典,其键包括'conferences'、'events'等。
- record_type的值是去掉尾部's'后的名字,即把'events'变成'event'。
- 使用record_type和‘serial’字段构成key。
- 把'serial'字段的值设成完整的键。
- 构建Record实例,存储在数据库的key键名下。
使用特性获取链接的记录
下一版本目标:对于从Shelf对象中获取的event记录来说,读取它的venue或speakers属性时返回的不是编号,而是完整的对象。用法如下所示:
- DbRecord类扩展Record类,添加对数据库的支持:为了操作数据库,必须为DbRecord提供一个数据库的引用。
- DbRecord.fetch类方法能获取任何类型的记录。
- event时Event类的实例,而Event类扩展DbRecord类。
- event.venue返回一个DbRecord实例。
- 获取event.venue的名称。
- 还可以迭代event.speakers列表,获取表示各位演讲者的DbRecord对象。
本节使用的主要类如下:
- Record,__init__方法与schedule1.py脚本中的一样,为了辅助测试,增加__eq__方法。
- DbRecord,Record的子类,添加__db类属性,用于设置和获取__db属性的set_db和get_db静态方法,用于从数据库中获取记录的fetch类方法,以及辅助调试和测试的__repr__实例方法。
- Event,DbRecord类的子类,添加了用于获取所链接记录的venue和speakers属性,以及特殊的__repr__方法。
DbRecord.__db被设置为私有类属性,然后定义了普通的读值与设值方法,以防不小心覆盖__db属性的值,之所以没有使用特性去管理__db属性,是因为:特性是用于管理实例属性的类属性。 即特性本身是类属性,但是是用来管理实例属性的。
- 导入inspect模块,在load_db函数中使用。
- 数据库文件。
- 定义__eq__方法。
- 自定义的异常通常是标志类,没有定义体。写一个文档字符串,说明异常的用途,比只写一个pass语句要好。
- DbRecord类扩展Record类。
- __db类属性存储一个打开的shelve.Shelf数据库引用。
- set_db是静态方法。
- 设置__dp属性。
- get_db是静态方法。
- fetch是类方法。
- 从数据库中获取ident键对应的记录。
- 弱捕获到TypeError异常,并且db变量的值是None,抛出自定义的异常,说明必须设置数据库。
- 否则重新抛出TypeError异常,因为我们不知道怎么处理。
- 如果记录有serial属性,在字符串表示形式中使用。
- 否则调用继承的__repr__方法。
- Event类扩展DbRecord类。
- 在venue特性中使用venue_serial属性构建key,然后传给继承自DbRecord类的fetch类方法。
- speakers特性检查记录是否有_speaker_objs属性。
- 如果没有,直接从__dict__实例属性中获取'speakers'属性的值,防止无线递归,因为这个特性的公开名称也是speakers。
- 获取fetch类方法的引用。
- 使用fetch获取speaker记录列表,然后赋值给self._speaker_objs。
- 返回前边获取的列表。
- 如果记录有name属性,在字符串表示形式中使用。
- 否则,调用继承的__repr__方法。
二、使用特性验证属性
我们已知使用@property装饰器可以实现只读特性,本节要创建一个可读写的特性。
LineItem类第一版:表示订单中商品的类
这个实现中,如果错误地把重量设为负值,则计算出的金额为负值。解决这个问题有两种方案,一是使用读值方法和设值方法管理weight属性,二是把数值属性换成特性。
LineItem类第二版:能验证值的特性
- 这里已经使用特性的设值方法了,确保所创建实例的weight属性不能为负值。
- @property装饰器修饰读值方法。
- 实现特性的方法,其名称与公开属性的名称一样------weight。
- 真正的值存储在私有属性__weight中。
- 被装饰的读值方法有个.setter属性,这个属性也是装饰器,这个装饰器把读值方法和设值方法绑定在一起。
- 如果值大于0,设置私有属性__weight。
- 否则,抛出ValueError异常。
三、特性全解析
property类
虽然内置的property经常用作装饰器,但它本质上是一个类。在python中,类和函数通常可以互换,因为二者都是都是可调用的对象(调用类即实例化一个对象),由于python没有实例化对象的new运算符,因此调用构造函数和调用工厂函数没有区别。只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器。
由于@装饰器语法比property出现的晚,因此最早的时候都是通过把读值与设值函数传给property类的前两个参数来实现特性。
- 普通的读值方法。
- 普通的设值方法。
- 构造property对象,然后赋值给公开的类属性。
特性会覆盖实例属性
特性都是类属性(有点奇怪,特性中明明使用了self,却还是类属性),但是特性管理的是实例属性的存取。
第一个例子:实例属性遮盖类属性
- 定义Class类,其有两个类属性:data数据属性和prop特性。
- vars函数返回对象obj的__dict__属性,结果显示没有实例属性。
- 读取obj.data,获取的是Class.data类属性的值。
- 为obj.data赋值,创建了一个实例属性。
- 用vars函数审查实例,查看实例属性。
- 再次读取obj.data,获取的是实例属性的值。即实例属性data遮盖了类属性data。
- Class.data的值完好无损。
第二个例子:实例属性不会遮盖类特性
- 直接从Class中读取prop特性,获取的是特性对象本身,不会运行特性的读值方法。
- 读取obj.prop会执行特性的读值方法。
- 尝试设置prop实例属性,结果失败了。
- 但是可以直接把‘prop’存入obj.__dict__,从而创建prop实例属性。
- 审查对象,看到了data和prop两个实例属性。
- 但是读取obj.prop时仍会运行特性的读值方法,特性并没有被同名的实例属性覆盖。
- 直接在类属性上覆盖Class.prop,销毁特性对象。
- 现在,obj.prop获取的是实例属性,因为Class.prop变成了普通的类属性,不再是特性了,因此被实例属性所覆盖。
第三个例子:为Class类新添一个特性,覆盖实例属性
- obj.data获取实例属性data。
- Class.data获取类属性。
- 使用新特性覆盖Class.data。
- 现在,obj.data被Class.data特性覆盖了。
- 删除特性。
- obj.data恢复原样,仍然获取实例属性。
本节主要观点:obj.attr这样的表达式不是从obj开始寻找attr的,而是从obj.__class__开始。仅当类中没有名为attr的特性时,python才会在obj实例中寻找。
特性的文档
控制台中help()函数或者IDE工具要显示特性的文档时,会从特性的__doc__属性中提取信息。
- 可以在调用property类的时候直接为特性对象设置文档字符串,只需传入__doc__参数即可:
- 使用装饰器创建property对象时,读值方法的文档字符串作为一个整体,变成特性的文档。
四、定义一个特性工厂
如果我们的类中有两个实例属性都需要用特性来管理,那么对两个属性都分别实现读值与设值方法会使得代码重复,因此需要定义特性工厂函数quantity。比如下边例子中self.weight和self.price都需要用特性进行管理:
- 使用工厂函数把第一个自定义的特性weight定义为类属性。
- 第二次调用,构建另一个自定义的特性price。
- 这里,特性已经激活,确保不能把weight设为负数或零。
- 这里也用到了特性,使用特性获取实例中存储的值。
下面是quantity特性工厂函数的实现:
- storage_name参数确定各个特性的数据存在哪里,其实就是对应实例的数据属性的变量名。
- qty_getter函数的第一个参数instance直到要把属性存储其中的LineItem实例(即存储特性的管理的数据属性的那个实例)。
- qty_getter引用了storage_name,将其保存在qty_getter的闭包中,值直接从instance.__dict__中获取,为的是跳过特性,防止无线递归。因为直接instance.attr搜寻属性时会先找特性然后才是实例属性、类属性等。
- 定义qty_setter函数,第一个参数也是instance。
- 值直接存到instance.__dict__中,这也是为了跳过特性。
- 传入读值方法与取值方法,构建一个自定义的特性对象,然后将其返回。
值得注意的一点是,引用self.weight或obj.weight都由特性函数处理,只有直接存取__dict__属性才能跳过特性。
下面示例创建并审查一个LineItem示例,说明存储值的是哪个属性:
- 通过特性读取weight和price,这会遮盖同名实例属性。
- 使用vars函数审查nutmeg示例,表明真正用于存储值的是实例属性。
五、处理属性删除操作
对象的属性可以使用del语句删除:
使用python时不常删除属性,通过特性删除属性更少见,但是允许这么做。可以使用@my_property.deleter装饰器装饰一个方法,负责删除特性管理的属性。
六、处理属性的重要属性和函数
影响属性处理方式的特殊属性
- __class__,对象所属类的引用(即obj.__class__与type(obj)的作用相同)。python的某些特殊方法比如__getattr__,只在对象的类中寻找,而不在实例中寻找。
- __dict__,一个映射,存储对象或类的可写属性。有__dict__属性的对象,任何时候都能随意设置新属性。如果类有__slots__属性,它的实例可能没有__dict__属性。
- __slots__,类可以定义这个属性,限制实例能有哪些属性。__slots__属性的值是一个字符串组成的元素,指明允许有的属性。如果__slots__中没有__dict__,那么该类的实例中没有__dict__属性,实例只允许有指定名称的属性。
处理属性的内置函数
- 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对象获取指定的属性,返回True。这个函数的实现方法是调用getattr(object, name)函数,看是否抛出AttributeError异常。
- setattr(object, name, value),把object对象指定属性的值设为value,前提是object对象能接受那个值,这个函数可能会创建一个新属性,或者覆盖现有的属性。
- vars([object]),返回object对象的__dict__属性,如果实例所属的类定义了__slots__属性,实例没有__dict__属性,那么vars函数不能处理那个实例,但是dir函数可以处理。如果没有指定参数,则vars函数与locals函数一定:返回表示本地作用域的字典。
处理属性的特殊方法
在用户自定义类中,使用点号或内置的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')方法,但是仅当找不到指定的属性时才会触发。
- __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)方法。