通过类层次结构合并dict类属性

时间:2015-08-22 18:50:00

标签: python class python-2.7 inheritance dictionary

我正在尝试使以下代码正常工作。我想创建一个class属性,它是一个默认值的字典,通过类层次结构自动从子级更新为父级。理想情况下,我想在父对象中使用魔术来执行此操作,以便子对象可以根据需要简单地覆盖值。如果对这类事情有成语,我也会接受如何重新设计的建议。

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

当然,现在这导致以下输出:

{'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

2 个答案:

答案 0 :(得分:1)

感谢sobolevn的元类示例。我建立在它上面来移动子类中的默认值而不是像原始问题中的示例那样的单个外部数据结构。如果被复制的字典变大,性能可能会成为一个问题,但由于这是代码作为配置,在实践中会出乎意料。

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

答案 1 :(得分:0)

我听起来你想要类似于通常使用类属性发生的事情 - 孩子继承父属性,并且如果需要可以隐藏具有自己值的父属性。

例如

class A(object):
    a = 'a'

class B(A):
    b = 'b'

class C(A):
    a = 'c'

assert A.a == 'a'and A().a == 'a'
assert B.a == 'a' # B inherits a
assert C.a == 'c' # C overrides a

它还允许实例覆盖默认值

a = A()
a.a = 'not a'
assert A.a == 'a' and A().a == 'a'
assert a.a == 'not a'

但是,您可能不希望像这样访问您的默认值。别担心。在父项上定义默认类,并让子项定义子默认值。例如

class A(object):
    class defaults(object):
        a = 'a'

class B(A):
    class defaults(A.defaults):
        b = 'b'

class C(A):
    class defaults(A.defaults):
        a = 'c'

assert A.defaults.a == 'a' and A().defaults.a == 'a'
assert B.defaults.a == 'a' # B inherits a
assert C.defaults.a == 'c' # C overrides a

或者,你真的,真的,希望默认值是一个类似dict的对象,然后只需定义一个类似于dict的对象,它会覆盖__getitem__以及你可能关心的其他各种方法。

def defaults(**kwargs):
    """decorator to add defaults to a class"""
    def default_setter(cls):
        if not hasattr(cls, "defaults"):
            cls.defaults = DefaultGetter()
        for key, value in kwargs:
            setattr(cls, "_default_"+key, value)
        return cls
    return default_setter

class DefaultGetter(object):
    """dict-like object that retrieves the defaults"""
    def __getitem__(self, name):
        try:
            return getattr(self.owner, "_default_" + name)
        except AttributeError:
            raise KeyError(name) from None

@defaults(a='a')
class A(object):
     pass

assert A.defaults['a'] == 'a'
assert A._default_a == 'a' # actual storage location of a
A.defaults['b'] # raises KeyError