※ 引述《littrabble (littrabble)》之铭言:
: @property
: def name(self):
: return self._name
: @name.setter
: def name(self, new_name):
: self._name = new_name
: 然后可以使用 instance p,
: p.name 取值, p.name = 1 设值
: 我的疑问是,
: 1. 这根本无法保护变量,为什么教程还要说这种写法保护变量
: 2. 加那个@property @name.setter, 到底有什么好处?
: 我如果不使用@property, 而是把方法名称改成 get_name, 跟 set_name 程式码读起来,不是更清楚明白吗?
: 有没有很有经验的大大,能帮我解惑一下
: 感恩
我们从几个角度来思考这个问题:
1. 语感
当我们使用 class Human 时,在普遍的语感上,属性或成员是一些这个 class
实例具有的状态或资讯:
human1 = Human(...)
print(human1.name) # 印出 human1 的 名字
而方法在语感上是一些行为或动作:
human1.dance() # 让 human1 进行 跳舞 这个行为
那我们来思考一下,如果我们采用 get_name,那印出姓名会是这样的语感:
print(human1.get_name()) # 让 human1 进行 取得自己姓名 这个行为,然后印出
# 这个行为的结果
比较一下两种印出名字的语感,是不是采用属性或成员比较自然、不拐弯抹角?
然而,这也不代表 get/set method 形式就要完全舍弃。在 PEP 8 中有提到相
关的建议。
首先,如果是超级单纯的直接成员存取,也没有特殊的限制逻辑考量,则你应该
干脆地直接使用公开成员,什么 @property 或 get/set method 都免了。
再来,如果这个逻辑变得复杂,我们随时都可以使用 @property 进行包装,让
使用方式跟公开成员完全相同,但内部处理逻辑改变。
但是,使用 @property 的情况下,因为其语感给使用者就像是直接存取一个成
员变量,所以我们会希望就算它有包装一些处理逻辑,但这些处理逻辑不要带来副作
用,也不要是太过昂贵的操作,因为使用者不会设想一个简单的:
human1.name = "ddavid"
操作背后居然会导致他的银行帐户变成我的,或者要执行三天只因为真的去跑户
政事务所改名流程。当你真的想要让上面两件事情发生,使用 method 来表现的语感
就更为合适:
human1.set_bank_account_name("ddavid")
human1.set_id_card_name("ddavid")
法律小提示:银行帐户没法转让啦,所以放心吧。直接转帐给我就好啦(误)
以下是 PEP 8 相关原文:
For simple public data attributes, it is best to expose just the attribute
name, without complicated accessor/mutator methods. Keep in mind that Python
provides an easy path to future enhancement, should you find that a simple
data attribute needs to grow functional behavior. In that case, use
properties to hide functional implementation behind simple data attribute
access syntax.
Note 1: Try to keep the functional behavior side-effect free, although
side-effects such as caching are generally fine.
Note 2: Avoid using properties for computationally expensive operations; the
attribute notation makes the caller believe that access is (relatively) cheap.
2. 保护变量
原 po 可能误解的一点是,@property 的保护变量是跟直接暴露成员相比的。在
保护变量这一点上,它跟 set/get method 效果相差不大。
比如相较于:
class Human:
def __init__(height: float):
self.height = height
human1 = Human(170.1)
human1.height = -1 # 乱给身高为负值
使用以下方法可以对此做出保护:
class Human:
def __init__(self, height: float):
self._height = height
@property
def height(self):
return self._height
@height.setter
def height(self, value: float):
if value < 0:
raise ValueError("Height cannot be negative")
self._height = value
当然你一样可以用 set_height 的写法做到这一点:
def set_height(self, value: float):
if value < 0:
raise ValueError("Height cannot be negative")
self._height = value
但当考量到前述的语感理由,在 height 是个单纯属性处理的情况下,就没什么
必要强调操作性。
同时,我们也可以拿掉 setter/getter 其中之一,让其变成可读不可写或可写
不可读,这也是一种保护。
当然我们知道,即便使用 _ 甚至 __ 前缀的成员,在 Python 中始终有手段直
接操作原始成员,因为 Python 把这些判断留给 programmer。
3. 封装逻辑
比如说,对于人类而言,BMI 语感上作为一个很单纯的属性值也很直觉。可是当
我们已经存了身高体重,额外存一个 BMI 好像在某些情况下有点多余。于是我们就
可以在维持其属性语感的前提下把逻辑包装起来:
class Human:
def __init__(self, height: float, weight: float):
self.height = height
self.weight = weight
@property
def bmi(self):
return self.weight / (self.height * self.height)
所以这么做后,我们就可以用 human1.bmi 这样直觉的方式取得这个人的 BMI,
而且在身高体重有变化时还可以自然跟着变化。而因为这不是很昂贵的运算,所以每
次取都算一下也没太大关系。
同时,因为我们没有给予 setter,也表达出了对于这个值的保护是唯读的,我
们不能手改 BMI 或想借由改 BMI 去影响身高体重值之类。
如前述,如果语感上要强调 BMI 每次都是计算出来的,我认为写成 get_bmi 的
方法也无不可。