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

Question:

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 DriveImage ProcessingMapping Projectcoretypes.py", line 30, in __getattr__
    return self.__getitem__(attr_name)Lab
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 14, in __getitem__
    if self.raise_KeyError and key not in self.datastore:
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 30, in __getattr__
    return self.__getitem__(attr_name)
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 14, in __getitem__
    if self.raise_KeyError and key not in self.datastore:
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 30, in __getattr__
    return self.__getitem__(attr_name)
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 14, in __getitem__
    if self.raise_KeyError and key not in self.datastore:
  File "G:My DriveImage ProcessingMapping Projectcoretypes.py", line 30, in __getattr__
    return self.__getitem__(attr_name)
RecursionError: maximum recursion depth exceeded

Any help appreciated.

Asked By: Michael Sohnen

||

Answers:

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'])
Answered By: Tim Roberts

Okay, thank you to @tdelaney and @Tim Roberts for your help solving this problem. I would like to post the final code I got working, just to help others.

import warnings

class fluiddict:

    MEMBER_ATTRIBUTE_LIST = [
        "raise_KeyError",
        "datastore",
        "default_factory",
        "bypass_del_KeyError",
        "__getstate__" # Comes from pickling
    ]

    """!    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 self.is_defined(key)

    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 __getattr__(self, attr_name):
        if attr_name in fluiddict.MEMBER_ATTRIBUTE_LIST:
            return object.__getattr__(self,attr_name) 
        return self.__getitem__(attr_name)

    def __setattr__(self,attr_name,value):
        if attr_name in fluiddict.MEMBER_ATTRIBUTE_LIST:
            object.__setattr__(self,attr_name,value)
        else:
            self.__setitem__(attr_name,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 self.datastore[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
Answered By: Michael Sohnen