属性与 @property 方法让你的python更高效


Posted in Python onSeptember 21, 2020

一、用属性替代 getter 或 setter 方法

以下代码中包含手动实现的 getter(get_ohms) 和 setter(set_ohms) 方法:

class OldResistor(object):
  def __init__(self, ohms):
    self._ohms = ohms
    self.voltage = 0
    self.current = 0

  def get_ohms(self):
    return self._ohms

  def set_ohms(self, ohms):
    self._ohms = ohms


r0 = OldResistor(50e3)
print(f'Before: {r0.get_ohms()}')
r0.set_ohms(10e3)
print(f'After: {r0.get_ohms()}')
# => Before: 50000.0
# => After: 10000.0

这些工具方法有助于定义类的接口,使得开发者可以方便地封装功能、验证用法并限定取值范围。
但是在 Python 语言中,应尽量从简单的 public 属性写起:

class Resistor(object):
  def __init__(self, ohms):
    self.ohms = ohms
    self.voltage = 0
    self.current = 0

r1 = Resistor(50e3)
print(f'Before: {r1.ohms}')
r1.ohms = 10e3
print(f'After: {r1.ohms}')
# => Before: 50000.0
# => After: 10000.0

访问实例的属性则可以直接使用 instance.property 这样的格式。

如果想在设置属性的同时实现其他特殊的行为,如在对上述 Resistor 类的 voltage 属性赋值时,需要同时修改其 current 属性。
可以借助 @property 装饰器和 setter 方法实现此类需求:

from resistor import Resistor

class VoltageResistor(Resistor):
  def __init__(self, ohms):
    super().__init__(ohms)
    self._voltage = 0

  @property
  def voltage(self):
    return self._voltage

  @voltage.setter
  def voltage(self, voltage):
    self._voltage = voltage
    self.current = self._voltage / self.ohms


r2 = VoltageResistor(1e3)
print(f'Before: {r2.current} amps')
r2.voltage = 10
print(f'After: {r2.current} amps')
Before: 0 amps
After: 0.01 amps

此时设置 voltage 属性会执行名为 voltage 的 setter 方法,更新当前对象的 current 属性,使得最终的电流值与电压和电阻相匹配。

@property 的其他使用场景

属性的 setter 方法里可以包含类型验证和数值验证的代码:

from resistor import Resistor

class BoundedResistor(Resistor):
  def __init__(self, ohms):
    super().__init__(ohms)

  @property
  def ohms(self):
    return self._ohms

  @ohms.setter
  def ohms(self, ohms):
    if ohms <= 0:
      raise ValueError('ohms must be > 0')
    self._ohms = ohms


r3 = BoundedResistor(1e3)
r3.ohms = -5
# => ValueError: ohms must be > 0

甚至可以通过 @property 防止继承自父类的属性被修改:

from resistor import Resistor

class FixedResistance(Resistor):
  def __init__(self, ohms):
    super().__init__(ohms)

  @property
  def ohms(self):
    return self._ohms

  @ohms.setter
  def ohms(self, ohms):
    if hasattr(self, '_ohms'):
      raise AttributeError("Can't set attribute")
    self._ohms = ohms


r4 = FixedResistance(1e3)
r4.ohms = 2e3
# => AttributeError: Can't set attribute

要点

  • 优先使用 public 属性定义类的接口,不手动实现 getter 或 setter 方法
  • 在访问属性的同时需要表现某些特殊的行为(如类型检查、限定取值)等,使用 @property
  • @property 的使用需遵循 rule of least surprise 原则,避免不必要的副作用
  • 缓慢或复杂的工作,应放在普通方法中

二、需要复用的 @property 方法

对于如下需求:
编写一个 Homework 类,其成绩属性在被赋值时需要确保该值大于 0 且小于 100。借助 @property 方法实现起来非常简单:

class Homework(object):
  def __init__(self):
    self._grade = 0

  @property
  def grade(self):
    return self._grade

  @grade.setter
  def grade(self, value):
    if not (0 <= value <= 100):
      raise ValueError('Grade must be between 0 and 100')
    self._grade = value


galileo = Homework()
galileo.grade = 95
print(galileo.grade)
# => 95

假设上述验证逻辑需要用在包含多个科目的考试成绩上,每个科目都需要单独计分。则 @property 方法及验证代码就要重复编写多次,同时这种写法也不够通用。

采用 Python 的描述符可以更好地实现上述功能。在下面的代码中,Exam 类将几个 Grade 实例作为自己的类属性,Grade 类则通过 __get__ __set__ 方法实现了描述符协议。

class Grade(object):
  def __init__(self):
    self._value = 0

  def __get__(self, instance, instance_type):
    return self._value

  def __set__(self, instance, value):
    if not (0 <= value <= 100):
      raise ValueError('Grade must be between 0 and 100')
    self._value = value


class Exam(object):
  math_grade = Grade()
  science_grade = Grade()


first_exam = Exam()
first_exam.math_grade = 82
first_exam.science_grade = 99
print('Math', first_exam.math_grade)
print('Science', first_exam.science_grade)

second_exam = Exam()
second_exam.science_grade = 75
print('Second exam science grade', second_exam.science_grade, ', right')
print('First exam science grade', first_exam.science_grade, ', wrong')
# => Math 82
# => Science 99
# => Second exam science grade 75 , right
# => First exam science grade 75 , wrong

在对 exam 实例的属性进行赋值操作时:

exam = Exam()
exam.math_grade = 40

Python 会将其转译为如下代码:

Exam.__dict__['math_grade'].__set__(exam, 40)

而获取属性值的代码:

print(exam.math_grade)

也会做如下转译:

print(Exam.__dict__['math_grade'].__get__(exam, Exam))

