news 2026/6/14 10:37:02

Python 属性访问的暗门:彻底搞懂 `__getattr__` 和 `__getattribute__` 的区别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 属性访问的暗门:彻底搞懂 `__getattr__` 和 `__getattribute__` 的区别

Python 属性访问的暗门:彻底搞懂__getattr____getattribute__的区别

在 Python 编程中,很多初学者第一次看到__getattr____getattribute__时,都会产生一个疑问:它们名字这么像,都是“获取属性”,到底有什么区别?

这个问题看似只是一个语法点,实际却通向 Python 对象模型的核心。理解它,你会更懂 ORM、代理对象、配置系统、懒加载、RPC 客户端、动态 API 封装等高级设计。很多优秀框架之所以“写起来像魔法”,背后往往就藏着这些双下划线方法。

本文将从基础语法讲起,逐步深入到属性查找流程、实战案例、常见陷阱与最佳实践,帮助你真正掌握这两个方法。


一、先给结论:一句话区分它们

__getattribute__会在访问对象的任何属性时被调用。

__getattr__只会在正常属性查找失败时被调用。

也就是说:

obj.name

当你访问obj.name时,Python 首先会进入__getattribute__。如果属性找不到,并且类里定义了__getattr__,才会继续调用__getattr__作为兜底处理。

可以把它们理解成:

__getattribute__:属性访问的总入口 __getattr__:属性不存在时的后备方案

二、最小示例:直观看出调用时机

先看__getattr__

classUser:def__init__(self):self.name="Alice"def__getattr__(self,item):print(f"__getattr__ 被调用,找不到属性:{item}")return"默认值"user=User()print(user.name)print(user.age)

输出结果:

Alice __getattr__ 被调用,找不到属性:age 默认值

访问user.name时,实例里本来就有name,所以不会调用__getattr__

访问user.age时,Python 找不到这个属性,于是才调用__getattr__

再看__getattribute__

classUser:def__init__(self):self.name="Alice"def__getattribute__(self,item):print(f"__getattribute__ 被调用,正在访问:{item}")returnobject.__getattribute__(self,item)user=User()print(user.name)

输出结果:

__getattribute__ 被调用,正在访问:name Alice

无论属性是否存在,__getattribute__都会先被调用。


三、Python 属性查找的大致流程

当执行下面代码时:

value=obj.attr

Python 并不是简单地去对象里拿一个字段,而是会经历一套查找流程。

简化流程如下:

访问 obj.attr ↓ 调用 obj.__getattribute__("attr") ↓ 查找数据描述符 ↓ 查找实例 __dict__ ↓ 查找类属性和非数据描述符 ↓ 如果仍然失败,抛出 AttributeError ↓ 如果定义了 __getattr__,调用 __getattr__("attr")

用 Mermaid 表达更直观:

找到

未找到或抛出 AttributeError

访问 obj.attr

调用 __getattribute__

属性是否找到

返回属性值

是否定义 __getattr__

调用 __getattr__

抛出 AttributeError

所以,__getattr__并不是属性访问的第一站,而是最后的补救机制。


四、核心区别对比表

对比项__getattribute____getattr__
调用时机每次访问属性都会调用只有属性不存在时调用
风险很容易导致无限递归相对安全
适用场景全局拦截、权限控制、调试跟踪、代理对象懒加载、动态属性、兼容旧字段、默认值
是否必须手动调用父类方法通常必须通常不需要
性能影响较大,因为每次属性访问都会经过它较小,只处理缺失属性
推荐程度谨慎使用更常用、更安全

一句实践建议:

普通业务代码优先使用__getattr__;只有确实需要拦截所有属性访问时,才考虑__getattribute__


五、__getattr__的典型场景:动态属性与兜底处理

1. 给不存在的属性提供默认值

classConfig:def__init__(self):self.host="localhost"self.port=8000def__getattr__(self,name):returnNoneconfig=Config()print(config.host)print(config.debug)

输出:

localhost None

这在配置系统里很常见。某些配置项可能没有定义,但你不希望程序立即崩溃。

不过,实际项目中不建议所有缺失属性都返回None,因为它可能掩盖拼写错误。

更稳妥的写法是只处理特定字段:

