流畅的python笔记(十九)动态属性和特性

目录

前言

一、使用动态属性转换数据

接下来示例中使用JSON格式数据源

使用动态属性访问JSON类数据

处理无效属性名

使用__new__方法以灵活的方式创建对象

使用shelve模块调整OSCON的JSON数据源的结构

使用特性获取链接的记录

二、使用特性验证属性

LineItem类第一版:表示订单中商品的类

LineItem类第二版:能验证值的特性

三、特性全解析

property类

特性会覆盖实例属性

特性的文档

四、定义一个特性工厂

五、处理属性删除操作

六、处理属性的重要属性和函数

影响属性处理方式的特殊属性

处理属性的内置函数

处理属性的特殊方法


前言

python中,数据的属性和处理数据的方法统称属性attribute,即方法只是可调用的属性。除了这二者之外,我们还可以创建特性property,在不改变类接口的前提下,使用存取方法(即读值方法和设值方法)修改数据属性。

        python使用点号访问属性时,比如obj.attr,python解释器会调用特殊的方法(如__getattr__和__setattr__)计算属性。用户自定义的类可以通过__getattr__方法实现虚拟属性,当访问不存在的属性时,即时计算属性的值。

        动态创建属性是一种元编程,框架的作者经常这么做。然而在python中相关的技术十分简单,任何人都可以使用。

一、使用动态属性转换数据

接下来示例中使用JSON格式数据源

图示JSON数据源中只列出了4条记录。整个数据集是一个JSON对象,里边有一个键"Schedule",这个键对应的值也是一个映像(相当于字典),有四个键:"conferences"、"events"、"speakers"和"venues"。这4个键对应的值都是一个记录列表,示例中各个列表中只有一条记录,每条记录都是一个字典。在完整的数据集中,列表中有成百上千条记录。不过"conferences"键对应的列表中只有一条记录。这四个列表中每条记录都有一个名为"serial"的字段,这是元素在各个列表中的唯一标识符。

        第一个脚本:下载JSON数据源。

  1.  如果需要下载,就发出提醒信息。
  2.  在with语句中使用两个上下文管理器,分别用于读取和保存远程文件。即把url的远程内容读取出来然后写入本地的JSON文件。
  3.  json.load函数解析JSON文件,返回python原生对象。在这个数据源中有这几种数据类型:dict、list、str、int。

有了以上代码,就可以审查JSON数据源中的任何字段:

  1.  feed = json.load(fp),feed是JSON的解析结果,feed本质是python的原生类型对象,即一个字典,里边嵌套着字典和列表,存储着字符串和整数。
  2.  列出"Schedule"键中的4个记录集合。
  3.  显示4个记录集合中的记录数量。
  4.  深入嵌套的字典和列表,获取最后一个演讲者的名字。
  5.  获取那位演讲者的编号。
  6.  每个事件都有一个'speakers'字段,列出0个或多个演讲者的编号。

使用动态属性访问JSON类数据

上边示例中feed字典要用feed['Schedule']['events'][40]['name']来获取到具体的某个字段值,这种句法很冗长。JavaScript中,可以使用feed.Schedule.events[40].name获取那个值。下面用python实现了一个FrozenJson类可达到同样效果,但是只支持读取数据,该类能递归,自动处理嵌套的字典和列表。

  1. 传入嵌套的字典和列表组成的raw_feed,创建一个FrozenJSON实例。
  2. FrozenJSON实例能使用属性表示法遍历嵌套的字典,这里我们speakers列表中的元素数量,其中的元素都是字典。
  3. 使用底层字典的方法,.keys(),获取字典中所有的键。
  4. 使用items方法,获取字典中各个元素组成的列表 [key, value],然后显示各个集合中的元素数量,这里的value是一个列表,列表中的各个元素都是字典。
  5. feed.Schedule.speakers仍是列表,但如果列表里的元素是映射,即speakers[-1]是映射,则会被转换成FrozenJSON对象。
  6. events列表中的40号元素是一个字典,现在变成了一个FrozenJSON实例。
  7. talk是events列表中的第40个元素,talk本质是字典,其有一个speakers键,对应值是列表。
  8. 读取不存在的属性会抛出KeyError异常,而不是通常抛出的AttributeError异常。

下边是FrozenJSON对象的实现:

  1. 使用mapping参数创建一个字典,作为FrozenJSON类的实例属性。这样做目的有二:一是确保传入的是字典或者能转成成字典的对象,二是为安全起见创建了一个副本。
  2. 仅当按实例属性---类属性---继承树搜寻不到指定名称的属性(数据属性和方法统称为属性)时才调用__getattr__方法。
  3. 如果name是FrozenJSON类的实例属性__data的属性,返回那个属性。
  4. 否则,从self.__data中获取name键对应的元素,返回调用FrozenJSON.build()方法得到的结果。
  5. 用@classmethod修饰的方法是类方法,经常用这种方法来实现一个备选构造方法,比如这里的build就是一个备选构造方法。
  6. 如果obj是映射,那么就构建一个FrozenJSON对象,
  7. 如果是MutableSequence对象,这里必然是列表,因此把列表obj中的每个元素递归地传给build方法,列表推导式构建出一个列表。
  8. 如果既不是字典也不是列表,那么原封不动地返回元素。

