7
\$\begingroup\$

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.

\$\endgroup\$
8
\$\begingroup\$

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)
\$\endgroup\$
  • \$\begingroup\$ Any reason not to inherit from typing.Mapping or collections.abc.Mapping to provide the basic scaffolding for a read only dict 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 Mar 11 '17 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\$ – Mathias Ettinger Mar 11 '17 at 10:39
4
\$\begingroup\$

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'})
\$\endgroup\$
  • 1
    \$\begingroup\$ Couldn't you simplify by using Mapping instead of MutableMapping? There's a lot of extras that come with MutableMapping that also wouldn't work (pop, update, clear, etc.). These are not defined for Mapping. Mapping + __setitem__() gives what the OP wants. \$\endgroup\$ – AChampion Mar 11 '17 at 5:00
0
\$\begingroup\$

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'}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.