classConfig:defaults={"debug":False,"timeout":30,"retries":3,}def__getattr__(self,name):ifnameinself.defaults:returnself.defaults[name]raiseAttributeError(f"{type(self).__name__}没有属性{name}")

这里有一个非常重要的原则:

如果__getattr__不能处理某个属性,一定要抛出AttributeError,不要随便返回一个值。


2. 实现懒加载

懒加载是__getattr__的高频应用。比如某个对象的属性计算成本很高,只有真正访问时才加载。

classReport:def__init__(self,path):self.path=pathdef__getattr__(self,name):ifname=="data":print("正在加载数据...")data=self._load_data()self.data=datareturndataraiseAttributeError(f"{name}不存在")def_load_data(self):return["row1","row2","row3"]report=Report("sales.csv")print("对象已创建")print(report.data)print(report.data)

输出:

对象已创建 正在加载数据... ['row1', 'row2', 'row3'] ['row1', 'row2', 'row3']

第一次访问report.data时,属性不存在,于是进入__getattr__。加载完成后,我们把data写回实例,第二次访问就不会再触发__getattr__

这类技巧在数据分析、机器学习模型加载、大文件解析、远程 API 客户端中非常有用。


六、__getattribute__的典型场景:全局拦截属性访问

__getattribute__更像一扇总闸门。任何属性访问都必须经过它。

例如,我们想记录对象所有属性的访问行为:

classTrackedObject:def__init__(self):self.name="Python"self.version="3.x"def__getattribute__(self,name):print(f"访问属性:{name}")returnobject.__getattribute__(self,name)obj=TrackedObject()print(obj.name)print(obj.version)

输出:

访问属性:name Python 访问属性:version 3.x

注意这里的关键代码:

returnobject.__getattribute__(self,name)

为什么不能写成下面这样?

returnself.__dict__[name]

因为访问self.__dict__本身也会触发__getattribute__,然后又访问self.__dict__,无限递归,最终导致:

RecursionError: maximum recursion depth exceeded

正确写法永远是通过父类方法绕开当前拦截逻辑:

object.__getattribute__(self,name)

七、经典陷阱:无限递归

这是很多开发者第一次使用__getattribute__时最容易踩的坑。

错误示例:

classUser:def__init__(self):self.name="Alice"def__getattribute__(self,name):print(f"访问:{name}")returnself.__dict__[name]

看似合理,实际会炸。

原因是:

self.__dict__

也是一次属性访问,它会再次调用__getattribute__。于是程序进入死循环。

正确写法:

classUser:def__init__(self):self.name="Alice"def__getattribute__(self,name):print(f"访问:{name}")data=object.__getattribute__(self,"__dict__")returndata[name]

更通用的写法是:

classUser:def__init__(self):self.name="Alice"def__getattribute__(self,name):print(f"访问:{name}")returnobject.__getattribute__(self,name)

记住一句话:

__getattribute__内部访问对象属性时,要特别小心,优先使用object.__getattribute__


八、两者如何配合工作?

如果一个类同时定义了__getattribute____getattr__,会发生什么?

classDemo:def__init__(self):self.exists="我存在"def__getattribute__(self,name):print(f"先进入 __getattribute__:{name}")returnobject.__getattribute__(self,name)def__getattr__(self,name):print(f"再进入 __getattr__:{name}")return"兜底值"demo=Demo()print(demo.exists)print(demo.missing)

输出:

先进入 __getattribute__:exists 我存在 先进入 __getattribute__:missing 再进入 __getattr__:missing 兜底值

流程非常清晰:

访问已存在属性时,只走__getattribute__

访问不存在属性时,先走__getattribute__,查找失败后,再走__getattr__


九、实战案例一:把字典变成可点号访问对象

很多配置文件来自 JSON 或 YAML,默认是字典形式:

config={"database":{"host":"localhost","port":5432}}

访问时需要写:

config["database"]["host"]

我们可以用__getattr__封装成点号访问:

classDotDict:def__init__(self,data):self._data=datadef__getattr__(self,name):try:value=self._data[name]exceptKeyError:raiseAttributeError(f"没有配置项:{name}")ifisinstance(value,dict):returnDotDict(value)returnvalue config=DotDict({"database":{"host":"localhost","port":5432},"debug":True})print(config.database.host)print(config.database.port)print(config.debug)

