Python dataclass中__setattr__访问属性引发递归错误的排查与解决
Dataclass字段短别名实现的递归错误解析
1. 问题根源
- 无限递归触发:重写
__setattr__后,所有实例属性的赋值操作(包括dataclass初始化时设置short_name_map)都会进入自定义的__setattr__方法。而你的__setattr__第一行就尝试访问self.short_name_map,此时该属性还未被存入实例的__dict__,Python会触发属性查找逻辑,最终调用__getattr__;同时__getattr__中错误地写了self._short_name_map(多了下划线),会再次触发__getattr__,形成无限递归,导致栈溢出(递归错误)或内核崩溃。 - 属性名匹配错误:
__getattr__里引用的_short_name_map和实际定义的short_name_map不匹配,即使没有递归,也无法正确获取映射字典。
2. 解决方案
要避免递归,核心是在自定义属性方法中绕开自定义的访问逻辑,直接操作底层存储或使用类属性,同时修正属性名错误。下面提供几种可行方案:
方案一:修正__getattr__和__setattr__,直接操作__dict__
from dataclasses import dataclass, field @dataclass class Foo: very_long_name_of_a_number: int very_long_name_of_a_string: str short_name_map: dict = field( default_factory=lambda: { "number": "very_long_name_of_a_number", "string": "very_long_name_of_a_string", }, init=False, repr=False, ) def __getattr__(self, name): # 直接从__dict__取映射,避免触发递归 if name in self.__dict__.get('short_name_map', {}): return getattr(self, self.short_name_map[name]) raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'" ) def __setattr__(self, name, value): # 先从__dict__获取映射,绕开自定义属性逻辑 short_map = self.__dict__.get('short_name_map') if short_map and name in short_map: target_attr = short_map[name] self.__dict__[target_attr] = value else: # 非别名属性调用父类方法正常设置 super().__setattr__(name, value)
方案二:将映射设为类属性
映射规则是类级别的固定配置,无需每个实例存储一份,改为类属性可彻底避免实例属性访问的递归问题:
from dataclasses import dataclass @dataclass class Foo: very_long_name_of_a_number: int very_long_name_of_a_string: str # 类属性,所有实例共享 short_name_map = { "number": "very_long_name_of_a_number", "string": "very_long_name_of_a_string", } def __getattr__(self, name): if name in self.short_name_map: return getattr(self, self.short_name_map[name]) raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'" ) def __setattr__(self, name, value): if name in self.short_name_map: target_attr = self.short_name_map[name] self.__dict__[target_attr] = value else: super().__setattr__(name, value)
方案三:元类自动生成别名属性
如果需要更灵活的配置(比如通过字段注解指定别名),可以用元类在类创建时自动生成@property装饰的别名,无需手动编写每个属性的getter和setter:
from dataclasses import dataclass, fields class AliasMeta(type): def __new__(cls, name, bases, attrs): new_cls = super().__new__(cls, name, bases, attrs) # 遍历dataclass字段,读取metadata中的别名配置 for field in fields(new_cls): alias = field.metadata.get('alias') if alias: # 动态生成getter和setter def getter(self, field_name=field.name): return getattr(self, field_name) def setter(self, value, field_name=field.name): setattr(self, field_name, value) setattr(new_cls, alias, property(getter, setter)) return new_cls @dataclass class Foo(metaclass=AliasMeta): very_long_name_of_a_number: int = field(metadata={'alias': 'number'}) very_long_name_of_a_string: str = field(metadata={'alias': 'string'})
测试示例:
foo = Foo(123, "test") print(foo.number) # 输出: 123 foo.string = "new content" print(foo.very_long_name_of_a_string) # 输出: new content
内容的提问来源于stack exchange,提问作者Lukas
相关产品推荐
相关产品推荐

