views:

128

answers:

2

How can I make as "perfect" a subclass of dict as possible? The end goal is to have a simple dict in which the keys are lowercase.

It would seem that should be some tiny set of primitives I can override to make this work, but all my research and attempts have made it seem like this isn't the case:

Here is my first go at it, get() doesn't work at least, and no doubt there are many minor subtle problems. Damn you Python, you're supposed to make stuff easy.

class arbitrary_dict(dict):
  """A dictionary which applies an arbitrary key-altering function before accessing the keys"""

  def __keytransform__(self, key):
    return key

  # Overrided methods. List from a http://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict
  def __init__(self, *args, **kwargs):
    self.update(*args, **kwargs)

  # Use dict directly, since super(dict, self) doesn't work. Not sure why, perhaps dict is not a new-style class.
  def __getitem__(self, key):
    return dict.__getitem__(self, self.__keytransform__(key))

  def __setitem__(self, key, value):
    return dict.__setitem__(self, self.__keytransform__(key), value)

  def __delitem__(self, key):
    return dict.__delitem__(self, self.__keytransform__(key))

  def __contains__(self, key):
    return dict.__contains__(self, self.__keytransform__(key))



class lcdict(arbitrary_dict):

  def __keytransform__(self, key):
    return str(key).lower()
+3  A: 

You can write a object that behaves like a dict quite easily with the ABC. It even tells you if you missed a method, so below is the minimal version that shuts the ABC up.

import collections
class TransformedDict(collections.MutableMapping):
    """A dictionary which applies an arbitrary key-altering function before accessing the keys"""

    def __init__(self, *args, **kwargs):
        self.store = dict()
        self.update(dict(*args, **kwargs)) # use the free update to set keys

    def __getitem__(self, key):
        return self.store[self.__keytransform__(key)]

    def __setitem__(self, key, value):
        self.store[self.__keytransform__(key)] = value

    def __delitem__(self, key):
        del self.store[self.__keytransform__(key)]

    def __iter__(self):
        return iter(self.store)

    def __len__(self):
        return len(self.store)

    def __keytransform__(self, key):
        return key

You get a few free methods from the ABC (Abstract Base Classes)

class MyTransformedDict(TransformedDict):
  def __keytransform__(self, key):
    return key.lower()

s = MyTransformedDict( [('Test', 'test')])

assert s.get('TEST') is s['test'] # free get
assert 'TeSt' in s # free __contains__
# free setdefault, __eq__, and so on

import pickle
assert pickle.loads(pickle.dumps( s )) == s #works too since we just use a normal dict

I wouldn't subclass dict (or other builtins) directly. It often makes no sense, because what you actually want to do is implement the interface of a dict. And that is exactly what ABCs are for.

THC4k
What does "ABC" mean?
Paul Biggar
Why not subclass dict? Does this miss any methods? Why use `self.store[key]` rather than `self.store.__getitem__(key)`? Will get() work? Why is `__iter__` needed?
Paul Biggar
Paul Biggar: I edited some answers in. Why not `self.store.__getitem__(key)`? Because it's much more to type. `__iter__` is required because the abc says so (mappings should be iterable)!
THC4k
@THC4k: Thanks for the answers! I guess the reason not to override dict directly is that its somehow weird, and stuff stops working.
Paul Biggar
@THC4k: If you're going to use a `store`, why not just subclass a UserDict?
KennyTM
A: 

So far as I can see, apart from the confusion over super your only problem is that you didn't follow the list of methods that your comment said it follows. Add in the missing override for update and it looks like it works. Without the override to update the keys passed to the initialiser don't get transformed.

class arbitrary_dict(dict):
  """A dictionary which applies an arbitrary key-altering function before accessing the keys"""

  def __keytransform__(self, key):
    return key

  # Overrided methods. List from a http://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict
  def __init__(self, *args, **kwargs):
    self.update(*args, **kwargs)

  def __getitem__(self, key):
    return super(arbitrary_dict, self).__getitem__(self.__keytransform__(key))

  def __setitem__(self, key, value):
    return super(arbitrary_dict, self).__setitem__(self.__keytransform__(key), value)

  def __delitem__(self, key):
    return super(arbitrary_dict, self).__delitem__(self.__keytransform__(key))

  def __contains__(self, key):
    return super(arbitrary_dict, self).__contains__(self.__keytransform__(key))

  def update(self, *args, **kwargs):
      trans = self.__keytransform__
      super(arbitrary_dict, self).update(*[(trans(k), v) for k,v in args], **dict((trans(k), kwargs[k]) for k in kwargs))

class lcdict(arbitrary_dict):

  def __keytransform__(self, key):
    return str(key).lower()

d = lcdict(A=1, B=2)
assert d['A']==1
assert d['b']==2
assert d.get('a', 5)==1
assert d.get('c', 5)==5
d['B'] += 1
assert d['b']==3
Duncan