处理无效属性名

FrozenJSON类有个缺陷:没有对名称为python关键字地属性做特殊处理,比如以如下方式构建一个对象:

此时用grad.class是无法读取对应的值的,因为在python中class是保留字:

这个时候可以用getattr(grad, 'class')来读取对应的属性:

但是Frozen类的目的是为了便于用 '.' 来访问数据,因此更好的方法是检查传给FrozenJSON.__init__方法的映射中是否有键的名称是关键字,如果有,那么在键名后加上_,然后就可以通过以下方式读取了:

FrozenJSON类的__init__方法改成如下所示:

 

  1.  先导入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__方法实现。

 

  1.  __new__是类方法,第一个参数是类本身,余下的参数与__init__方法一样,只不过没有self。
  2. 默认的行为是委托给超类的__new__方法,这里调用的是object基类的__new__方法,唯一的参数cls是FrozenJSON。
  3. __new__方法中余下的代码与原来的build一样。
  4. 这里之前调用的是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']

  1. shelve.open函数打开现有的数据库文件,或者新建一个。
  2. 判断数据库是否填充的简便方法是,检查某个已知的键是否存在,这里检查的是conference.115,即conference记录的键。
  3. 如果数据库是空的,就调用load_db(db),加载数据。
  4. 获取一条speaker记录。
  5. speaker是Record类的实例。
  6. 各个Record实例都有一系列自定义的属性,对应于底层JSON记录里的字段。
  7. 一定要关闭shelve.Shelf对象,如果可以,使用with块确保Shelve对象会关闭。

 schedule1.py脚本的代码如下所示:

  1.  导入osconfeed模块,用于解析JSON文件,返回python原生对象。
  2.  这是使用关键字参数传入的属性构建实例的常用方式。因为__dict__属性中存储着实例的所有属性及对应值,这里直接传入映射对象来更新__dict__可以快速添加大量属性。
  3.  如果本地没有副本,从网上下载JSON数据源。
  4.  迭代Schedule关键字对应的值,该值也是个字典,其键包括'conferences'、'events'等。
  5.  record_type的值是去掉尾部's'后的名字,即把'events'变成'event'。
  6.  使用record_type和‘serial’字段构成key。
  7.  把'serial'字段的值设成完整的键。
  8.  构建Record实例,存储在数据库的key键名下。

使用特性获取链接的记录

下一版本目标:对于从Shelf对象中获取的event记录来说,读取它的venue或speakers属性时返回的不是编号,而是完整的对象。用法如下所示:

  1. DbRecord类扩展Record类,添加对数据库的支持:为了操作数据库,必须为DbRecord提供一个数据库的引用。
  2. DbRecord.fetch类方法能获取任何类型的记录。
  3. event时Event类的实例,而Event类扩展DbRecord类。
  4. event.venue返回一个DbRecord实例。
  5. 获取event.venue的名称。
  6. 还可以迭代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属性,是因为:特性是用于管理实例属性的类属性。 即特性本身是类属性,但是是用来管理实例属性的。

  1. 导入inspect模块,在load_db函数中使用。
  2. 数据库文件。
  3. 定义__eq__方法。

 

  1. 自定义的异常通常是标志类,没有定义体。写一个文档字符串,说明异常的用途,比只写一个pass语句要好。
  2. DbRecord类扩展Record类。
  3. __db类属性存储一个打开的shelve.Shelf数据库引用。
  4. set_db是静态方法。
  5. 设置__dp属性。
  6. get_db是静态方法。
  7. fetch是类方法。
  8. 从数据库中获取ident键对应的记录。
  9. 弱捕获到TypeError异常,并且db变量的值是None,抛出自定义的异常,说明必须设置数据库。
  10. 否则重新抛出TypeError异常,因为我们不知道怎么处理。
  11. 如果记录有serial属性,在字符串表示形式中使用。
  12. 否则调用继承的__repr__方法。

  1. Event类扩展DbRecord类。
  2. 在venue特性中使用venue_serial属性构建key,然后传给继承自DbRecord类的fetch类方法。
  3. speakers特性检查记录是否有_speaker_objs属性。
  4. 如果没有,直接从__dict__实例属性中获取'speakers'属性的值,防止无线递归,因为这个特性的公开名称也是speakers。
  5. 获取fetch类方法的引用。
  6. 使用fetch获取speaker记录列表,然后赋值给self._speaker_objs。
  7. 返回前边获取的列表。
  8. 如果记录有name属性,在字符串表示形式中使用。
  9. 否则,调用继承的__repr__方法。

 

 

二、使用特性验证属性

我们已知使用@property装饰器可以实现只读特性,本节要创建一个可读写的特性。

LineItem类第一版:表示订单中商品的类

这个实现中,如果错误地把重量设为负值,则计算出的金额为负值。解决这个问题有两种方案,一是使用读值方法和设值方法管理weight属性,二是把数值属性换成特性。

