作者ddavid (谎言接线生)
看板Python
标题Re: [问题] @property 真正的运用是啥
时间Wed Jan 15 12:03:40 2025
※ 引述《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 的
方法也无不可。
--
「可是你......不是天使吗?」
「天使?」她缓缓的转过头来,用悲伤的表情。「天使,只不过是神创造出来的
不死玩偶。」
「而神,也只不过是诅咒下的伪善使者。」
--星.幻.梦的传说
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 125.229.62.213 (台湾)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Python/M.1736913822.A.4BF.html
※ 编辑: ddavid (125.229.62.213 台湾), 01/15/2025 14:32:22
1F:推 sating00: 就…如果你写的code也不是什麽大型专案,没必要这样设 01/15 20:23
2F:→ sating00: 计这些保护,除非设计理念跟有哲学上的洁癖(像我) 01/15 20:23
这倒不完全跟专案大小有关,跟系统是否接触外部或者有其他协作者比较有关。
3F:推 melancholy07: 讨论推 01/15 21:01
※ 编辑: ddavid (125.229.62.213 台湾), 01/16/2025 10:46:49
4F:→ leolarrel: 我跟一楼一样,写程式有洁癖 01/16 11:41
5F:推 cuteSquirrel: 狮子习惯良好 01/17 07:33
6F:→ mantour: 但这样的写法使用者可能会以为自己在单纯的赋值和取值, 01/18 17:29
7F:→ mantour: 等到出错才会意识到这不是一个单纯的属性,用getter/set 01/18 17:29
8F:→ mantour: ter明示不是比较不容易误解吗。 01/18 17:29
9F:→ Hsins: 在使用物件时,没有经过 @property 装饰器修饰的,无法这样 01/18 18:07
10F:→ Hsins: 操作;为了实现物件导向程式开发的封装概念,本来也不应该 01/18 18:07
11F:→ Hsins: 在 class 以外直接操作属性。所以如果产生「我不知道是在直 01/18 18:07
12F:→ Hsins: 接操作属性还是使用 getter 或 setter 耶」这样的想法,需 01/18 18:07
13F:→ Hsins: 要回来想想看审视一下当下的写法有没有问题。 01/18 18:07
14F:→ Hsins: 我上面说的可能有些绕口,简单来说就是在 OOP 的理念中,属 01/18 18:09
15F:→ Hsins: 性本来就不该被直接操作。 01/18 18:09
16F:推 ck574b027: 我试着比楼上更精确些。有封装的概念时, 01/18 23:30
17F:→ ck574b027: 我没给的,外人本来就不能要;外人能拿到的代表有控制 01/18 23:32
所以 PEP 8 才会强调不应该带有副作用,基本概念就是保持使用者把它当直接
的属性操作不会误解出事。比如说作者只是加了一堆型别检查、型别转换、范围检查
等等,都通过了以後最後还是简单的直接赋值,那就不用担心使用者理解错误。
事实上大概就是 Pydantic 之类做的事情。
又或是你内部存公尺,但允许使用者用英尺存取,这只是很简单的转换。
还有像 requests 里面呼叫 API 回应的 Response.ok:
@property
def ok(self):
"""Returns True if :attr:`status_code` is less than 400, False if not.
This attribute checks if the status code of the response is between
400 and 600 to see if there was a client error or a server error. If
the status code is between 200 and 400, this will return True. This
is **not** a check to see if the response code is ``200 OK``.
"""
try:
self.raise_for_status()
except HTTPError:
return False
return True
它确实不是单纯的取值,但概念很单纯,虽然 raise_for_status() 里面其实多
做了一些事情,但没有会影响状态的东西,所以也没太大问题。
※ 编辑: ddavid (114.44.6.70 台湾), 01/28/2025 06:39:06
18F:→ w0005151: 纯个人经验来说,不是在开发lib或框架,真的没必要用 01/31 00:10
19F:→ w0005151: 大部分时候遵守KISS原则带来的好处胜於一切 01/31 00:11