python 字典赋值_高质量python代码:按需生成属性

写在前面:内容参照自《Effective Python》,其实你完全可以直接去看书,什么?你不想自己看书,那么你也可以关注我,我会不定期从书中挑出常用到的有效方法分享出来,这样你就可以一边刷头条,一边学习知识,岂不美哉。

正文

其实这篇的标题应该为:用 __getattr__、__getattribute__ 和 __setattr__ 实现按需生成的属性,只是头条标题字数有限制,只能简短写了。个人觉得这篇内容很重要,像 Django 就大量应用元类编程方面的东西,还有些时候你阅读其他大佬写的代码,你会发现有很多应用到本篇所讲的内容,所以建议耐心阅读。

Python 语言提供了一些挂钩,使得开发者很容易就能编写出通用的代码,以便将多个系统黏合起来。例如,我们要把数据库的行(row)表示为 Python 对象。由于数据库有自己的一套结构(schema),也称架构、模式、纲要、概要、大纲,所以在操作与行相对应的对象时,我们必须知道这个数据库的结构。然而,把 Python 对象与数据库相连接的这些代码,却不需要知道行的结构,所以,这部分代码应该写得通用一些。

那么,如何实现这种通用的代码呢?普通的实例属性、@property 方法和描述符,都不能完成此功能,因为它们都必须预先定义好,而像这样的动态行为,则可以通过 Python 的__getattr__特殊方法来做。如果某个类定义了__getattr__,同时系统在该类对象的实例字典中又找不到待查询的属性,那么,系统就会调用这个方法。

c244b8b584f92ff9b95daa01dec02408.png

下面,访问 data 对象所缺失的 foo 属性。这会导致 Python 调用刚才定义的 __getattr__ 方法,从而修改实例的dict字典:

0326f94ab2f6614521d05db83d1c4993.png

然后,给 LazyDB 添加记录功能,把程序对 __getattr__ 的调用行为记录下来。请注意,为了避免无限递归,我们需要在 LoggingLazyDB 子类里面通过 super()__.getatr__() 来获取真正的属性值。

14923148a706c35cdaa84fe5a25a0925.png

由于exists属性本身就在实例字典里面,所以访问它的时候,绝不会触发 __getattr__。而foo属性刚开始并不在实例字典中,所以初次访问的时候会触发 __getattr__。由于 __getattr__ 又会调用 __setattr__ 方法,并把 foo 放在实例字典中,所以第二次访问 foo 的时候,就不会再触发 __getattr__ 了。

这种行为非常适合实现无结构数据(schemaless data,无模式数据)的按需访问。初次执行__getattr__的时候进行一些操作,把相关的属性加载进来,以后再访问该属性时,只需从现有的结果之中获取即可。

现在假设我们还要在数据库系统中实现事务(transaction,交易)处理。用户下次访问某属性时,我们要知道数据库中对应的行是否依然有效,以及相关事务是否依然处于开启状态。这样的需求,无法通过__getattr__挂钩可靠地实现出来,因为 Python 系统会直接从实例字典的现存属性中迅速查出该属性,并返回给调用者。

为了实现此功能,我们可以使用 Python 中的另外一个挂钩,也就是__getatribute__。程序每次访问对象的属性时,Python 系统都会调用这个特殊方法,即使属性字典里面已经有了该属性,也依然会触发 __getattribute__ 方法。这样就可以在程序每次访问属性时,检查全局事务状态。下面定义的这个 ValidatingDB 类,会在 __getatribute__ 方法里面记录每次调用的时间。

7c46684d36cb3a47ebf455b6153a3404.png

按照 Python 处理缺失属性的标准流程,如果程序动态地访问了一个不应该有的属性,那么可以在 __getattr__ 和 __getattribute__ 里面抛出 AttributeError 异常。

2a545ef6f1061e1331c3ed745bf688a4.png

实现通用的功能时,我们经常会在 Python 代码里使用内置的 hasattr 函数来判断对象是否已经拥有了相关的属性,并用内置的 __getattr__ 函数来获取属性值。这些函数会先在实例字典中搜索待查询的属性,然后再调用 __getattr__。

769ad482ac585585aab81a2e7f57b212.png
d2512bb1953f35a9b3cc61516e671378.png

现在,假设当程序把值赋给 Python 对象之后,我们要以惰性的方式将其推回数据库。此功能可以用 Python 所提供的 __setattr__ 挂钩来实现,它与前面所讲的那两个挂钩类似,可以拦截对属性的赋值操作。但是与 __getattr__ 和 __getattribute__ 不同的地方在于,我们不需要分成两个方法来处理。只要对实例的属性赋值,无论是直接赋值,还是通过内置的 __setattr__ 函数赋值,都会触发 __setatr__ 方法。

5a71c0f0ec2eb03d12441341c1a1a2b6.png

下面定义的这个 LoggingSavingDB 类,是 SavingDB 的子类,每次对它的属性赋值时,都会触发 __setattr__ 方法。

052ba4e5ab37eea55f4b5f404da9f0d1.png

使用 __getattribute__ 和 __setattr__ 挂钩方法时要注意:每次访问对象属性时,它们都会触发,而这可能并不是你想要的效果。例如,我们想在查询对象的属性时,从对象内部的一份字典里面,搜寻与待查属性相关联的属性值。

350951068393604aaa9009abfc273658.png

上面这段代码,会在 __getattribute__ 方法里面访问 self._data。试着运行一下,你就会发现:这段代码将导致 Python 程序反复递归,从而令其突破最大的栈深度并崩溃。

212845ebd5400e3fb93792e821d58b2a.png

问题在于,__getatribute__会访问 self._data,而这就意味着需要再次调用__getattribute__,然后它又会继续访问 self._data,并无限循环。解决办法是采用super().__getatribute__() 方法,从实例的属性字典里面直接获取 _data 属性值,以避免无限递归。

16d8bbfadebdc9d0c8effb8df041e8e8.png

与之类似,如果要在 __setattr__ 方法中修改对象的属性,那么也需要通过 super().__setattr__() 来完成。

要点

  • 通过 __getattr__ 和 __setatr__,我们可以用惰性的方式来加载并保存对象的属性。
  • 要理解 __getattr__ 与 __getattribute__ 的区别:前者只会在待访问的属性缺失时触发,而后者则会在每次访问属性时触发。
  • 如果要在 __getattribute__ 和 __setattr__ 方法中访问实例属性,那么应该直接通过super()(也就是object类的同名方法)来做,以避免无限递归。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值