属性与 @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设计模式大全
Jun 27 Python
详解Python import方法引入模块的实例
Aug 02 Python
python基础while循环及if判断的实例讲解
Aug 25 Python
Python中列表与元组的乘法操作示例
Feb 10 Python
在VS Code上搭建Python开发环境的方法
Apr 06 Python
Python获取基金网站网页内容、使用BeautifulSoup库分析html操作示例
Jun 04 Python
windows上安装python3教程以及环境变量配置详解
Jul 18 Python
Python列表list常用内建函数实例小结
Oct 22 Python
Keras自定义IOU方式
Jun 10 Python
Python Scrapy多页数据爬取实现过程解析
Jun 12 Python
Python Django中间件使用原理及流程分析
Jun 13 Python
python实现二分查找算法
Sep 18 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中使用Akismet防止垃圾评论的代码
2011/06/10 PHP
ThinkPHP写第一个模块应用
2012/02/20 PHP
PHP获取本周第一天和最后一天示例代码
2014/02/24 PHP
?牟┛途W扣了一??效果出?? target=
2007/05/27 Javascript
javascript正则表达式中参数g(全局)的作用
2010/11/11 Javascript
微信支付如何实现内置浏览器的H5页面支付
2015/09/25 Javascript
基于JS实现移动端访问PC端页面时跳转到对应的移动端网页
2020/12/24 Javascript
JS检测数组类型的方法小结
2017/03/14 Javascript
JS检测window.open打开的窗口是否关闭
2017/06/25 Javascript
在Vue中使用echarts的方法
2018/02/05 Javascript
vue项目打包后打开页面空白解决办法
2018/06/29 Javascript
JavaScript实现的简单Tab点击切换功能示例
2018/07/06 Javascript
vue同步父子组件和异步父子组件的生命周期顺序问题
2018/10/07 Javascript
微信小程序非跳转式组件授权登录的方法示例
2019/05/22 Javascript
24行JavaScript代码实现Redux的方法实例
2019/11/17 Javascript
uin-app+mockjs实现本地数据模拟
2020/08/26 Javascript
Python的语言类型(详解)
2017/06/24 Python
pandas通过loc生成新的列方法
2018/11/28 Python
python将list转为matrix的方法
2018/12/12 Python
python使用KNN算法识别手写数字
2019/04/25 Python
对python 中class与变量的使用方法详解
2019/06/26 Python
python通过http下载文件的方法详解
2019/07/26 Python
anaconda3安装及jupyter环境配置全教程
2020/08/24 Python
python中yield的用法详解
2021/01/13 Python
微信浏览器左上角返回按钮拦截功能
2017/11/21 HTML / CSS
html5 svg 中元素点击事件添加方法
2013/01/16 HTML / CSS
HTML5 video 事件应用示例
2014/09/11 HTML / CSS
localStorage、sessionStorage使用总结
2017/11/17 HTML / CSS
浅谈Html5移动端ios/Android兼容性总结
2018/06/01 HTML / CSS
会计实习自我鉴定
2013/12/04 职场文书
财务经理的岗位职责
2013/12/17 职场文书
四群教育工作实施方案
2014/03/26 职场文书
电子商务专业毕业生求职信
2014/06/12 职场文书
旷课检讨书范文
2015/01/27 职场文书
公司承诺书格式范文
2015/04/28 职场文书
2016年清明节红领巾广播稿
2015/12/17 职场文书