LineItem类第二版:能验证值的特性

 

  1.  这里已经使用特性的设值方法了,确保所创建实例的weight属性不能为负值。
  2. @property装饰器修饰读值方法。
  3. 实现特性的方法,其名称与公开属性的名称一样------weight。
  4. 真正的值存储在私有属性__weight中。
  5. 被装饰的读值方法有个.setter属性,这个属性也是装饰器,这个装饰器把读值方法和设值方法绑定在一起。
  6. 如果值大于0,设置私有属性__weight。
  7. 否则,抛出ValueError异常。

三、特性全解析

property类

虽然内置的property经常用作装饰器,但它本质上是一个类。在python中,类和函数通常可以互换,因为二者都是都是可调用的对象(调用类即实例化一个对象),由于python没有实例化对象的new运算符,因此调用构造函数和调用工厂函数没有区别。只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器。

由于@装饰器语法比property出现的晚,因此最早的时候都是通过把读值与设值函数传给property类的前两个参数来实现特性。

 

  1. 普通的读值方法。
  2. 普通的设值方法。
  3. 构造property对象,然后赋值给公开的类属性。

特性会覆盖实例属性

特性都是类属性(有点奇怪,特性中明明使用了self,却还是类属性),但是特性管理的是实例属性的存取

第一个例子:实例属性遮盖类属性

  1. 定义Class类,其有两个类属性:data数据属性和prop特性。
  2. vars函数返回对象obj的__dict__属性,结果显示没有实例属性。
  3. 读取obj.data,获取的是Class.data类属性的值。
  4. 为obj.data赋值,创建了一个实例属性。
  5. 用vars函数审查实例,查看实例属性。
  6. 再次读取obj.data,获取的是实例属性的值。即实例属性data遮盖了类属性data。
  7. Class.data的值完好无损。

第二个例子:实例属性不会遮盖类特性

  1.  直接从Class中读取prop特性,获取的是特性对象本身,不会运行特性的读值方法。
  2.  读取obj.prop会执行特性的读值方法。
  3.  尝试设置prop实例属性,结果失败了。
  4.  但是可以直接把‘prop’存入obj.__dict__,从而创建prop实例属性。
  5.  审查对象,看到了data和prop两个实例属性。
  6.  但是读取obj.prop时仍会运行特性的读值方法,特性并没有被同名的实例属性覆盖。
  7.  直接在类属性上覆盖Class.prop,销毁特性对象。
  8.  现在,obj.prop获取的是实例属性,因为Class.prop变成了普通的类属性,不再是特性了,因此被实例属性所覆盖。

第三个例子:为Class类新添一个特性,覆盖实例属性

  1. obj.data获取实例属性data。
  2. Class.data获取类属性。
  3. 使用新特性覆盖Class.data。
  4. 现在,obj.data被Class.data特性覆盖了。
  5. 删除特性。
  6. obj.data恢复原样,仍然获取实例属性。

本节主要观点:obj.attr这样的表达式不是从obj开始寻找attr的,而是从obj.__class__开始。仅当类中没有名为attr的特性时,python才会在obj实例中寻找。

特性的文档

控制台中help()函数或者IDE工具要显示特性的文档时,会从特性的__doc__属性中提取信息。

  • 可以在调用property类的时候直接为特性对象设置文档字符串,只需传入__doc__参数即可:

  •  使用装饰器创建property对象时,读值方法的文档字符串作为一个整体,变成特性的文档。

四、定义一个特性工厂

如果我们的类中有两个实例属性都需要用特性来管理,那么对两个属性都分别实现读值与设值方法会使得代码重复,因此需要定义特性工厂函数quantity。比如下边例子中self.weight和self.price都需要用特性进行管理:

  1. 使用工厂函数把第一个自定义的特性weight定义为类属性。
  2. 第二次调用,构建另一个自定义的特性price。
  3. 这里,特性已经激活,确保不能把weight设为负数或零。
  4. 这里也用到了特性,使用特性获取实例中存储的值。

下面是quantity特性工厂函数的实现:

  1.  storage_name参数确定各个特性的数据存在哪里,其实就是对应实例的数据属性的变量名。
  2. qty_getter函数的第一个参数instance直到要把属性存储其中的LineItem实例(即存储特性的管理的数据属性的那个实例)。
  3. qty_getter引用了storage_name,将其保存在qty_getter的闭包中,值直接从instance.__dict__中获取,为的是跳过特性,防止无线递归。因为直接instance.attr搜寻属性时会先找特性然后才是实例属性、类属性等。
  4. 定义qty_setter函数,第一个参数也是instance。
  5. 值直接存到instance.__dict__中,这也是为了跳过特性。
  6. 传入读值方法与取值方法,构建一个自定义的特性对象,然后将其返回。

        值得注意的一点是,引用self.weight或obj.weight都由特性函数处理,只有直接存取__dict__属性才能跳过特性。

        下面示例创建并审查一个LineItem示例,说明存储值的是哪个属性:

  1. 通过特性读取weight和price,这会遮盖同名实例属性。
  2. 使用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)方法。

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值