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.
CodePudding user response:
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'])
CodePudding user response:
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