被 `@property` 坑惨了:setter 里的递归爆栈,与那条被误读的 `_` 前缀

简介: 本文以一次因`@property`误用导致的递归崩溃为引,深入剖析其原理:setter中`self.age = value`会无限调用自身。关键在于区分“接口”(`age`)与“存储”(`_age`),强调`_`前缀是约定而非强制,用于隔离内部实现与外部接口,避免逻辑混淆。

一个让我差点把电脑砸了的下午

先说我遇到过的一个事儿。

当时在写一个用户管理系统,有个 User 类,需要限制用户年龄在 0 到 150 之间。我觉得用 @property 特别合适——既有普通属性的访问方式,又能加校验逻辑,完美。

于是我写了这样的代码:

class User:
   def __init__(self, name, age):
       self.name = name
       self.age = age

   @property
   def age(self):
       return self.age

   @age.setter
   def age(self, value):
       if not 0 <= value <= 150:
           raise ValueError("年龄必须在 0 到 150 之间")
       self.age = value

看起来完全没问题,对吧?属性访问和赋值用起来和普通属性一样,背后还有校验逻辑。

然后我创建了一个用户:

u = User("张三", 25)

然后程序直接崩溃了。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (66).png

RecursionError: maximum recursion depth exceeded

递归深度超限。我盯着屏幕看了五分钟,完全不知道发生了什么。才创建了一个对象就爆栈?这代码里哪里有递归?

后来我才想明白——self.age = value 这一行,在 setter 内部调用了 setter 自身。

当时我真的恨不得把电脑砸了。原来在 @age.setter 里面写 self.age = value,不是赋值,是递归调用自己。

@property 的工作原理:它把“属性”变成了“方法”

在搞懂为什么 setter 里写 self.age = value 会递归之前,先看看 @property 到底在干什么。

@property 装饰器的作用是——把方法伪装成属性

class Person:
   def __init__(self, name):
       self._name = name

   @property
   def name(self):
       return self._name

这样你就可以用 p.name 来获取名字,而不是 p.name()。它看起来像个属性,用起来也像个属性,但实际上它是个方法。

@name.setter 是配套的装饰器,用来处理赋值操作:

@name.setter
def name(self, value):
   self._name = value

现在 p.name = "李四" 会触发这个 setter 方法。

这个机制本身很强大——你可以控制属性的读取和赋值行为,加校验、加日志、加缓存,啥都行。

但问题在于:当你在 setter 里面写 self.age = value 的时候,Python 会认为你又触发了 setter,于是再次调用它。setter 里面又调用了 setter,无限循环,直到栈溢出。

这是一个经典的逻辑错误,但用 @property 的时候特别容易掉进去——因为你觉得 self.age = value 是“给属性赋值”,但实际上是“调用 setter 方法”。

为什么 _ 前缀才是真正的“私密约定”

现在回到我之前写的那段错误代码。如果我当时这样写:

class User:
   def __init__(self, name, age):
       self._age = age   # 注意:用 _age,不是 age
       self.name = name

   @property
   def age(self):
       return self._age   # 返回的是内部存储

   @age.setter
   def age(self, value):
       if not 0 <= value <= 150:
           raise ValueError("年龄必须在 0 到 150 之间")
       self._age = value   # 设置的是内部存储

这样就不会递归了。setter 里赋值给 self._age,而 _age 只是一个普通属性,不会触发 setter。

这就是 _ 前缀的真正用途——把内部存储和公开属性区分开。

Python 里没有真正意义上的“私有”属性。你用 _name 还是 __name,都只是约定和名称修饰,不是真正的访问控制。但 _ 前缀的作用不在于“防止外部访问”,而在于 “这个是我的内部存储,外部不应该直接碰它”

回到属性装饰器的场景,_age 是真正的存储变量,age 是公开的访问接口。这个区分非常重要——如果你把存储变量和属性接口混用一个名字,递归就在那里等着你

__name 双下划线呢?

Python 里还有一种“私有”写法——双下划线前缀:

class Person:
   def __init__(self):
       self.__name = "张三"

   def get_name(self):
       return self.__name

双下划线会触发 “名称修饰”——Python 会把 __name 改成 _Person__name。这样做的主要目的是防止子类意外覆盖父类的属性,而不是真正的私有。

但双下划线有它自己的麻烦。比如你在调试的时候,属性名变成了 _Person__name,你打印 dir(obj) 看到一堆带类名的属性名,会很困惑。而且子类访问起来也很别扭。

