属性与 @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中比较运算符的使用
May 13 Python
浅谈python jieba分词模块的基本用法
Nov 09 Python
浅谈Scrapy框架普通反爬虫机制的应对策略
Dec 28 Python
Python TestCase中的断言方法介绍
May 02 Python
pandas实现将dataframe满足某一条件的值选出
Jun 12 Python
在自动化中用python实现键盘操作的方法详解
Jul 19 Python
使用python写的opencv实时监测和解析二维码和条形码
Aug 14 Python
浅谈Django+Gunicorn+Nginx部署之路
Sep 11 Python
基于python实现把图片转换成素描
Nov 13 Python
Python decimal模块使用方法详解
Jun 08 Python
pytorch 两个GPU同时训练的解决方案
Jun 01 Python
Python中的datetime包与time包包和模块详情
Feb 28 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的5个入手程序
2006/11/23 PHP
一个显示效果非常不错的PHP错误、异常处理类
2014/03/21 PHP
php中使用sftp教程
2015/03/30 PHP
PHP面向对象多态性实现方法简单示例
2017/09/27 PHP
PHP解决高并发的优化方案实例
2020/12/10 PHP
Javascript 汉字字节判断
2009/08/01 Javascript
基于JavaScript 下namespace 功能的简单分析
2013/07/05 Javascript
javascript 循环调用示例介绍
2013/11/20 Javascript
JQuery记住用户名密码实现下次自动登录功能
2015/04/27 Javascript
《JavaScript函数式编程》读后感
2015/08/07 Javascript
深入探讨Vue.js组件和组件通信
2016/09/12 Javascript
vuejs在解析时出现闪烁的原因及防止闪烁的方法
2016/09/19 Javascript
KnockoutJS 3.X API 第四章之表单submit、enable、disable绑定
2016/10/10 Javascript
js replace()去除代码中空格的实例
2017/02/14 Javascript
JS简单实现数组去重的方法示例
2017/03/27 Javascript
微信小程序中使用javascript 回调函数
2017/05/11 Javascript
vue 使某个组件不被 keep-alive 缓存的方法
2018/09/21 Javascript
react native 原生模块桥接的简单说明小结
2019/02/26 Javascript
封装Vue Element的table表格组件的示例详解
2020/08/19 Javascript
Vue页面跳转传递参数及接收方式
2020/09/09 Javascript
树莓派中python获取GY-85九轴模块信息示例
2013/12/05 Python
Python中非常实用的一些功能和函数分享
2015/02/14 Python
对pandas的dataframe绘图并保存的实现方法
2017/08/05 Python
python单向循环链表原理与实现方法示例
2019/12/03 Python
Python 统计位数为偶数的数字代码详解
2020/03/15 Python
Python机器学习工具scikit-learn的使用笔记
2021/01/28 Python
html5写一个BUI折叠菜单插件的实现方法
2019/09/11 HTML / CSS
全球最大最受欢迎的旅游社区:Tripadvisor
2017/11/03 全球购物
上课迟到检讨书100字
2014/01/11 职场文书
优秀社区干部事迹材料
2014/02/03 职场文书
2014年大学庆元旦迎新年活动方案
2014/03/09 职场文书
死亡赔偿协议书
2015/01/28 职场文书
中秋联欢会主持词
2015/07/04 职场文书
2016入党心得体会范文
2016/01/06 职场文书
Linux中Nginx的防盗链和优化的实现代码
2021/06/20 Servers
【DOTA2】高能暴走TK秀!PSG LGD vs ASTER - DPC 2022 WINTER TOUR CN
2022/04/02 DOTA