首页 / 资讯中心 / 文章详情

Python魔术方法__dir__:控制dir()输出与补全体验

Python魔术方法__dir__:控制dir()输出与补全体验 ★ FEATURED ARTICLE
聊到Python魔术方法dir__算是存在感很低的那一类。它平时不显山露水但只要你敲过dir(obj)就已经在跟它打交道了——dir()函数的底层协议就是__dir。这个系列走到现在在Python 3.12环境里逐一验证我发现__dir__的表现始终很稳定解释器在类型MRO里找它找到就调用找不到就按默认逻辑收集一堆属性名。这篇文章就专门把__dir__讲透默认规则、自定义方法、真实场景、常见坑以及落进业务代码里的最佳实践。不管你是刚入门想搞懂dir()为什么输出那些名字还是老手想在框架里控制接口面、优化IDE补全这篇都值得读完。1. __dir__是什么先把这个魔术方法看透1.1 从dir()函数说起dir()是Python内置函数行为分两种。不带参数调用时它返回当前局部作用域里的所有名字本质上是你当前这块代码的符号表带参数调用时它返回对象有效属性的列表。大多数人眼里dir(obj)就是快速看一个对象有什么属性的工具在交互式环境里一天能敲几十次。但很少有人停下来想dir(obj)返回的那些名字到底是从哪收集来的答案的关键就在__dir__。我直接给结论dir(obj)内部优先查找obj所属类型的MRO里有没有__dir__方法找到自定义版本就调用它如果没有自定义版本就走object类型的默认收集逻辑——把实例的__dict__里的键、类以及所有父类__dict__里的键统统收集起来去重、排序生成一个字符串列表。用一段交互式代码来看这个过程class A: def __init__(self): self.instance_attr 1 def method_a(self): pass class B(A): def method_b(self): pass b B() print(type(b).__mro__) print(dir(b))输出结果里既有instance_attr这类实例属性也有method_a、method_b这类类方法还有object附带的一大串双下划线名字。这些名字来源不同但最终都在dir()的结果里汇合。有个细节很多人不知道dir()返回的内容是排序后的。哪怕自定义__dir__返回一个乱序的列表最终交给使用者的也一定是排好序的版本。也就是说底层的sort逻辑是dir()函数帮你做的不是你__dir__该操心的。1.2 背后的查找规则为什么说__dir__是底层协议因为在CPython里dir(obj)本质上就是查找并调用__dir__的过程。查找规则有几个关键点我按优先级拆开讲。第一特殊方法查找基于类型不基于实例。这意味着解释器是在type(obj).mro__里搜索__dir而不是在obj自己的__dict__里搜索。也就是说你给某个实例动态绑定一个__dir__属性dir(obj)根本不会调用它。这个坑后面会展开这里先记住规则。第二如果找到的__dir__正好是object类提供的默认实现解释器会进入一套默认收集逻辑。这套逻辑对实例和类还有细微区别对普通实例收集实例__dict__的键加上类型MRO中所有类的__dict__键对类对象本身还会把元类的属性也纳进去。大多数时候你不需要记住这么多细节知道默认结果来自实例字典加类字典就够用了。第三如果类型MRO里出现了自定义的__dir__解释器就把话语权完全交给你你返回什么dir()就展示什么。这里有一个重要认知自定义__dir__实际上是接管了dir()的展示结果而不是在默认结果上做增量。你不把基类属性列进去用户就看不到基类属性。Python 3.12里这个查找链路没有变化。我在3.12.0和3.12.x几个版本上都试过行为一致没有出现行为漂移。对于这个系列来说__dir__是很踏实的一个主题学完基本不用回炉。1.3 返回值约定自定义__dir__的返回值必须是一个可迭代对象实践里最常用的是列表或元组。元素应该是字符串。话虽这么说我在代码评审里见过不少奇怪的返回值这里提前打个预防针。最经典的错误是返回字符串def __dir__(self): return hello字符串确实是可迭代的但迭代出来的是一个个字符于是dir(obj)会输出[e, h, l, l, o]这样的字母表。你说这是不是完全没意义所以记住__dir__要返回元素为字符串的可迭代对象千万不要因为字符串可迭代就偷懒。另外返回值里如果混入非字符串元素不同版本的处理略有差异。稳妥的方案是让调用方拿到一个干净的字符串列表。我习惯在返回前做一次过滤和处理让每个元素都确保是str同时去掉重复项和None。这个习惯能帮你绕开很多版本差异问题。2. 自定义__dir__的四个真实场景2.1 场景一接口收口让对象看起来更干净很多业务类内部有一堆下划线开头的方法和属性。比如一个支付渠道类内部要维护连接会话、缓存、重试计数器这些内部成员对使用方来说都是噪音。你希望调用者只看到charge、refund这类稳定的公共方法而不是一Tab补全冒出一整屏下划线。实现思路很简单自己收集公共名字过滤掉下划线开头的内容。class PaymentChannel: def __init__(self, channel_name): self.channel_name channel_name self._session None self._cache {} self._retry_times 0 def charge(self, amount): return amount * 1.01 def refund(self, order_id): return order_id def _init_session(self): self._session object() def __dir__(self): public [] for name in self.__dict__: if not name.startswith(_): public.append(name) for cls in type(self).__mro__: for name in cls.__dict__: if not name.startswith(_): public.append(name) return sorted(set(public))这个实现把实例自身的__dict__和类型MRO里的类属性都扫了一遍只保留不以单下划线开头的名字。效果是dir()输出的列表干净很多交互式环境下补全也清爽多了。必须强调一个边界__dir__是展示层的收口不是安全机制。用户照样可以通过getattr(obj, _session)直接访问内部状态。想真正阻止外部访问得靠前置下划线约定和代码审查而不是靠dir()做掩体。这一点我在团队里反复强调因为它太容易让人产生虚假安全感。2.2 场景二动态属性也有补全提示有些对象的属性不是直接在类和实例字典里写死的而是通过__getattr__动态解析的。典型例子是ORM模型数据库字段名存在一个映射或查询结构里代码里并没有为每个字段写一个类属性。这时候dir(obj)默认看不到这些字段名IDE的自动补全也不会提示它们。给这类对象加一个__dir__把动态字段名并进展示列表体验立刻不一样class Model: def __init__(self, fields: dict): self._fields fields def __getattr__(self, name): if name in self._fields: return self._fields[name] raise AttributeError(name) def __dir__(self): base super().__dir__() dynamic list(self._fields) return sorted(set(base dynamic))在这个例子里Model实例可以访问model.name、model.email这类字段而dir(model)和IDE补全也能把它们展示出来。注意__getattr__只负责找不到时的兜底它完全不参与dir()的默认结果所以必须由__dir__把动态属性翻译给外部工具看。这个模式在ORM、配置系统、规则引擎里特别实用。我做过一个轻量的配置对象底层是一个多层字典通过__getattr__做路径式访问配合__dir__之后团队成员在交互式环境里几乎不用翻文档就能找到配置项开发体验提升非常明显。2.3 场景三代理对象的门面展示代理类通常把__getattr__转发给内部目标对象。问题是在没加__dir__的情况下dir(proxy)展示的是代理自身的东西比如_target、_conn这些内部字段而不是被代理对象的能力。你明明代理的是一个数据库连接按Tab却看不到execute、commit这些方法很让人抓狂。解决方法一句话让__dir__返回被代理对象的dir()结果。class Proxy: def __init__(self, target): self._target target def __getattr__(self, name): return getattr(self._target, name) def __dir__(self): return dir(self._target)这个实现很直接但有个细节值得注意代理自身可能也要暴露自己的方法。如果直接把dir()交给_target代理自己的方法就没法展示了。更精细的写法是把两者合并def __dir__(self): own [name for name in super().__dir__() if not name.startswith(_)] target dir(self._target) return sorted(set(own target))这样代理暴露出来的属性既包括被代理对象的能力也包括自己提供的公共接口两边都不耽误。门面模式、服务定位器、远程调用封装都可以参考这个写法。2.4 场景四调试和运维用的信息面板有时候你想让dir(obj)展示的不是属性名而是值得关注的状态入口。比如一个后台服务实例使用者在交互式环境里敲dir(service)时你希望看到backend、active_tasks、last_error这些字段名——即使它们部分是从__getattr__动态生成的或者只是你希望使用者未来关注的名字。我在一个内部运维工具里就是这样做的把所有动态状态指标的访问器集中在一个管理对象里__dir__返回一组受控的名字这些名字对应的属性全部通过__getattr__兜底提供。效果是运维同学在故障排查时敲完对象名加Tab就能看到所有可能用到的状态项不用翻文档也不用靠记忆力。这种用法本质上是把dir()当作一个可交互的目录面板。它的价值在于低成本、零侵入地提升可探索性。调试器、REPL、自定义CLI、运维脚本里这个思路都能用上。3. 实操过程从零写一个可用的__dir__3.1 先跑通最小示例想验证__dir__的作用一个最小示例就够。我的习惯是先定义一个简单的类返回两个名字看看dir()的输出怎么变化。class Demo: def __init__(self): self.x 1 def __dir__(self): return [x, only_show_me] d Demo() print(dir(d))输出是[only_show_me, x]。注意两点列表被排序了only_show_me这个属性实际上并不存在但dir()依然会展示它。这说明__dir__返回的是一种展示性声明不要求列出的名字真的在对象上存在。这个特性有两面性用得好可以给使用者展示承诺的接口面用得不好会把根本不存在的名字列出来误导别人。所以我在设计时总是要求__dir__列出的名字要么真的能从对象身上取到要么由__getattr__完整兜底两者至少占一个。3.2 综合示例一个支持动态字段的业务对象把前面的知识点揉在一起我写一个更像真实业务的综合示例。这个类有固定方法、slots__约束、动态字段和自定义__dir各个机制互相配合。class Order: __slots__ (_fields, _loaded) def __init__(self, order_id, extra_fieldsNone): self._fields {order_id: order_id} self._loaded set() if extra_fields: self._fields.update(extra_fields) def __getattr__(self, name): if name in self._fields: return self._fields[name] raise AttributeError(name) def __dir__(self): base super().__dir__() names set(base) | set(self._fields) useful [name for name in names if not name.startswith(__)] return sorted(useful) order Order(20250101, {customer_name: 张三, amount: 99.5}) print(order.customer_name) print(dir(order))这个示例踩了好几个关键点。先说__slots__。它让实例不再有__dict__只能绑定声明好的slot名。有人会担心没有__dict__之后super().dir()还能正常工作吗能。object的默认dir()逻辑会检查实例有没有__dict__没有就跳过实例字典部分继续扫类型MRO里的类字典。__slots__生成的描述符就在类字典里所以dir()仍然能看到它们。再说base和useful的处理。我先用super().dir()拿到默认全集避免自己手工从MRO收集属性时漏掉继承来的东西然后把动态字段名并进去最后过滤掉双下划线开头的名字。这样展示出来的是一个既有继承方法、又有动态字段、又没有嘈杂特殊方法的名字集合非常适合普通业务对象。3.3 效果验证交互式补全代码写完了要验证。最直接的办法是到Python 3.12的交互式环境里跑一遍看看Tab补全和dir()的输出。我在终端里执行python进入REPL导入Order类创建实例然后敲order.再按Tab补全列表里能出现customer_name、amount这些动态字段同时也有order_id以及类里定义的方法。这个过程不需要重启解释器比写单元测试更直观。如果你是IPython或Jupyter用户补全机制基本一致底层依赖的就是对象的dir()结果。所以自定义__dir__对交互式开发体验的影响是立竿见影的。这里有另一个经验如果某个类加了__dir__但按Tab补全看不到预期结果优先检查返回列表里是否有空字符串、None、非字符串元素以及排序后是否真的包含目标名字。4. 踩坑记录与常见问题排查实录4.1 五个高频坑第一坑返回字符串。前面已经演示过这个坑非常容易踩因为新手总觉得字符串也是可迭代的返回它也没问题。实际结果是dir()把字符串拆成了单个字符列表完全没法看。排查方法很简单看看dir(obj)的输出是不是一堆单字母。修复方法也简单返回列表。def __dir__(self): return list(self._names) # 而不是 return abc第二坑dir__里忘了基类属性。如果你自定义__dir__时直接返回一个新列表而没有把父类和object的属性并进来那dir()的结果会异常空旷IDE补全连基本的__class、__str__都不提示。很多人的第一版__dir__都犯过这个错。我建议默认以super().dir()作为数据结构的地基再往里面加东西而不是从零开始搭。第三坑把__dir__当成访问控制。有人以为定义了__dir__外界就看不到那些属性了。事实完全不是这样。__dir__只是控制dir()这个查询工具的展示结果getattr(obj, secret)照样能访问。安全边界永远不能建立在展示层上这个认知必须刻在脑子里。第四坑在实例上动态绑定__dir__。因为特殊方法查找走的是类型MRO不是实例字典所以下面这种写法根本不会生效class Foo: pass f Foo() f.__dir__ lambda: [a] print(dir(f))输出还是默认的全量属性列表不会变成[a]。如果你确实需要每个实例有不同的dir结果正确的做法是在类里定义__dir__让它根据self的状态动态计算而不是往实例身上挂函数。第五坑返回值里混入非字符串。有些人的__dir__是从集合运算里拿结果的过程中不小心混入了None或整数。虽然某些版本会忽略非字符串但这个行为不可依赖。我见过一个线上小工具因为Python补丁版本升级dir()从静默忽略非字符串变成抛TypeError的情况。解决方案是返回前统一处理def __dir__(self): raw set(self._names) names {str(n) for n in raw if n is not None} return sorted(names)4.2 常见问题速查表遇到相关问题可以对症下药症状可能原因解决方案dir(obj)输出单个字母__dir__返回了字符串改成返回字符串列表dir(obj)结果里没有任何object方法__dir__没有包含基类属性用super().dir()打底再合并扩展Tab补全看不到动态字段忘了在__dir__里加入动态属性名把动态字段名并进返回列表实例上定义的__dir__不生效特殊方法查找基于类型MRO在类里定义__dir__基于self动态计算dir(obj)返回空列表__dir__过滤条件太严检查过滤逻辑保留必要基类名字补全列表出现None或数字返回值里混入非字符串返回前统一做类型清洗和去重这个表是我在代码评审时常用的自查清单。每一条都是真实见过的情况不是理论推演。4.3 先想清楚给谁看排查完问题我想多聊一层设计上的思考。__dir__的输出对象是谁是开发者、运维人员还是普通用户这决定了你的过滤策略完全不同。给开发者看下划线开头的名字恰恰是重要的路标全滤掉反而让人摸不着头脑。给运维面板看稳定性优先展示的名字应该都是公共且受支持的接口。给调研脚本看完整性和可探索性优先宁可多列也不要漏。我在不同项目里用了不同策略内部框架保留双下划线之外的一切业务收口类只展示公共接口运维工具则展示一个精心维护的固定集合。没有万能的__dir__写法只有适应当前场景的展示策略。5. __dir__的最佳实践清单5.1 应该做的事我的经验里一套健康的__dir__实现通常满足下面这些条件。以默认实现为地基。先调用super().dir()拿到全集再往里合并自己的增量。这样继承来的属性和object的通用方法不会丢。担心双下划线刷屏的话可以在合并之后做过滤而不是上来就放弃地基。返回排序去重后的字符串列表。sorted(set(...))这个组合基本就是标准答案。它既满足dir()对迭代器的要求也让结果稳定可预期。让展示与可访问性对齐。__dir__列出的名字应当真的能从对象身上访问到。如果名字对应的属性不存在请确保__getattr__等机制能够兜底。纯粹制造幻影属性的做法只会让使用者在运行时撞上AttributeError得不偿失。给__dir__写注释。在类里写明过滤策略和意图。我曾在一个共享组件里看到没注释的__dir__返回列表里既有业务字段又有奇怪的内部缩写代码评审时谁也说不清这个列表是干嘛的。后来加上一段注释说明这里只展示对外稳定接口内部成员请直接看源码分歧立刻消失。5.2 不要做的事不要返回空列表。dir(obj)返回空列表意味着交互式环境下按Tab什么都补全不出来这对使用者基本就是灾难。哪怕你觉得这个对象不用展示任何东西至少要保留object提供的通用方法。不要依赖__dir__做权限控制。它管不住getattr管不住vars更管不住对象内部的真正成员。权限和防泄漏要靠设计层面的访问控制而不是靠展示层。不要在每个类里写风格迥异的__dir__逻辑。团队项目里统一规则比个别优化重要。风格一致别人接手代码时才能快速判断行为。如果多个类都需要自定义dir考虑做一个公共的基类方法或工具函数把过滤逻辑收敛到一处。不要在返回列表里保留重复和脏数据。排序后去重是顺手的事成本极低但能避免不少排查补全问题时的困惑。5.3 落地检查清单最后给一个落地时的自查清单每一条都是我实际查过的问题dir(obj)的结果是否包含了使用者必需的属性和方法继承链上的关键方法还在不在object的通用方法还有没有动态字段是否已经在列表里并且确实可通过getattr访问返回结果是否排序后的字符串列表交互式环境下按Tab补全是否符合预期有没有依赖__dir__做安全控制的地方如果有立刻改掉。团队内部是否约定统一的展示规则背后有没有注释和说明按这份清单检查一遍你的__dir__实现基本就能在项目里稳定服役了。这个魔术方法虽然低调但把它的规则吃透之后你在接口设计、交互式工具、调试体验上都会多一张牌。我在实际项目里最常用它做服务面板一个对象把对外能力收拢成一张整齐的目录同事在调试时按Tab就能看到所有调用入口。开发体验这个东西很多时候就是靠这种小细节一点一点堆出来的。最后再分享一个小技巧你完全可以在单元测试里用assert set(dir(obj)) expected_public_api来锁定对象的公共接口契约接口一变更测试立刻报警比靠人盯代码靠谱得多。
阅读完成 · 觉得有帮助?
咨询建站