HiveBrain v1.2.0
Get Started
← Back to all entries
snippetpythonModerate

Create dictionary with default immutable keys

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
createwithkeysdefaultimmutabledictionary

Problem

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 `__s

Solution

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 "", line 1, in 
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 "", line 1, in 
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 "", line 1, in 
AttributeError: __delitem__
>>> del b.leg_left
>>> b.leg_left
Traceback (most recent call last):
  File "", line 1, in 
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)

Code Snippets

>>> 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
>>> 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
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)
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)
>>> 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

Context

StackExchange Code Review Q#157297, answer score: 12

Revisions (0)

No revisions yet.