写 Java 的同学刚来 Python 的时候最不适应的一件事就是类里面的属性直接写在那谁都能改。看着不像封装总觉得得套一层get_xxx()/set_xxx()才安心。Python 不这么干。它的思路是一开始你就把属性公开出去调用方怎么爽怎么用。哪天你发现需要加校验、要做计算、要在赋值时跑点逻辑再用property把这个属性包一下——关键是调用方的代码一行都不用改。这才是property真正的用处。最朴素的写法先看一个不用 property 的类classStudent:def__init__(self,name,age):self.namename self.ageage sStudent(小明,18)print(s.age)# 18s.age20print(s.age)# 20属性就是属性直接.点出来直接赋值。简单粗暴。什么时候需要 property假设产品说年龄不能是负数也不能超过 150。你当然可以写个方法classStudent:def__init__(self,name,age):self.namename self.set_age(age)defset_age(self,age):ifnot(0age150):raiseValueError(年龄不合理)self._ageagedefget_age(self):returnself._age但这样一来所有调用方都得从s.age改成s.get_age()原来是s.age 20现在要写s.set_age(20)。老代码全得跟着改烦。property就是来解决这个尴尬的classStudent:def__init__(self,name,age):self.namename self.ageage# 注意这里直接调 setter校验在后面propertydefage(self):returnself._ageage.setterdefage(self,value):ifnot(0value150):raiseValueError(年龄不合理)self._agevalue调用方还是那副德行sStudent(小明,18)print(s.age)# 18读起来像属性s.age20# 写起来也像属性s.age-5# 抛 ValueError外面看就是个普通属性里面该校验校验、该记日志记日志。这就是它的精髓。计算属性另一个常见场景一个值不是存出来的是算出来的。classRectangle:def__init__(self,width,height):self.widthwidth self.heightheightpropertydefarea(self):returnself.width*self.height rRectangle(3,4)print(r.area)# 12r.width5print(r.area)# 20自动跟着变area没有对应的存储字段但读起来和真属性没区别。你不需要让调用方写r.area()那个括号就是多余的。只读属性只要不写 setter它就是只读的classCircle:def__init__(self,radius):self.radiusradiuspropertydefdiameter(self):returnself.radius*2cCircle(5)print(c.diameter)# 10c.diameter20# 抛 AttributeError: cant set attribute想让人只能读不能改的东西这么写就行比单独写个get_diameter()然后指望大家别乱赋值靠谱。几个坑1. 别在 setter 里自己赋值给自己刚写的时候很容易犯classBad:propertydefx(self):returnself.x# 错这会无限递归x.setterdefx(self,value):self.xvalue# 错同样无限递归property 的方法名就叫x你在里面再self.x就是又调了一次 property。真正存值的地方要用另一个名字通常加个下划线classGood:propertydefx(self):returnself._xx.setterdefx(self,value):self._xvalue下划线开头那个_x是约定俗成的内部属性外面别碰。Python 没有真正的私有全靠自觉。2. 别把重计算藏在 property 里这是最容易被坑的一点。s.age看起来就是读个属性天经地义 O(1)。如果有人在 property 里偷偷跑个数据库查询、读个大文件、算个模型推理调用方根本看不出来# 反面教材classUser:propertydefexpensive_report(self):# 谁知道你每次访问都去查了一次数据库returndb.query(SELECT ... WHERE user_id...)这种东西老老实实地写成方法user.build_report()让那个括号提醒读者这活儿不便宜。property 是给看起来就该是属性的东西用的不是给你藏逻辑用的。3. 需要缓存时用 cached_propertyproperty 每次访问都会重新算一遍。如果计算很贵但输入不变用functools.cached_propertyfromfunctoolsimportcached_propertyclassDataLoader:cached_propertydefbig_dataframe(self):# 第一次访问的时候才跑之后直接用缓存returnpd.read_csv(huge_file.csv)注意cached_property只在实例第一次访问时算一次之后缓存到实例字典里。你要是改了输入让它重算得手动把缓存清掉del instance.big_dataframe。和 dataclass 配合用dataclass的时候property 照样能用fromdataclassesimportdataclassdataclassclassStudent:name:strage:intpropertydefis_adult(self):returnself.age18sStudent(小明,20)print(s.is_adult)# Truedataclass 生成的__init__里self.age age会正常触发你写的 setter如果有的话不用额外操心。一句话Python 不要求你从第一天就把所有属性包成 getter/setter。先用公开属性哪天需要加逻辑了再用property在外面套一层调用方无感知。这个后加封装的思路比 Java 那种先封装再说灵活得多。