一、用属性替代 getter 或 setter 方法
以下代码中包含手动实现的 getter(get_ohms
) 和 setter(set_ohms
) 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 属性写起:
1 2 3 4 5 6 7 8 9 10 11 12 | 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
方法实现此类需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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
方法里可以包含类型验证和数值验证的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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
防止继承自父类的属性被修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 方法实现起来非常简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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__
方法实现了描述符协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 实例的属性进行赋值操作时:
1 2 | exam = Exam() exam.math_grade = 40 |
Python 会将其转译为如下代码:
1 | Exam.__dict__[ 'math_grade' ].__set__(exam, 40 ) |
而获取属性值的代码:
1 | print (exam.math_grade) |
也会做如下转译:
1 | print (Exam.__dict__[ 'math_grade' ].__get__(exam, Exam)) |
但上述实现方法会导致不符合预期的行为。由于所有的 Exam 实例都会共享同一份 Grade 实例,在多个 Exam 实例上分别操作某一个属性就会出现错误结果。
1 2 3 4 5 6 | 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 中,用字典结构保存每个实例的状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
:
1 2 | from weakref import WeakKeyDictionary self ._values = WeakKeyDictionary() |
参考资料
以上就是属性与 @property 方法让你的python更高效的详细内容,更多关于python 属性与 @property 方法的资料请关注自学编程网其它相关文章!
- 本文固定链接: https://zxbcw.cn/post/196064/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)