这个案例非常实用,适合用于小型项目配置读取。

但在生产环境中,还要考虑类型校验、默认值、错误提示、配置来源优先级等问题。成熟项目可以使用 Pydantic、dataclasses 或专门的配置管理库来增强可靠性。


十、实战案例二:API 客户端的动态方法

假设我们要封装一个远程接口客户端,希望这样调用:

client.users()client.orders()client.products()

但这些 API 路径不想一个个手写,可以用__getattr__动态生成请求函数。

classAPIClient:def__init__(self,base_url):self.base_url=base_urldef__getattr__(self,name):defendpoint(**params):print(f"请求地址:{self.base_url}/{name}")print(f"请求参数:{params}")return{"status":"ok","resource":name}returnendpoint client=APIClient("https://api.example.com")print(client.users(page=1))print(client.orders(status="paid"))

输出:

请求地址:https://api.example.com/users 请求参数:{'page': 1} {'status': 'ok', 'resource': 'users'} 请求地址:https://api.example.com/orders 请求参数:{'status': 'paid'} {'status': 'ok', 'resource': 'orders'}

这类设计在 SDK、RPC 框架、自动化工具中很常见。

不过要注意:动态 API 虽然灵活,但会降低 IDE 自动补全能力。因此更推荐在内部工具或高度动态的场景中使用,公共 SDK 则应补充文档、类型注解和明确的错误提示。


十一、实战案例三:使用__getattribute__做敏感字段保护

假设我们有一个用户对象,不希望外部直接读取密码字段:

classSecureUser:def__init__(self,username,password):self.username=username self.password=passworddef__getattribute__(self,name):ifname=="password":raiseAttributeError("password 是敏感字段,禁止直接访问")returnobject.__getattribute__(self,name)user=SecureUser("alice","secret")print(user.username)print(user.password)

输出:

alice AttributeError: password 是敏感字段,禁止直接访问

这只是一个演示。在真实系统中,敏感字段不应该以明文形式存储在对象中,更不应该只依赖属性访问拦截来保障安全。正确方式应包括哈希存储、权限控制、日志脱敏和数据传输加密。

但这个例子说明了__getattribute__的能力:它可以拦截所有访问,包括已经存在的属性。

如果用__getattr__,就无法拦截password,因为password本来就存在。


十二、性能差异:不要滥用__getattribute__

由于__getattribute__每次属性访问都会执行,它对性能更敏感。

例如:

classNormal:def__init__(self):self.x=1classHooked:def__init__(self):self.x=1def__getattribute__(self,name):returnobject.__getattribute__(self,name)

在大量循环访问属性时,Hooked往往会更慢。虽然多数业务系统中这点差异不一定成为瓶颈,但在高频访问对象、数据处理、数值计算、序列化框架等场景中,这种额外开销值得关注。

实践建议:

只需要处理“缺失属性”时,用 __getattr__ 必须拦截“所有属性”时,才用 __getattribute__

十三、与hasattr的关系

很多人不知道,hasattr(obj, "name")本质上也会触发属性访问。

例如:

classDemo:def__getattr__(self,name):print(f"查找缺失属性:{name}")raiseAttributeError(name)demo=Demo()print(hasattr(demo,"x"))

输出:

查找缺失属性:x False

hasattr会尝试读取属性,如果捕获到AttributeError,就返回False

因此,如果你的__getattr__没有正确抛出AttributeErrorhasattr的行为也会变得不可靠。

错误示例:

classBad:def__getattr__(self,name):returnNonebad=Bad()print(hasattr(bad,"anything"))

输出:

True

因为__getattr__返回了None,没有抛出AttributeError,所以hasattr认为属性存在。

这也是为什么前面反复强调:无法处理的属性,一定要抛出AttributeError


十四、最佳实践清单

1. 优先选择__getattr__

如果你的目标是:

动态字段 默认配置 懒加载 兼容旧属性名 把字典包装成对象

优先使用__getattr__

它更安全、更容易维护,也不会影响所有属性访问。


2. 谨慎使用__getattribute__