所以很多 Python 开发者更倾向于用单下划线 _——它是约定俗成的“内部使用”,不会触发名称修饰,调试起来也清爽。

_ 前缀在 Python 社区里是一个共识:看到 _ 开头的东西,就知道“这是内部实现细节,外部不应该依赖它”。它不是强制的,但大家都遵守这个默契。

其实还有一个坑:property 和继承的关系

再补充一个顺带踩过的坑——@property 在继承里的表现和你想的也不一样。

class Parent:
   @property
   def name(self):
       return "Parent"

class Child(Parent):
   @property
   def name(self):
       return "Child"

子类重写父类的 property,这个没问题,和普通方法重写一样。

但如果你想重写父类 property 的 setter,事情就微妙了:

class Parent:
   @property
   def name(self):
       return self._name

   @name.setter
   def name(self, value):
       self._name = value

class Child(Parent):
   @name.setter   # 这里会报错——找不到 name
   def name(self, value):
       self._name = value.upper()

这种情况下,你不能直接用 @name.setter,因为父类的 name property 在子类作用域里访问起来比较绕。

正确的做法是重写整个 property,或者用更复杂的 super() 配合。但大多数情况下,如果你需要在子类里修改父类 property 的行为,直接用普通方法比 property 更省心。

这也是 property 的一个局限——它适合简单的封装,但在复杂的继承链里,行为会变得不那么直观。

什么时候用 property,什么时候不用?

踩完这些坑之后,我给自己定了一套使用规则:

@property 的场景:

  • 需要加校验逻辑(比如年龄范围、邮箱格式)
  • 需要做惰性计算(比如第一次访问时才去查数据库)
  • 需要把内部数据结构的变化对用户隐藏(比如把列表改成字典,但外部接口不变)

不用 @property 的场景:

  • 只是一个普通的值存储,没有任何附加逻辑——直接用普通属性
  • 赋值操作有复杂的副作用(比如修改一个属性要同时更新多个地方)——用普通方法更清晰
  • 继承层次复杂,子类需要频繁重写行为——用普通方法更可控

最重要的原则是:**@property 是用来“封装”的,不是用来“包装”的。** 如果你只是把 self.age 包装成 @property 然后直接返回 self._age,啥额外的逻辑都没有,那就纯属多此一举。直接写 self.age = value 就完了,别给自己找麻烦。

写在最后

那次爆栈之后,我每次写 @property 的 setter,都会下意识地看一眼里面有没有 self.xxx = value——如果有,立刻改成 self._xxx = value

这个习惯帮我避免了很多次同样的错误。

其实说白了,@property 的递归问题和 _ 前缀的约定,本质上都是同一个问题:你混淆了“接口”和“存储”

  • 接口是 age——外部用这个来读写,背后可能有一堆逻辑
  • 存储是 _age——真正的数据放在这儿,不对外暴露

把这两个分开,你就能安全地使用 @property。混在一起,递归就在前面等着你。

至于“私密约定”——_ 前缀不是用来“防止”别人访问的,而是用来“提醒”别人的。它说:“哥们,这个是我的内部数据,你别直接动。你真要动我也不拦你,但你得自己承担后果。”

Python 的哲学就是这样的——大家都是成年人,自己对自己负责。

希望你的代码里,永远不会出现 RecursionError。

