I've created a dictionary subclass with a set of default immutable keys.
from collections import MutableMapping
class Bones(MutableMapping):
"""Basic dict subclass for bones, that forbids bones from being deleted
and new bones from being added."""
def __init__(self, init_val=0):
self.bones = {"between_hips": init_val,
"bicep_right": init_val,
"bicep_left": init_val,
"arm_right": init_val,
"arm_left": init_val,
"thigh_right": init_val,
"thigh_left": init_val,
"leg_right": init_val,
"leg_left": init_val,
"spine_lower": init_val,
"spine_upper": init_val}
def __getitem__(self, item):
return self.bones[item]
def __setitem__(self, key, value):
if key in self.bones:
self.bones[key] = value
else:
raise KeyError("Can't add a new bone!")
def __delitem__(self, key):
raise TypeError("Can't delete bones")
def __iter__(self):
return iter(self.bones)
def __len__(self):
return len(self.bones)
Here are a few tests to check that it's working correctly:
from bones import Bones
bn = Bones()
bn["between_hips"] = 1
# these should all raise errors
bn["foo"] = 1
del bn["between_hips"]
Is this the proper way to create the subclass?
Background
My search to do this correctly started at the StackOverflow question "How to 'perfectly' override a dict" where I discovered there were two approaches to doing this. One was sub-classing an Abstract Base Class from the collections
module and the other was actually sub-classing dict
.
The justification for actually sub-classing dict
confused me. Specifically the discussion of the object properties of __dict__
and __slots__
. As best as I can tell from this question, I should be using __slots__
, but I'm super unclear how.
3 Answers 3
When reading the title of the question, I wondered: "Why use a dict subclass when a regular class with __slots__
can do?". And judging by the interface, it seems that it may fit.
Slots defines attributes names that are reserved for the use as attributes for the instances of the class. However, no more attributes can be added to the instances when defining __slots__
on the class:
>>> class Example:
... __slots__ = ('a', 'b', 'c')
...
>>> e = Example()
>>> e.a = 3
>>> e.d = 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Example' object has no attribute 'd'
>>> e.a
3
However, __slots__
are not auto-populated and doing so at class level is prohibited:
>>> class Example:
... __slots__ = ('a', 'b', 'c')
... a = 3
... b = 4
... c = 5
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: 'a' in __slots__ conflicts with class variable
You can, however, still define them in __init__
:
class Bones:
__slots__ = (
'between_hips',
'bicep_right',
'bicep_left',
'arm_right',
'arm_left',
'thigh_right',
'thigh_left',
'leg_right',
'leg_left',
'spine_lower',
'spine_upper',
)
def __init__(self, default_value=0):
for attribute in self.__slots__:
setattr(self, attribute, default_value)
Now the only thing left is to add a dictionary-like interface (and/or inherit from collections.abc.Mapping
):
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
setattr(self, key, value)
def __len__(self):
return len(self.__slots__)
def __iter__(self):
return iter(self.__slots__)
def items(self):
for attribute in self.__slots__:
yield attribute, getattr(self, attribute)
Or not necessarily, since you can still access the __slots__
as regular attributes, it all depend on your use case.
Note that with this implementation, you can still delete values, which you try to prohibit in your original implementation:
>>> b = Bones()
>>> b.bicep_right
0
>>> b['arm_left'] = 6
>>> b.arm_left
6
>>> del b['leg_left']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delitem__
>>> del b.leg_left
>>> b.leg_left
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: leg_left
If you wish to prevent that, you need to define __delattr__
on your class as well:
def __delattr__(self, key):
raise AttributeError(key)
-
\$\begingroup\$ Any reason not to inherit from
typing.Mapping
orcollections.abc.Mapping
to provide the basic scaffolding for a read onlydict
as you've implemented all the abstract methods - and you would get a few extra things for free, e.g.keys
,items
,values
, etc. \$\endgroup\$AChampion– AChampion2017年03月11日 04:56:15 +00:00Commented Mar 11, 2017 at 4:56 -
\$\begingroup\$ @AChampion no particular reason not to. I don't know the specifics of OP's need. So it's up to them to add it if they want to. \$\endgroup\$301_Moved_Permanently– 301_Moved_Permanently2017年03月11日 10:39:06 +00:00Commented Mar 11, 2017 at 10:39
First things first:
__dict__
is a storage of unspecified attributes. When you create a class and assign a attribute to it, you're by default assigning to this.__slots__
reduces memory, by specifying known attributes of the class. And removing__dict__
from the class.
The simplest way to show how these work it to mess around with them in the interpreter:
>>> class Slots:__slots__=['bar']
>>> s = Slots()
>>> s.__dict__
AttributeError: 'Slots' object has no attribute '__dict__'
>>> s.bar = 'bar'
>>> s.baz = 'baz'
AttributeError: 'Slots' object has no attribute 'baz'
>>> s.__dict__
AttributeError: 'Slots' object has no attribute '__dict__'
>>> class Dict:pass
>>> d = Dict()
>>> d.__dict__
{}
>>> d.bar = 'bar'
>>> d.baz = 'baz'
>>> d.__dict__
{'bar': 'bar', 'baz': 'baz'}
And so, if you're writing a class with a specific number of attributes, using __slots__
can reduce memory consumption, by not using __dict__
.
To note: this is not specific to your code, this is the same with any and all classes. And in most cases, if you ignore that these exist then your code will still work fine.
And so ignoring the above, as it's not needed, we need to look into which is better. Subclassing an ABC, or subclassing dict
.
To know which is better, you have to understand what a subclass and an interface are.
And what pros and cons come to both.
First, the best example for writing a subclass, is probably defaultdict
.
This is as a default dict wants to be an exact copy of dict
, it wants everything it has.
But, it wants to change the usage slightly, so that if a key is missing then you don't get an error.
And so you'd use something like:
class DefaultDict(dict):
__slots__ = ['default_factory']
def __init__(self, default_factory, d):
super().__init__(d)
self._default_factory = default_factory
def __missing__(self, key):
ret = self[key] = self._default_factory()
return ret
This, without us having to code it, allows you to use DefaultDict
almost exactly the same way as dict
.
It still has dict.update
, dict.items
, item getters, setters, and deletors.
To get the same with an ABC, you'd have to add each function to the class like you did.
This is good for cases when you want something that is a mapping, but doesn't have all the attributes or functions that dict
has.
And so this is the best one to go with, as you do not want dict.update
, as it can add keys that you don't want.
To improve your code, I'd recommend that you instead make the core Bones
class a separate type. Say a 'limited dict', or a 'half frozen dict'.
And then, you can go two routes, 1. subclass, so that you can add more methods, or 2. instantiate.
Showing how you could use 2 would be:
from collections import MutableMapping
class LimitedDict(MutableMapping):
__slots__ = ['_dict']
def __init__(self, dict):
self._dict = dict
def __getitem__(self, item):
return self._dict[item]
def __setitem__(self, key, value):
if key in self._dict:
self._dict[key] = value
else:
raise KeyError("Can't add a new bone!")
def __delitem__(self, key):
raise TypeError("Can't delete bones")
def __iter__(self):
return iter(self._dict)
def __len__(self):
return len(self._dict)
d = LimitedDict({'foo': 'bar'})
-
1\$\begingroup\$ Couldn't you simplify by using
Mapping
instead ofMutableMapping
? There's a lot of extras that come withMutableMapping
that also wouldn't work (pop
,update
,clear
, etc.). These are not defined forMapping
.Mapping
+__setitem__()
gives what the OP wants. \$\endgroup\$AChampion– AChampion2017年03月11日 05:00:32 +00:00Commented Mar 11, 2017 at 5:00
Maybe you should rewrite the magic method __setitem__
(or someone like this) of MutableMapping. Add your immutekey in the magic method.
I rewrite the __setitem__
of dict to immute some keys like below:
class ImmuteKey(dict):
def __setitem__(self, key, value):
if key in ['a','b','c']:
print("%s can't be used as a key!" % key)
else:
dict.__setitem__(self, key, value)
>>> a = ImmuteKey()
>>> a['a']='apple'
a can't be used as a key!
>>> a['s'] = 'solo'
>>> a
{'s': 'solo'}
Explore related questions
See similar questions with these tags.