
本文介绍如何为 Python 实例创建动态、只读且支持 IDE/REPL 自动补全(如 IPython Tab 补全)的属性,通过重写 __getattr__、__setattr__ 和关键的 __dir__ 方法实现,避免类级描述符导致的跨实例污染问题。
本文介绍如何为 python 实例创建动态、只读且支持 ide/repl 自动补全(如 ipython tab 补全)的属性,通过重写 `__getattr__`、`__setattr__` 和关键的 `__dir__` 方法实现,避免类级描述符导致的跨实例污染问题。
要实现类似 Pandas DataFrame 的行为——即每个实例拥有独立的、可通过 obj.attr 访问的只读字段,且这些字段在交互式环境中(如 Jupyter 或 IPython)能被 Tab 键正确补全——核心在于三点:属性动态解析、写入严格拦截和补全目录显式声明。
单纯依赖 __getattr__ 是不够的,因为该方法仅在常规属性查找失败时触发,而自动补全(如 IPython 的 dir(obj) 调用)默认只返回 __dict__ 和类定义中的属性名,不会包含运行时字典中的键。因此,必须重写 __dir__ 方法,主动将内部字典的键注入补全列表。
以下是一个健壮、生产就绪的实现:
from types import MappingProxyType
from typing import Any, List, Dict
class DynamicReadOnly:
def __init__(self, keys: List[str], values: List[Any]) -> None:
# 使用普通 dict 存储,便于初始化和内部操作
self._d: Dict[str, Any] = dict(zip(keys, values))
@property
def d(self) -> MappingProxyType:
"""公开只读视图,防止外部修改底层字典"""
return MappingProxyType(self._d)
def __getattr__(self, name: str) -> Any:
"""当访问不存在的常规属性时,尝试从 _d 中获取"""
try:
return self._d[name]
except KeyError:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
def __setattr__(self, name: str, value: Any) -> None:
"""禁止对已存在于 _d 中的 key 进行赋值;允许设置新属性(如 _d 本身)"""
if name != "_d" and hasattr(self, "_d") and name in self._d:
raise TypeError(f"Cannot assign to read-only attribute '{name}'")
super().__setattr__(name, value)
def __dir__(self) -> List[str]:
"""增强 dir() 结果,使 Tab 补全显示动态键"""
# 获取父类的默认属性列表
attrs = super().__dir__()
# 移除内部字典名(避免暴露 _d)
if "_d" in attrs:
attrs.remove("_d")
# 添加所有动态键(仅限字符串键,确保安全)
attrs.extend(k for k in self._d.keys() if isinstance(k, str))
return sorted(set(attrs)) # 去重并排序,提升可读性✅ 使用示例:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
立即学习“Python免费学习笔记(深入)”;
a = DynamicReadOnly(['x', 'y'], [10, "hello"])
b = DynamicReadOnly(['z'], [3.14])
print(a.x) # → 10
print(b.z) # → 3.14
print(a.d) # → mappingproxy({'x': 10, 'y': 'hello'})
# 尝试修改 → 抛出 TypeError
# a.x = 99 # TypeError: Cannot assign to read-only attribute 'x'
# 尝试访问不存在的键 → 抛出 AttributeError
# a.missing # AttributeError: 'DynamicReadOnly' object has no attribute 'missing'
# 在 IPython 中输入 `a.` + Tab → 将显示 `a.x`, `a.y`, `a.d`(不含 `z`)
# 同理,`b.` + Tab → 仅显示 `b.z`, `b.d`⚠️ 注意事项:
-
__dir__是补全的关键:它直接决定dir(obj)返回的内容,而大多数 REPL 和 IDE(包括 VS Code、PyCharm 的智能提示)均依赖此方法生成候选列表; -
MappingProxyType用于安全暴露_d的只读视图,比手动封装__getitem__更符合 Python 惯例; -
__setattr__中需先检查hasattr(self, "_d"),以避免__init__初始化_d时触发误判; - 若需完全禁止新增实例属性(即让对象“冻结”),可额外添加
__slots__ = ("_d",),但会牺牲灵活性; - 动态键名应为合法标识符(如不能含空格或以数字开头),否则虽可存储,但无法通过点号访问(
obj.123语法错误),此时仍需用obj.d["123"]访问。
这种模式平衡了动态性与安全性,是构建配置对象、轻量数据容器或 DSL 接口的理想选择。

