但上述实现方法会导致不符合预期的行为。由于所有的 Exam 实例都会共享同一份 Grade 实例,在多个 Exam 实例上分别操作某一个属性就会出现错误结果。

second_exam = Exam()
second_exam.science_grade = 75
print('Second exam science grade', second_exam.science_grade, ', right')
print('First exam science grade', first_exam.science_grade, ', wrong')
# => Second exam science grade 75 , right
# => First exam science grade 75 , wrong

可以做出如下改动,将每个 Exam 实例所对应的值依次记录到 Grade 中,用字典结构保存每个实例的状态:

class Grade(object):
  def __init__(self):
    self._values = {}

  def __get__(self, instance, instance_type):
    if instance is None:
      return self
    return self._values.get(instance, 0)

  def __set__(self, instance, value):
    if not (0 <= value <= 100):
      raise ValueError('Grade must be between 0 and 100')
    self._values[instance] = value


class Exam(object):
  math_grade = Grade()
  writing_grade = Grade()
  science_grade = Grade()


first_exam = Exam()
first_exam.math_grade = 82
second_exam = Exam()
second_exam.math_grade = 75
print('First exam math grade', first_exam.math_grade, ', right')
print('Second exam math grade', second_exam.math_grade, ', right')
# => First exam math grade 82 , right
# => Second exam math grade 75 , right

还有另外一个问题是,在程序的生命周期内,对于传给 __set__ 的每个 Exam 实例来说,_values 字典都会保存指向该实例的一份引用,导致该实例的引用计数无法降为 0 从而无法被 GC 回收。
解决方法是将普通字典替换为 WeakKeyDictionary

from weakref import WeakKeyDictionary
self._values = WeakKeyDictionary()

参考资料

以上就是属性与 @property 方法让你的python更高效的详细内容,更多关于python 属性与 @property 方法的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python client使用http post 到server端的代码
Feb 10 Python
python自动化测试实例解析
Sep 28 Python
用Python编写一个国际象棋AI程序
Nov 28 Python
Python实现类似jQuery使用中的链式调用的示例
Jun 16 Python
浅谈django orm 优化
Aug 18 Python
Python通用函数实现数组计算的方法
Jun 13 Python
python爬虫之快速对js内容进行破解
Jul 09 Python
解决win7操作系统Python3.7.1安装后启动提示缺少.dll文件问题
Jul 15 Python
详解python中eval函数的作用
Oct 22 Python
详解Anconda环境下载python包的教程(图形界面+命令行+pycharm安装)
Nov 11 Python
Django分组聚合查询实例分享
Apr 29 Python
termux中matplotlib无法显示中文问题的解决方法
Jan 11 Python
Python如何在bool函数中取值
Sep 21 #Python
python 密码学示例——凯撒密码的实现
Sep 21 #Python
python 密码学示例——理解哈希(Hash)算法
Sep 21 #Python
python中的垃圾回收(GC)机制
Sep 21 #Python
如何在Python3中使用telnetlib模块连接网络设备
Sep 21 #Python
总结Pyinstaller的坑及终极解决方法(小结)
Sep 21 #Python
python生成xml时规定dtd实例方法
Sep 21 #Python
You might like
php visitFile()遍历指定文件夹函数
2010/08/21 PHP
ThinkPHP的模版中调用session数据的方法
2014/07/01 PHP
Sublime里直接运行PHP配置方法
2014/11/28 PHP
轻松实现php文件上传功能
2017/02/17 PHP
js 目录列举函数
2008/11/06 Javascript
JavaScript 字符编码规则
2009/05/04 Javascript
JavaScript实现QueryString获取GET参数的方法
2013/07/02 Javascript
jQuery的text()方法用法分析
2014/12/20 Javascript
AngularJS实现树形结构(ztree)菜单示例代码
2016/09/18 Javascript
解决vuecli3.0热更新失效的问题
2018/09/19 Javascript
element-ui 时间选择器限制范围的实现(随动)
2019/01/09 Javascript
jquery无缝图片轮播组件封装
2020/11/25 jQuery
使用vue实现各类弹出框组件
2019/07/03 Javascript
js对象数组和对象的使用实例详解
2019/08/27 Javascript
JavaScript页面加载事件实例讲解
2019/09/01 Javascript
5分钟快速看懂ES6中的反射与代理
2019/12/19 Javascript
javascript canvas检测小球碰撞
2020/04/17 Javascript
addEventListener()和removeEventListener()追加事件和删除追加事件
2020/12/04 Javascript
python正则匹配查询港澳通行证办理进度示例分享
2013/12/27 Python
Python类的多重继承问题深入分析
2014/11/09 Python
pygame学习笔记(3):运动速率、时间、事件、文字
2015/04/15 Python
Python 网页解析HTMLParse的实例详解
2017/08/10 Python
Django中使用Whoosh进行全文检索的方法
2019/03/31 Python
谈一谈基于python的面向对象编程基础
2019/05/21 Python
Python大数据之使用lxml库解析html网页文件示例
2019/11/16 Python
Python 获取命令行参数内容及参数个数的实例
2019/12/20 Python
Python matplotlib实时画图案例
2020/04/23 Python
印尼在线精品店:Berrybenka.com
2016/10/22 全球购物
俄罗斯最大的灯具网站:Fandeco
2020/03/14 全球购物
编写strcpy函数
2014/06/24 面试题
计算机专业自荐信
2013/10/14 职场文书
棉花姑娘教学反思
2014/02/15 职场文书
个人求职信范文
2014/05/24 职场文书
四风专项整治工作情况汇报
2014/10/28 职场文书
孝老爱亲事迹材料
2014/12/24 职场文书
python 判断文件或文件夹是否存在
2022/03/18 Python