目录
相关文章
|
27天前
|
数据采集 存储 Web App开发
豆瓣读书爬虫:爬取TOP250图书评分与书评,做推荐系统数据源
本文详解如何爬取豆瓣读书TOP250数据构建图书推荐系统:剖析反爬机制(User-Agent伪装、IP限频、Cookie登录态),提供完整Python爬虫代码(含隧道代理集成)、CSV/数据库存储方案,并延伸至协同过滤与内容推荐实践,兼顾合规性与工程落地。
111 0
|
23天前
|
Web App开发 人工智能 缓存
当 Codex Chrome 插件遇上开源视频剪辑 Skill:浏览器里的素材,开始直接流向可编辑时间线
2026年,Codex Chrome插件与开源Timeline Studio剪辑Skill深度协同:Agent可实时理解网页内容(含YouTube字幕、选中文本、多标签页),并驱动专业多轨时间线生成可编辑的`.timeline`工程,打通“浏览→理解→剪辑→迭代”全链路,让AI真正融入创作者工作流。
|
23天前
|
BI API 网络架构
科学运算-科学计算器使用说明-科学计算器API接口介绍
这是一款功能完备的科学计算器API,支持三角函数、对数指数、阶乘、统计、方程求解等百余种运算,严格遵循数学优先级规则。适用于校园学习、工程计算、科研实验及日常拓展,提供简洁REST接口,支持角度/弧度切换,响应快速准确。
87 1
jmeter:利用beanshell进行多重断言
在接口测试中,我们对返回结果的正确性判断一般是基于响应报文的返回内容进行断言。但有些时候,按照正常的业务逻辑来说,一个请求返回的内容是多种不同的。
jmeter:利用beanshell进行多重断言
|
22天前
|
人工智能 API 调度
万亿参数大模型平民化:Qwen3.8‑Max‑Preview 开发者上手全指南
随着AI产业向复杂工程开发、多智能体协同、超长文档深度解析方向演进,市场对于基座模型的综合推理、长文本理解、多模态处理与任务规划能力提出更高的要求。新一代旗舰基座Qwen3.8‑Max‑Preview预览版正式登场,作为突破万亿参数规格的大模型产品,总参数量达到2.4万亿,依托迭代升级的MoE混合专家架构,在推理性能、超长文本处理、多模态理解、复杂任务规划等维度实现全面升级,综合能力跻身全球第一梯队,对标海外顶级旗舰模型,为复杂工程开发、多智能体协同、超长文档解析、专业办公自动化场景提供优质国产基座选择。
215 2
|
23天前
|
人工智能 Linux API
Codex接入DeepSeek‑V4‑Flash完整实操:两套方案补齐识图能力保姆级教程
在AI编程Agent工具生态当中,Codex凭借强大的本地工程读写、代码修改、命令执行能力,成为开发者日常项目调试、代码重构、问题排查的常用客户端。DeepSeek‑V4‑Flash作为一款高性价比的文本大模型,拥有超大上下文窗口,Agent任务规划、代码生成、逻辑推演表现十分突出,API调用成本低廉,非常适合作为Codex底层推理基座。但是该模型属于纯文本推理模型,原生并不支持图像输入,当开发者把报错截图、UI界面截图、架构图、数据图表粘贴进会话,模型会直接提示无法解析图片内容,很多开发场景就此被卡住。
343 1
|
26天前
|
人工智能 运维 安全
通义千问Qwen3.8-Max旗舰模型详解:架构、百万上下文与多模态智能体能力
随着大模型技术持续向复杂工程、长周期自主任务、多模态闭环交互方向演进,通义千问Qwen3.8-Max作为当前千问系列的旗舰基座模型,在参数规模、长序列记忆、自主智能体、代码工程、跨模态理解等维度实现了跨越式升级,不再局限于单次问答、短文生成这类轻量化任务,而是面向真实业务里多步骤、长周期、需要自我校验迭代的复杂工作流打造。很多开发者、企业技术团队、科研人员在选型旗舰大模型时,都会关注模型底层架构、上下文承载力、编程交付能力、多模态支持范围,以及线上调用的实操方式,本文将从底层架构、核心功能、场景落地、API代码调用、使用注意事项几个维度,完整拆解Qwen3.8-Max的各项能力,帮助不同类型使
411 4
|
24天前
|
Java Linux 开发工具
IDEA官网下载2026|IDEA社区版安装+使用图文步骤
IntelliJ IDEA(简称IDEA)是JetBrains推出的主流Java集成开发环境,集代码编写、编译、调试、版本控制等功能于一体。提供免费社区版(支持Java/Kotlin/Maven/Gradle等)和付费旗舰版(增强Spring、数据库、前端等支持),开箱即用、智能提示强,适合学习与日常开发。
|
23天前
|
SQL 人工智能 分布式计算
阿里云大数据 AI 产品月刊-2026年7月
阿里云大数据& AI 产品技术月刊【2026 年 7 月】,涵盖 7 月技术速递、产品和功能发布、市场和客户应用实践等内容,帮助您快速了解阿里云大数据& AI 方面最新动态。
|
2月前
|
数据采集 人工智能 自然语言处理
自动化比价系统:从采集到数据清洗,全链路打通教程
本文详解淘宝/京东/拼多多三平台自动化比价系统全链路:用OpenClaw自然语言采集、站大爷隧道代理防封(24小时成功率98.2%+)、AI智能清洗价格(统一格式、核销优惠、去重校验),自动生成可决策的比价报告与飞书预警,真正实现“采得稳、洗得准、用得上”。
266 1