1. 从需求说起为什么Python需要property先从一个真实场景讲起。假设你在写一个学生管理系统需要一个Student类里面有姓名、年龄两个字段。最直接的做法是定义成公开属性让调用方随便读写class Student: def __init__(self, name, age): self.name name self.age age stu Student(小明, 20) stu.age -10 # 这玩意居然能跑年龄是负数这明显不符合逻辑但代码不会报错程序照常运行数据就脏掉了。这个问题的根源在于直接把内部字段暴露出去任何外部代码都能绕过校验随意赋值。传统面向对象语言比如Java的解决方案很经典——把属性私有化写getXxx()和setXxx()方法在setter里加校验逻辑。但问题是Java那套写法到了Python里显得极其啰嗦。你想想Python的设计哲学是简洁优雅如果每定义一个属性都要手写两个方法代码量直接翻倍读起来还费劲。property装饰器就是解决这个矛盾的方案它让你既能保留直接操作属性的简洁语法又能在底层挂接自定义的读取、赋值、删除逻辑。调用方写stu.age 10这种代码背后实际执行的是你定义的setter方法校验、类型转换、日志记录这些都能塞进去。具体来说property能做什么把方法包装成属性调用时不需要加括号stu.age拿到的是方法的返回值。配合x.setter给属性赋值时执行自定义逻辑实现数据校验和类型约束。配合x.deleter用del stu.age时执行自定义清理逻辑。还能做出只读属性——只定义getter不定义setter赋值直接抛AttributeError。这篇文章我会把property的完整用法、底层原理、常见坑全部过一遍有基础的朋友可以直接跳到第3节看高级玩法刚入门的朋友建议从头读起逐步理解。2. 最基础的用法从普通属性到受控属性2.1 先写一个最普通的类假设我们要开发一个简单的银行账户类第一个版本是这样的class BankAccount: def __init__(self, owner, balance): self.owner owner self.balance balance account BankAccount(张三, 1000) print(account.balance) # 1000 account.balance 500 # 直接改 print(account.balance) # 500代码很好懂但也埋了个雷余额可以被随意改成负数、字符串、None没有任何防线。这时候就该用property了。2.2 添加property后的版本改造思路是把balance从直接存储在实例字典里的属性变成由getter方法提供的属性。存储上改名_balance对外暴露balance属性。class BankAccount: def __init__(self, owner, balance): self.owner owner self._balance balance property def balance(self): 获取余额 return self._balance balance.setter def balance(self, value): 设置余额必须是非负数字 if not isinstance(value, (int, float)): raise TypeError(余额必须是数字) if value 0: raise ValueError(余额不能为负数) self._balance value account BankAccount(张三, 1000) print(account.balance) # 1000不用加括号 account.balance 2000 # 正常 print(account.balance) # 2000 account.balance -100 # ValueError: 余额不能为负数 account.balance abc # TypeError: 余额必须是数字关键点在于外部代码的调用方式没变。之前怎么写account.balance 500现在还是这么写但内部执行逻辑完全不同了。这种语法不变、行为增强的特性让你能在不破坏已有调用方代码的前提下给属性加上保护和约束。这里有个细节值得说明存储属性我命名为_balance带一个下划线前缀。这是Python社区约定俗成的保护变量写法表示这个属性是内部实现细节外部不要直接访问。你非要从外部访问account._balance也能做到但大家都默认遵守这个约定不主动去碰。2.3 getter返回值的时机问题很多人一开始会困惑property修饰的方法什么时候执行答案是每次访问属性时都执行不是缓存一次的。class RandomValue: property def value(self): import random return random.randint(1, 100) rv RandomValue() print(rv.value) # 每次都不一样 print(rv.value) # 每次都不一样这在设计时要特别注意。如果你的getter方法里有耗时操作比如查数据库、做复杂计算那么每次读属性都会触发一次完整流程性能上可能扛不住。后面第5节会讲怎么用缓存优化。3. 三个核心方法的完整拆解3.1 property定义读取逻辑property本身是一个装饰器它把被修饰的方法变成property对象。这个对象有三个方法getter、setter、deleter分别用于定义属性的读取、赋值、删除行为。只写property不写setter得到的属性是只读属性class Circle: def __init__(self, radius): self._radius radius property def area(self): 圆的面积由半径实时计算 import math return math.pi * self._radius ** 2 c Circle(2) print(c.area) # 12.566... c.area 100 # AttributeError: cant set attribute这个特性非常实用。比如你已经在上线版本中直接暴露了area属性后来发现area不应该被外部修改直接给属性加上property并且不提供setter外部赋值就报错了——只读属性天然防御了非法写入。这里有一个使用原则property修饰的方法名就是对外暴露的属性名方法内部对应的私人存储一般带下划线。这个关联不是Python强制要求的但强烈建议大家遵守否则代码看起来非常混乱。3.2 x.setter拦截赋值操作setter的语法比较特殊不是property.setter而是balance.setter——必须用被property修饰的方法名来引用。来看一个带更多业务逻辑的例子。假设我们要记录余额的每次变动历史class BankAccount: def __init__(self, owner, balance0): self.owner owner self._balance balance self._history [] property def balance(self): return self._balance balance.setter def balance(self, value): if not isinstance(value, (int, float)): raise TypeError(余额必须是数字) if value 0: raise ValueError(余额不能为负数) old self._balance self._balance value self._history.append(f{old} - {value}) property def history(self): return self._history account BankAccount(张三, 1000) account.balance 1500 account.balance 800 print(account.history) # [1000 - 1500, 1500 - 800]在setter里可以进行类型检查、范围检查、格式转换、日志记录、数据同步等操作。它的执行时机非常明确每次执行account.balance 任何值时都会调用这个setter包括在__init__里的赋值。等等有个很重要的坑要讲在__init__里对_balance赋值走的是原生属性赋值不会触发setterclass Demo: def __init__(self, x): self._x x # 不会触发setter self.x x # 会触发setter property def x(self): return self._x x.setter def x(self, value): print(setter called) self._x value这是很多Python初学者踩过的大坑在__init__中写了self.x x期望着setter做校验却忘了self.x x这行代码本身会先进入setter——而setter内部如果又执行self.x value就会无限递归直到RecursionError。后面第6节会专门讲这个经典bug。3.3 x.deleter拦截删除操作deleter的用法相对少见但某些场景下非常关键。它的语法是balance.deleter def balance(self): # 自定义删除逻辑删除操作的触发方式是del account.balance。不定义deleter时删除属性会报错class Book: def __init__(self, title): self._title title property def title(self): return self._title title.setter def title(self, value): if not isinstance(value, str): raise TypeError(标题必须是字符串) self._title value book Book(Python编程入门) del book.title # AttributeError: cant delete attribute当你需要控制删除这个动作时可以定义deleter。比如删除余额时需要先做审计、清理关联数据、释放资源class BankAccount: def __init__(self, owner, balance0): self.owner owner self._balance balance self._closed False property def balance(self): return self._balance balance.setter def balance(self, value): if self._closed: raise RuntimeError(账户已销户无法操作) self._balance value balance.deleter def balance(self): # 执行销户清理逻辑 print(f正在销户 {self.owner}余额 {self._balance} 将被冻结) self._closed True del self._balance account BankAccount(张三, 1000) del account.balance print(account.balance) # 报错 AttributeError: BankAccount object has no attribute _balance实际业务中del obj.attr的语义是移除这个属性正常情况下很少用。但在需要管理资源释放、钩子回调比如ORM框架中删除字段触发级联操作的场景deleter就派上用场了。3.4 快速掌握写法模版把三种方法的写法整理成模板照着套就行class MyClass: def __init__(self, value): # 注意最好直接操作私有变量避免触发setter逻辑不一致 self._value value property def value(self): 读取逻辑 return self._value value.setter def value(self, new_value): 赋值逻辑 self._value new_value value.deleter def value(self): 删除逻辑 del self._value记住一个最核心的语法规则setter/deleter的装饰器名称必须和property修饰的方法名保持一致。方法名为value就用value.setter和value.deleter方法名为name就用name.setter。4. 从装饰器到property的进阶机制4.1 三种写法背后的本质property写法其实是对property()内置函数的语法糖。了解一下底层机制遇到复杂场景就不会懵了。property()函数的完整签名是property(fgetNone, fsetNone, fdelNone, docNone)参数分别对应getter、setter、deleter和文档字符串。所以下面的写法是等价的写法一装饰器风格推荐class Person: def __init__(self, name): self._name name property def name(self): return self._name name.setter def name(self, value): self._name value写法二直接调用property()函数class Person: def __init__(self, name): self._name name def _get_name(self): return self._name def _set_name(self, value): self._name value name property(_get_name, _set_name)功能是相同的。实际项目里装饰器写法更常见因为代码结构清晰getter/setter逻辑直接嵌套在属性名称下面阅读时一目了然。但了解函数式写法对于理解property到底是什么很有帮助——它就是一个对象内部持有三个回调函数。如果给property()传入一个文档字符串name property(_get_name, _set_name, doc姓名属性不能为空字符串)这个文档会出现在help(Person.name)的输出中。4.2 property对象本身也是属性property是一个descriptor描述符它实现了__get__、__set__、__delete__方法。当你在类中定义了一个property对象这个对象就会被存储在类的字典中。当外部访问实例的属性时Python解释器会沿着实例字典 - 类字典的路径查找如果找到的是一个property对象就自动调用它对应的getter/setter。这个机制带来的一个实际影响是property不是存在实例字典里的而是存在类的定义中。所以当你用vars()查看实例的__dict__时看不到property本身只能看到存储属性的_name之类。p Person(张三) print(vars(p)) # {_name: 张三} print(Person.__dict__[name]) # property object at 0x...理解这一点对于调试为什么我改了实例的某个属性property不生效这类问题很有帮助。比如下面这段代码就非常容易踩坑p Person(张三) p.name 李四 # 正常触发setter p.__dict__[name] 王五 print(p.name) # 输出什么答案是李四还是王五实际结果是王五。原因在于p.__dict__[name] 王五直接把实例字典里塞了一个name键。当外部访问p.name时Python会先查实例字典发现__dict__里已经有name了就直接返回这个值根本不会找到类属性里的property对象。这个行为属于Python属性查找的优先级规则实例字典优先于类属性中的描述符非数据描述符除外property是数据描述符优先级高于实例字典——等等这里需要更严谨一点。严谨地说property属于数据描述符data descriptor数据描述符的优先级高于实例字典。也就是说上面这段代码里p.name应该仍然返回李四而不是实例字典里的王五。但如果property没有定义setter那它就不是一个完整的数据描述符……不对property始终包含__set__或__delete__只要实现了__set__就是数据描述符而只读property虽然没有可调用的setter但它仍然实现了__set__内部抛异常所以仍然是数据描述符。因此p.__dict__[name] 王五不会覆盖property的行为p.name仍然返回李四。这个细节容易把人绕晕在实际开发中你需要记住的要点就是property是类的属性不是实例的属性定义好之后对实例的属性操作都会被拦截并路由到对应的getter/setter方法。4.3 只读属性的实现细节前面提到只定义property不定义setter属性就是只读的。这个只读是严格遵守的因为property内部有__set__方法没有setter时会在赋值时抛AttributeError: cant set attribute。class Config: def __init__(self): self._version 1.0.0 property def version(self): return self._version cfg Config() cfg.version 2.0.0 # AttributeError: cant set attribute要注意只读是指通过这个属性名无法赋值并不是底层数据完全不可变。比如访问cfg._version还是能直接改的。Python没有严格的私有机制这点在设计API时需要心里有数。5. 高级用法与实际场景5.1 计算属性与派生字段property最常见的应用场景之一就是计算属性。比如一个订单类总价可以通过单价乘数量算出来class OrderItem: def __init__(self, product_name, price, quantity): self.product_name product_name self.price price self.quantity quantity property def total_price(self): 总价 单价 * 数量 return self.price * self.quantity property def description(self): return f{self.product_name} x{self.quantity}合计 ¥{self.total_price} item OrderItem(机械键盘, 399, 2) print(item.total_price) # 798 print(item.description) # 机械键盘 x2合计 ¥798关键优势是total_price不需要在每次价格或数量变化时手动更新每次读取时实时计算。如果使用传统方法你得在修改quantity和price时记住同步修改total字段很容易漏掉一处导致数据不一致。在写这种计算属性时需要注意一点当计算开销较大且属性会被频繁读取时实时计算可能成为性能瓶颈。比如一个复杂的报表汇总每次访问都重新算一遍压力山大。此时可以进行延迟缓存后面会讲到。类似地在数据模型、ORM实体中计算属性还常用来做序列化格式化。比如Pydantic、Django模型里常常定义一大堆只读property来组合输出字段。5.2 数据校验与类型转换setter里做数据校验是最经典的应用。可以约定一个通用模版先校验、再转换、最后赋值。class User: def __init__(self, name, age): self.name name self.age age property def name(self): return self._name name.setter def name(self, value): if not isinstance(value, str): raise TypeError(name 必须是字符串) if not value.strip(): raise ValueError(name 不能为空) self._name value.strip() # 去除首尾空格 property def age(self): return self._age age.setter def age(self, value): if not isinstance(value, int): # 尝试把数字字符串转成int比如 18 try: value int(value) except (TypeError, ValueError): raise TypeError(age 必须是整数) if not (0 value 150): raise ValueError(age 必须在 0 到 150 之间) self._age value u User( 张三 , 18) print(u.name) # 张三 print(u.age) # 18 u.age 200 # ValueError: age 必须在 0 到 150 之间这里要特别强调一个设计原则在__init__里给属性赋值时如果属性是有setter的推荐直接使用self.age value而不是self._age value这样能保证构造对象时数据就已经经过校验。如果你在__init__里绕过setter直接赋值等于是让非法数据溜进了系统。下面这样写是错误示范class User: def __init__(self, name, age): self._name name # 没有走setter非法值也能进来 self._age age # 没有走setter校验形同虚设正确姿势是class User: def __init__(self, name, age): self.name name # 触发setter完成校验 self.age age # 触发setter完成校验注意self.name name这一行触发了setter而setter内部会给自己定义self._name ...所以后面不要再写self._name name了否则会在setter里递归调用自己直接导致RecursionError。5.3 隐藏内部状态接口不变实现随意换property很强大的一点是类内部实现随便改对外接口保持不变。下面这个例子说明这个思路假定代码库早期版本中成绩是直接存成百分制整数的。后来业务调整需要用等级制A/B/C/D存储并且保留原始分数用于报表。如果直接改属性那么所有调用方都要跟着改。用property做适配层class Student: def __init__(self, name, grade): self.name name self.grade grade # grade 对外表现为字符串等级 property def grade(self): return self._grade grade.setter def grade(self, value): if isinstance(value, int): # 老代码传入的是百分制分数自动转等级 if value 90: self._grade A elif value 80: self._grade B elif value 70: self._grade C elif value 60: self._grade D else: self._grade F else: # 新代码直接传等级 if value.upper() not in (A, B, C, D, F): raise ValueError(无效等级) self._grade value.upper()老代码写student.grade 85新代码写student.grade A对外都是赋值同一个属性内部自动转换调用方完全无感知。这种渐进式重构正是property的价值所在——可以大幅降低大规模代码迁移的成本。5.4 经典案例缓存计算结果假设你需要频繁读取一个计算量很大的属性比如从大文件里解析配置class ConfigParser: def __init__(self, file_path): self.file_path file_path self._data None self._loaded False property def data(self): if not self._loaded: print(正在加载配置文件...) # 模拟耗时读取 with open(self.file_path, encodingutf-8) as f: self._data f.read().splitlines() self._loaded True return self._data cfg ConfigParser(config.txt) print(cfg.data) # 首次访问触发读取 print(cfg.data) # 第二次访问直接返回缓存 print(cfg.data) # 同样直接返回核心思路是用一个布尔标记_loaded记录是否已经加载只有首次访问时才真正执行重活。这种方式叫懒加载lazy loading在Python中还有更优雅的写法——使用functools.cached_propertyfrom functools import cached_property class ConfigParser: def __init__(self, file_path): self.file_path file_path cached_property def data(self): print(正在加载配置文件...) with open(self.file_path, encodingutf-8) as f: return f.read().splitlines() cfg ConfigParser(config.txt) print(cfg.data) # 触发一次读取 print(cfg.data) # 后续直接使用缓存值cached_property是Python 3.8加入的functools模块装饰器它在第一次访问时计算结果并缓存到实例的__dict__中后续访问直接从实例字典读取效率极高。要注意的是cached_property只适用于计算结果在后继访问中不会变化的场景如果底层数据可能会变就不能用否则会拿到旧缓存。对比一下普通property和cached_property特性propertycached_property每次访问时执行getter是仅首次是否自动缓存否是适合场景动态计算、数据可能经常变化计算开销大且结果稳定底层实现property描述符非数据描述符结果存在实例字典5.5 延迟初始化与单例模式利用cached_property还能实现简单的进程内单例from functools import cached_property class DatabaseConnection: def __init__(self, connection_string): self.connection_string connection_string self._conn None cached_property def connection(self): print(正在建立数据库连接...) return self._create_connection() def _create_connection(self): # 模拟创建连接对象 return {connected: True, string: self.connection_string} db DatabaseConnection(mysql://localhost:3306/test) conn1 db.connection print(第一次连接对象:, conn1) conn2 db.connection print(第二次连接对象:, conn2) print(连接对象是否相同:, conn1 is conn2) # True这种模式在需要重对象数据库连接、HTTP会话、资源句柄的场景非常有用把初始化成本延后到真正使用的那一刻又保证了后续访问的高效。5.6 与子类重写的配合在实际大型项目中你可能会在父类定义property在子类中重写getter/setter逻辑。重写的方式分两种方式一完全覆盖属性和方法体class Animal: def __init__(self, name): self._name name property def sound(self): return ... class Dog(Animal): property def sound(self): return Woof! sound.setter def sound(self, value): print(设置狗狗的声音:, value) self._sound value dog Dog(旺财) print(dog.sound) # Woof! dog.sound 汪汪 # 设置狗狗的声音: 汪汪这种写法相当于子类重新定义了一个新property完全覆盖父类的。实际开发中比较常见。方式二在子类setter中调用父类逻辑class Animal: def __init__(self, name): self.name name property def name(self): return self._name name.setter def name(self, value): if not value: raise ValueError(名字不能为空) self._name value class Dog(Animal): Animal.name.setter def name(self, value): # 先做子类校验 if not isinstance(value, str): raise TypeError(名字必须是字符串) # 调用父类setter super(Dog, type(self)).name.__set__(self, value) dog Dog(旺财) print(dog.name) # 旺财 dog.name 123 # TypeError: 名字必须是字符串注意这个语法比较绕super(Dog, type(self)).name.__set__(self, value)。其实更简洁的写法是利用super()直接调用property的行为比较麻烦因为super()返回的是代理对象访问属性时处理逻辑不同。实际中更多人选择第一种完全覆盖的方式需要复用父类逻辑时把父类的setter逻辑抽成一个方法class Animal: property def name(self): return self._name name.setter def name(self, value): self._validate_name(value) self._name value def _validate_name(self, value): if not value: raise ValueError(名字不能为空) class Dog(Animal): def _validate_name(self, value): super()._validate_name(value) if not isinstance(value, str): raise TypeError(名字必须是字符串)这样既复用了校验逻辑又避免了复杂的描述符方法调用。这是我个人比较推荐的工程实践——property只是门面真正的业务逻辑放在普通方法里这样重写和复用都方便。6. 常见坑与排查技巧6.1 RecursionErrorsetter里的无限递归这个错误太经典了几乎每个使用property的人都会踩到。看下面的错误写法class Bad: def __init__(self, x): self.x x property def x(self): return self.x # 错误递归调用 x.setter def x(self, value): self.x value # 错误递归调用问题在于getter方法内部访问了self.x而self.x又会调用getter形成无限递归。setter同理内部self.x value又触发setter调用。运行后报错RecursionError: maximum recursion depth exceeded。解决办法getter和setter内部必须访问私有存储属性比如self._x而不是self.xclass Good: def __init__(self, x): self.x x # 触发setter没问题 property def x(self): return self._x x.setter def x(self, value): self._x value # 在这里必须使用 _x一句话总结外部用obj.x内部用obj._x。6.2 命名冲突getter方法与存储属性同名如果你把存储属性直接命名为self.x同时又把property方法命名为x会直接导致冲突。class Conflict: property def x(self): return self.x # 这里拿到的是property对象本身而不是值运行会得到一个非常奇怪的错误问题的根源就是命名冲突。解决办法就是用带下划线的存储属性_x同时property方法名用x。6.3 AttributeError: cant set attribute出现这个错误说明你正在给一个只读属性赋值。常见场景是定义了property但没定义setter。把setter方法名写错了比如x.setter拼成了x.setter方法本身不存在。在类外部不小心赋值给了property属性。排查思路是检查你的类定义确认是否定义了对应的setter。如果确实需要只读属性就保持现状如果需要可写补上x.setter即可。6.4 疏忽不同实例会不会互相干扰由于property存储在类中而存储属性_x存储在各自的实例字典中所以不同实例之间互不影响这点不用担心。但你可能会遇到另一个问题多个实例共享同一个property对象但property对象的doc字符串是唯一的。如果你通过__doc__给property设置描述所有实例访问时看到的都是同一个文档。这在大多数场景下没有影响。6.5 使用property但误以为可以动态切换有朋友曾经问我我能不能在运行时给一个类动态添加property答案是可以的但方式不是obj.property_name something而是给类本身赋值一个property对象。比如class MyClass: pass def get_value(self): return 动态属性 MyClass.value property(get_value) obj MyClass() print(obj.value) # 动态属性这种动态添加属性在写框架、插件系统时会用到。需要注意这种动态property是类级别的所有实例共享。而且由于property是数据描述符实例字典中的同键数据会被覆盖。这个属于进阶玩法用到时再看文档也不迟。6.6 序列化框架如json与property的交互当使用json.dumps(obj)直接序列化对象时property属性不会被序列化因为json.dumps默认只处理实例的__dict__。这不是bug而是很多框架的默认行为。如果你希望property字段也出现在JSON中需要自己处理比如用dataclasses.asdict()、pydantic或者自定义序列化方法。class MyClass: def __init__(self): self._name 张三 property def name(self): return self._name def to_dict(self): return {name: self.name} import json obj MyClass() print(json.dumps(obj, defaultlambda o: o.to_dict())) # {name: 张三}这点在使用Django REST Framework、FastAPI、Pydantic等框架时尤其重要因为它们的序列化机制会检查字段的来源——是定义成普通类属性还是property。设计API返回字段时要提前规划好。6.7 排查技巧打印property对象调试时想查看一个类的property信息可以直接访问类属性print(MyClass.x) # property object at 0x... print(MyClass.x.fget) # function MyClass.x at 0x... print(MyClass.x.fset) # function MyClass.x at 0x...property对象的fget、fset、fdel属性分别指向getter、setter、deleter方法。这在排查为什么我的setter没有生效时非常有用。7. 常见问题速查表情况现象解决方案只定义property不定义setter赋值时报AttributeError确认是否需要可写需要就补上setterx.setter写错方法名setter不生效赋值直接改到底层存储确保装饰器名称与property方法名一致在getter/setter中用self.xx无限递归RecursionError内部使用self._xx私有存储存储属性和property同名数据冲突、递归异常给存储属性加下划线_xx在__init__里self.xx xx与self._xx xx混用setter逻辑不生效或触发两次用self.xx value触发setter不要在setter里再次self.xx value序列化时property丢失json.dumps输出缺少字段自定义to_dict或使用支持property的ORM/序列化库缓存计算结果频繁重算每次读属性都执行复杂计算使用cached_property或手动缓存标记8. 从实际项目中总结的几条经验最后聊几个实际项目中的经验。第一个经验property不是越多越好。Python的优点之一是代码直白过度设计反而增加阅读负担。如果属性没有校验、转换、计算等额外逻辑就不要包装成property直接写普通属性。只有当确实需要在赋值/读取时做点什么才值得加这一层。判断标准很简单你会在代码里写以后可能要为这个字段加逻辑这句话吗如果只是可能别提前写YAGNI原则你不会需要它在属性这块同样适用。第二个经验setter里尽量做纯校验不要做副作用操作。有朋友曾经在setter里写数据库更新操作导致每次给属性赋值都触发一次数据库IO性能慢得离谱。setter的职责是保证数据一致性、类型正确不是用来跑业务逻辑的。真正的业务逻辑应该放到明确的方法中比如account.deposit(100)而不是account.balance 100。第三个经验防御性编程要有度。校验太严格比如不允许任何形式的类型转换、不允许None、不允许边界值会让API很难用。实际项目中我一般遵循的规则是类型错误必须抛异常因为这是调用方写错了范围错误根据需要决定是否抛异常有时可以自动截断格式问题尽量自动清洗比如去掉字符串首尾空格。校验的目的不是惩罚调用方而是保证系统不进入不一致状态。第四个经验用了property就不要再直接改私有变量。一旦定义了name的property自己在类内部也要使用self.name而不是绕过property直接操作self._name。否则就会出现有些地方校验了有些地方没校验的尴尬局面排查起来极其痛苦。这条规则尤其适用于团队协作项目——同一属性的读写路径要统一。写Python这些年property是我用得最频繁的装饰器之一。它不算高深但用好了能让代码质量上一个台阶。希望这篇梳理能帮你把它的每个细节都吃透在实际项目中用起来心里有底。