整合dict、defaultdict与SimpleNamespace的自定义类遇无限递归错误
解决fluiddict类中__getattr__/__setattr__的无限递归问题
我正在编写一个整合dict、defaultdict和SimpleNamespace功能的Python类,目前已有如下代码:
import warnings class fluiddict: """! A class that emulates a dictionary, while also being able to support attribute assignment and default values. The default value of `default_factory` is None. This means a KeyError will be raised when non-existent data is requested To specify a default value of None, use `default_factory=lambda key: None` """ def __contains__(self, key): return key in self.datastore def __getitem__(self,key): if self.raise_KeyError and key not in self.datastore: raise KeyError(f"Key '{key}' was not found in the datastore and no default factory was provided.") if key not in self.datastore: try: return self.default_factory(key) except Exception as e: print("An unknown exception occured while trying to provide a default value. Is your default factory valid?") raise e return self.datastore[key] def __setitem__(self,key,value): self.datastore[key] = value def __delitem__(self, key): if key not in self.datastore: if not self.bypass_del_KeyError: raise KeyError(f"Key {key} was not found in the datastore.") else: warnings.warn(f"Attemping to delete nonexistent key {key} in the datastore. Ignoring del statement...") else: del self.datastore[key] def is_defined(self,key): return key in self.datastore def is_set(self,key): #PHP-style `isset` function return key in self.datastore and key is not None def __init__(self, default_factory =None, bypass_del_KeyError=False): self.datastore = {} self.raise_KeyError = False if default_factory is None: self.raise_KeyError = True self.bypass_del_KeyError = bypass_del_KeyError
该代码可正常运行,但添加以下__getattr__和__setattr__方法以实现SimpleNamespace式功能时,触发了无限递归错误:
def __getattr__(self,attr_name): return self.__getitem__(attr_name) def __setattr__(self,attr_name,value): self.__setitem__(attr_name,value)
报错回溯信息:
File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 30, in __getattr__ return self.__getitem__(attr_name)Lab File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 14, in __getitem__ if self.raise_KeyError and key not in self.datastore: File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 30, in __getattr__ return self.__getitem__(attr_name) File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 14, in __getitem__ if self.raise_KeyError and key not in self.datastore: File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 30, in __getattr__ return self.__getitem__(attr_name) File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 14, in __getitem__ if self.raise_KeyError and key not in self.datastore: File "G:\My Drive\Image Processing\Mapping Project\core\types.py", line 30, in __getattr__ return self.__getitem__(attr_name) RecursionError: maximum recursion depth exceeded
问题原因
无限递归的核心原因是:
- 自定义的
__setattr__会拦截所有属性赋值操作,包括初始化时self.datastore = {}、self.raise_KeyError = False这些类自有属性的设置,导致这些属性没有被存入实例的__dict__,而是被当作键值对存到了self.datastore字典中。 - 当
__getitem__方法中访问self.raise_KeyError或self.datastore时,Python找不到这些实例属性,会触发__getattr__,进而调用__getitem__,形成循环调用。
解决方案
修改__setattr__和__getattr__,区分类的自有属性和用户要存储的键值对:
- 在
__setattr__中,若要设置的是类的自有属性(datastore、raise_KeyError、bypass_del_KeyError、default_factory),则调用父类的__setattr__直接设置到实例;否则调用__setitem__存入datastore。 __getattr__保持逻辑不变,但此时自有属性已正确存入实例,不会触发不必要的调用。
修改后的完整代码:
import warnings class fluiddict: """! A class that emulates a dictionary, while also being able to support attribute assignment and default values. The default value of `default_factory` is None. This means a KeyError will be raised when non-existent data is requested To specify a default value of None, use `default_factory=lambda key: None` """ # 定义类的自有属性列表 _own_attrs = {'datastore', 'raise_KeyError', 'bypass_del_KeyError', 'default_factory'} def __contains__(self, key): return key in self.datastore def __getitem__(self,key): if self.raise_KeyError and key not in self.datastore: raise KeyError(f"Key '{key}' was not found in the datastore and no default factory was provided.") if key not in self.datastore: try: return self.default_factory(key) except Exception as e: print("An unknown exception occured while trying to provide a default value. Is your default factory valid?") raise e return self.datastore[key] def __setitem__(self,key,value): self.datastore[key] = value def __delitem__(self, key): if key not in self.datastore: if not self.bypass_del_KeyError: raise KeyError(f"Key {key} was not found in the datastore.") else: warnings.warn(f"Attemping to delete nonexistent key {key} in the datastore. Ignoring del statement...") else: del self.datastore[key] def is_defined(self,key): return key in self.datastore def is_set(self,key): #PHP-style `isset` function return key in self.datastore and key is not None def __init__(self, default_factory =None, bypass_del_KeyError=False): # 直接用父类__setattr__设置自有属性,避免被自定义__setattr__拦截 super().__setattr__('datastore', {}) super().__setattr__('raise_KeyError', False) super().__setattr__('default_factory', default_factory) if default_factory is None: super().__setattr__('raise_KeyError', True) super().__setattr__('bypass_del_KeyError', bypass_del_KeyError) def __getattr__(self, attr_name): return self.__getitem__(attr_name) def __setattr__(self, attr_name, value): if attr_name in self._own_attrs: super().__setattr__(attr_name, value) else: self.__setitem__(attr_name, value)
验证
现在可以正常使用属性访问和字典访问两种方式:
fd = fluiddict(default_factory=lambda k: 0) fd.name = "test" print(fd.name) # 输出 test print(fd['age']) # 输出 0 print(fd.datastore) # 输出 {'name': 'test'}
内容的提问来源于stack exchange,提问作者Michael Sohnen
相关产品推荐
相关产品推荐

