Search code examples
pythonpython-3.xdictionarydefaultdictpython-simplenamespace

Infinite recursion error in custom class that combines dict, defaultdict, and SimpleNamespace functionality


I am writing a class in python that combines the functionality of dict, defaultdict, and SimpleNamespace

So far I have the following code:

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

The code works, but I cannot figure out how to write a __getattr__ or __setattr__ function that provides SimpleNamespace-like functionality without infinite recursion.

With the following additional code, I get an infinite recursion error. I think it's because the self. syntax calls __getattr__ under the hood. I find this odd, since I have seen in other SO posts that __setattr__ and __getattr__ will only be called if the attribute wasn't found normally.

If I add the code:

def __getattr__(self,attr_name):
    return self.__getitem__(attr_name)

def __setattr__(self,attr_name,value):
    self.__setitem__(attr_name,value)

I get the following traceback:

  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

Any help appreciated.


Solution

  • I think this implements what you want. You can call the base class __setattr__ to allow the write.

    class xdict:
        def __init__(self):
            self.datastore = {}
        def __getitem__(self,key):
            if key not in self.datastore:
                raise KeyError(f'{key} not found')
            if key not in self.datastore:
                self.datastore[key] = 7
            return self.datastore[key]
        def __setitem__(self,key,value):
            self.datastore[key] = value
        def __delitem__(self,key):
            if key not in self.datastore:
                raise KeyError(f'{key} not found')
            del self.datastore[key]
        def __getattr__(self,attr):
            print("getattr",attr)
            if attr == 'datastore':
                return getattr(self,attr)
            return getattr(self,'datastore')[attr]
        def __setattr__(self,attr,val):
            print("setattr",attr)
            if attr == 'datastore':
                object.__setattr__(self,'datastore',val)
            else:
                getattr(self,'datastore')[attr] = val
    
    x = xdict()
    x['one'] = 'one'
    x.one = 'seven'
    print(x['one'])
    print(x.one)
    print(x['two'])