Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merging a dict class attribute through the class hierarchy

I'm trying to make the following code work. I'd like to create a class attribute which is a dictionary of default values that automatically updates from children to parents through the class hierarchy. Ideally I'd like to do it with magic in the parent object so that child objects can simply override values as needed. I'd also be open to suggestions for how to redesign this if there's an idiom for this kind of thing.

class A(object):
  DEFAULTS = {'a': 'default a', 'd': 'test d'}

  def __init__(self, *args, **kwargs):
    pass
    # but can i do something with super? this fails but is the
    # approximate idea of what I want...
    # self.DEFAULTS.update(super(self.__class__, self).DEFAULTS)

class B(A):
  DEFAULTS = {'b': 'default b'}

class C(A):
  DEFAULTS = {'a': 'a overridden in C'}

class D(C):
  DEFAULTS = {'d': 'd overridden in D'}

def test():
  a = A()
  b = B()
  c = C()
  d = D()
  print a.DEFAULTS
  print b.DEFAULTS
  print c.DEFAULTS
  print d.DEFAULTS
  assert (a.DEFAULTS == {'a': 'default a', 'd': 'test d'})
  assert (b.DEFAULTS == {'a': 'default a', 'b': 'default b', 'd': 'test d'})
  assert (c.DEFAULTS == {'a': 'overridden in c', 'd': 'test d'})
  assert (d.DEFAULTS == {'a': 'overridden in c', 'd': 'd overridden in D'})

test()

Of course right now this results in the following output:

{'a': 'default a', 'd': 'test d'}
{'b': 'default b'}
{'a': 'a overridden in C'}
{'d': 'd overridden in D'}
Traceback (most recent call last):
  File "experimental/users/edw/python/class_magic.py", line 36, in <module>
    test()
  File "experimental/users/edw/python/class_magic.py", line 32, in test
    assert (b.DEFAULTS == {'a': 'default a', 'b': 'default b', 'd': 'test d'})
AssertionError
like image 243
Ed W Avatar asked Oct 19 '22 04:10

Ed W


1 Answers

Thanks to sobolevn for the metaclass example. I built on that to move the defaults inside the child classes rather than a single external data structure like the example in the original question. Performance might be an issue if the dictionary being copied grows large but since this is code as config that would be unexpected in practice.

class DefaultsMetaBase(type):
  """Automatically merges DEFAULTS from all parent classes."""
  def __call__(self, *args, **kwargs):
    obj = type.__call__(self)
    for klass in obj.__class__.__mro__:
      if klass == obj.__class__ or klass == Base or not issubclass(klass, Base):
        continue
      if hasattr(klass, 'DEFAULTS'):
        d = klass.DEFAULTS.copy()
        d.update(obj.DEFAULTS)
        obj.DEFAULTS = d
    return obj

class Base(object):
  __metaclass__ = DefaultsMetaBase

class A(Base):
  DEFAULTS = {'a': 'default a', 'd': 'test d'}

class B(A):
  DEFAULTS = {'b': 'default b'}

class C(A):
  DEFAULTS = {'a': 'a overridden in C'}
  some_setting = ['a', 'b', 'c', 'd']

class D(C):
  DEFAULTS = {'d': 'd overridden in D'}
  some_setting = ['q', 'r', 's']
  another_setting = 'moar roar'

class F(D):
  another_setting = 'less roar'

def test():
  a = A()
  b = B()
  c = C()
  d = D()
  f = F()
  assert (a.DEFAULTS == {'a': 'default a', 'd': 'test d'})
  assert (b.DEFAULTS == {'a': 'default a', 'b': 'default b', 'd': 'test d'})
  assert (c.DEFAULTS == {'a': 'a overridden in C', 'd': 'test d'})
  assert (d.DEFAULTS == {'a': 'a overridden in C', 'd': 'd overridden in D'})
  assert (f.DEFAULTS == {'a': 'a overridden in C', 'd': 'd overridden in D'})
  print 'pass!'

test()
like image 81
Ed W Avatar answered Oct 22 '22 10:10

Ed W