属性与 @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 相关文章推荐
使用IronPython把Python脚本集成到.NET程序中的教程
Mar 31 Python
Python实现Mysql数据库连接池实例详解
Apr 11 Python
详解django.contirb.auth-认证
Jul 16 Python
python与caffe改变通道顺序的方法
Aug 04 Python
Python利用heapq实现一个优先级队列的方法
Feb 03 Python
python函数不定长参数使用方法解析
Dec 14 Python
tensorflow实现tensor中满足某一条件的数值取出组成新的tensor
Jan 04 Python
keras 特征图可视化实例(中间层)
Jan 24 Python
通过自学python能找到工作吗
Jun 21 Python
python3.8动态人脸识别的实现示例
Sep 21 Python
Python importlib模块重载使用方法详解
Oct 13 Python
python基础之错误和异常处理
Oct 24 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修改文件上传限制方法汇总
2015/04/07 PHP
php实现登录tplink WR882N获取IP和重启的方法
2016/07/20 PHP
Thinkphp 5.0实现微信企业付款到零钱
2018/09/30 PHP
php设计模式之备忘模式分析【星际争霸游戏案例】
2020/03/24 PHP
统一接口:为FireFox添加IE的方法和属性的js代码
2007/03/25 Javascript
事件冒泡是什么如何用jquery阻止事件冒泡
2013/03/20 Javascript
jQuery中offset()方法用法实例
2015/01/16 Javascript
Javascript 正则表达式实现为数字添加千位分隔符
2015/03/10 Javascript
js实现无缝循环滚动
2020/06/23 Javascript
EasyUI 中combotree 默认不能选择父节点的实现方法
2016/11/07 Javascript
jquery实现折叠菜单效果【推荐】
2017/03/08 Javascript
AngularJS前端页面操作之用户修改密码功能示例
2017/03/27 Javascript
Textarea输入字数限制实例(兼容iOS&amp;安卓)
2017/07/06 Javascript
JS基于递归实现网页版计算器的方法分析
2017/12/20 Javascript
vue2.0之多页面的开发的示例
2018/01/30 Javascript
vuex实现登录状态的存储,未登录状态不允许浏览的方法
2018/03/09 Javascript
vue中组件通信的八种方式(值得收藏!)
2019/08/09 Javascript
JsonProperty 的使用方法详解
2019/10/11 Javascript
Angular+ionic实现折叠展开效果的示例代码
2020/07/29 Javascript
js+audio实现音乐播放器
2020/09/13 Javascript
解决ant Design中this.props.form.validateFields未执行的问题
2020/10/27 Javascript
centos下更新Python版本的步骤
2013/02/12 Python
python实现web方式logview的方法
2015/08/10 Python
Linux下多个Python版本安装教程
2018/08/15 Python
Python模拟简单电梯调度算法示例
2018/08/20 Python
python自动发微信监控报警
2019/09/06 Python
使用CSS3的appearance属性改变任何元素的浏览器默认风格
2012/12/24 HTML / CSS
目前不被任何主流浏览器支持的CSS3属性汇总
2014/07/21 HTML / CSS
用CSS3的box-reflect来制作倒影效果
2016/11/15 HTML / CSS
英国蜡烛、蜡烛配件和家居香氛购买网站:Yankee Candle
2018/12/12 全球购物
软件测试工程师结构化面试题库
2016/11/23 面试题
财务支持类个人的自我评价
2014/02/14 职场文书
酒店管理毕业生自我鉴定
2014/03/02 职场文书
村党的群众路线教育实践活动工作总结
2014/10/25 职场文书
比较node.js和Deno
2021/04/27 Javascript
nginx rewrite功能使用场景分析
2022/05/30 Servers