只有当你需要:

记录所有属性访问 实现访问控制 制作透明代理 拦截已有属性 构建底层框架能力

才考虑__getattribute__

并且要牢牢记住:

object.__getattribute__(self,name)

这是避免递归的关键。


3. 不要吞掉所有错误

不推荐这样写:

def__getattr__(self,name):returnNone

更推荐这样:

def__getattr__(self,name):ifnameinself.defaults:returnself.defaults[name]raiseAttributeError(f"{name}不存在")

清晰的错误比沉默的默认值更有价值。沉默会让 bug 藏得更深。


4. 给动态行为补充文档和类型提示

动态属性很灵活,但也会让读代码的人困惑。

如果一个对象的属性并不直接写在类定义里,建议补充:

文档说明 类型注解 单元测试 示例代码 错误提示

这不仅是代码质量问题,也是团队协作问题。


十五、面试中如何回答这个问题?

如果面试官问:__getattr____getattribute__的区别是什么?

你可以这样回答:

__getattribute__是属性访问的总入口,每次访问对象属性都会调用它;__getattr__是兜底方法,只有当正常属性查找失败并抛出AttributeError时才会调用。前者能力更强,但容易造成无限递归,通常需要通过object.__getattribute__访问真实属性;后者更适合实现动态属性、懒加载、默认值和兼容逻辑。实践中应优先使用__getattr__,只有在需要拦截所有属性访问时才使用__getattribute__

如果能再补一个例子,效果会更好:

classDemo:def__getattribute__(self,name):print("__getattribute__",name)returnobject.__getattribute__(self,name)def__getattr__(self,name):print("__getattr__",name)return"default"

这说明你不仅知道概念,还理解调用流程。


十六、总结:理解魔法方法,少写魔法代码

Python 的魅力在于简洁,也在于开放。它允许我们深入对象模型,重写属性访问、函数调用、上下文管理、迭代协议等核心行为。

__getattr____getattribute__正是这种开放性的代表。

它们能让代码变得优雅,也能让代码变得晦涩。关键不在于“能不能用”,而在于“该不该用”。

最后记住三句话:

__getattribute__:每次访问属性都会调用。 __getattr__:属性找不到时才会调用。 能用 __getattr__ 解决的问题,就不要急着用 __getattribute__。

对于初学者来说,掌握这两个方法,是理解 Python 对象模型的重要一步。

对于资深开发者来说,真正的功力不只是会写这些魔法方法,而是知道什么时候不用它们。

技术的成长,往往不是从写出更复杂的代码开始,而是从写出更清晰、更可靠、更容易被他人理解的代码开始。

你在项目中用过__getattr____getattribute__吗?是为了懒加载、代理对象、配置封装,还是调试追踪?欢迎在评论区分享你的案例,也许你的一个实践经验,正好能帮另一个开发者少踩一个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/14 10:36:25

PyPI本质解析:包名、导入名与Wheel分发机制

1. 这不是“安装教程”,而是一份 Python 开发者真正需要的 PyPI 实战手札你刚学完print("Hello, World!"),兴冲冲想用pandas读个 Excel,结果在终端敲下pip install pandas后卡在了 “Collecting pandas” 十分钟不动;或…

作者头像 李华
网站建设 2026/6/14 10:36:00

告别估算!用RUSLE模型+ArcGIS Pro,手把手计算你家乡的土壤侵蚀强度

从零实战:用RUSLE模型与ArcGIS Pro精准测算土壤侵蚀强度土壤侵蚀如同无声的生态杀手,每年导致全球约240亿吨表土流失。在黄土高原地区,一场暴雨可能让农民眼睁睁看着耕作层被冲刷殆尽;而在南方红壤区,看似缓慢的侵蚀实…

作者头像 李华
网站建设 2026/6/14 10:28:01

视频转PPT:如何从3小时会议录像中提取出完美演示文稿

视频转PPT:如何从3小时会议录像中提取出完美演示文稿 【免费下载链接】extract-video-ppt extract the ppt in the video 项目地址: https://gitcode.com/gh_mirrors/ex/extract-video-ppt 你是否曾经面对长达数小时的会议录像或教学视频,需要从中…

作者头像 李华