属性与 @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入门篇之函数
Oct 20 Python
Python 自动补全(vim)
Nov 30 Python
使用Python脚本和ADB命令实现卸载App
Feb 10 Python
Python使用struct处理二进制的实例详解
Sep 11 Python
基于python(urlparse)模板的使用方法总结
Oct 13 Python
python使用tkinter实现简单计算器
Jan 30 Python
在python中利用KNN实现对iris进行分类的方法
Dec 11 Python
对python中if语句的真假判断实例详解
Feb 18 Python
python selenium 执行完毕关闭chromedriver进程示例
Nov 15 Python
python获取网络图片方法及整理过程详解
Dec 20 Python
基于Python获取docx/doc文件内容代码解析
Feb 17 Python
Python求两个字符串最长公共子序列代码实例
Mar 05 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中 === 的使用
2016/10/24 PHP
PHP实现本地图片转base64格式并上传
2020/05/29 PHP
JavaScript的9个陷阱及评点分析
2008/05/16 Javascript
基于jquery的商品展示放大镜
2010/08/07 Javascript
jQuery实现多级下拉菜单jDropMenu的方法
2015/08/28 Javascript
详解jQuery Mobile自定义标签
2016/01/06 Javascript
JQuery在循环中绑定事件的问题详解
2016/06/02 Javascript
javascript函数中的3个高级技巧
2016/09/22 Javascript
AngularJS中transclude用法详解
2016/11/03 Javascript
Angular.js中下拉框实现渲染html的方法
2017/06/18 Javascript
浅谈Vuex的状态管理(全家桶)
2017/11/04 Javascript
JavaScript生成简单等差数列
2017/11/28 Javascript
浅谈Vue-cli单文件组件引入less,sass,css样式的不同方法
2018/03/13 Javascript
Angular4集成ng2-file-upload的上传组件
2018/03/14 Javascript
微信小程序自定义底部导航带跳转功能
2018/11/27 Javascript
JQuery搜索框自动补全(模糊匹配)功能实现示例
2019/01/08 jQuery
微信小程序使用map组件实现获取定位城市天气或者指定城市天气数据功能
2019/01/22 Javascript
微信小程序实现点击卡片 翻转效果
2019/09/04 Javascript
微信公众号开发之微信支付代码记录的实现
2019/10/16 Javascript
vue中父子组件传值,解决钩子函数mounted只运行一次的操作
2020/07/27 Javascript
Python完全识别验证码自动登录实例详解
2019/11/24 Python
在Sublime Editor中配置Python环境的详细教程
2020/05/03 Python
windows10 pycharm下安装pyltp库和加载模型实现语义角色标注的示例代码
2020/05/07 Python
使用html5+css3来实现slider切换效果告别javascript+css
2013/01/08 HTML / CSS
写好求职应聘自荐信的三部曲
2013/09/21 职场文书
自我推荐书
2013/12/04 职场文书
2014迎新年晚会策划方案
2014/02/23 职场文书
《三顾茅庐》教学反思
2014/04/10 职场文书
安全责任书范本
2014/04/15 职场文书
合作协议书范文
2014/08/20 职场文书
2014年个人思想工作总结
2014/11/27 职场文书
求职自荐信范文(优秀篇)
2015/03/27 职场文书
拾金不昧通报表扬范文
2015/05/05 职场文书
MySql 缓存查询原理与缓存监控和索引监控介绍
2021/07/02 MySQL
8个JS的reduce使用实例和reduce操作方式
2021/10/05 Javascript
Python中异常处理用法
2021/11/27 Python