Welcome, guest | Sign In | My Account | Store | Cart

This recipe creates a metaclass, that can be used in any dict-like object to map specially named attributes to keys of that dictionary.

By default, such attributes are those whose name begin with one (and only one) underscore, and they are mapped to the dictionary key of the same name, without the underscore; but the method to determine that behavior is overridable.

For instance, accessing: d._somekey would return: d.["somekey"]

Creation, update and deletion of such attributes works as expected.

Python, 139 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
## ---[ Exception NotSpecialAttributeName ]----------------------------

class NotSpecialAttributeName(AttributeError):
  """
  NotSpecialAttributeName(AttributeError)

  Internal use only.
  This exception is used to distinguish between 'normal' attributes
  and the ones, identified by having one '_' as their first character,
  that are to be magically mapped to dictionary keys by the
  MetaDictProxy metaclass.
  """
  pass


## ---[ Class MetaDictProxy ]------------------------------------------

class MetaDictProxy(type):
  """
  MetaDictProxy(type)

  Metaclass that makes the items of a dictionary-like class accessible
  as attributes. Set it as a the metaclass for that class, and you can
  then access:
    d["somekey"]
  as:
    d._somekey
  It requires the target class to have the __{set|get|del}item__ methods
  of a dictionary.
  """

  def __init__(cls, name, bases, dict):

    super(MetaDictProxy, cls).__init__(name, bases, dict)
    setattr(cls, '__setattr__', MetaDictProxy.__mysetattr)
    setattr(cls, '__getattr__', MetaDictProxy.__mygetattr)
    setattr(cls, '__delattr__', MetaDictProxy.__mydelattr)


  @staticmethod
  def validatedAttr(attr):
    """
    validatedAttr(attr)

    Static method. Determines whether the parameter begins with one
    underscore '_' but not two. Raises NotSpecialAttributeName otherwise.
    Attributes beginning with one underscore will be looked up in the
    mapped dictionary.
    """

    if len(attr) > 2 \
      and attr.startswith("_") \
      and not attr.startswith("__"):

        return attr[1:]

    raise NotSpecialAttributeName(attr)


  @staticmethod
  def __mygetattr(obj, attr):

    try:
      vattr = MetaDictProxy.validatedAttr(attr)

    except NotSpecialAttributeName:
      ## This is neither an existing native attribute, nor a 'special'
      ## attribute name that should be read off the mapped dictionary,
      ## so we raise an AttributeError.
      raise AttributeError(attr)

    try:
      return obj[vattr]

    except KeyError:
      raise AttributeError(attr)


  @staticmethod
  def __mysetattr(obj, attr, value):

    try:
      attr = MetaDictProxy.validatedAttr(attr)

    except NotSpecialAttributeName:
      ## If this is a 'normal' attribute, treat it the normal way
      ## and then return.
      obj.__dict__[attr] = value
      return

    obj[attr] = value


  @staticmethod
  def __mydelattr(obj, attr):

    try:
      vattr = MetaDictProxy.validatedAttr(attr)

    except NotSpecialAttributeName:
      ## If this is a 'normal' attribute, treat it the normal way
      ## and then return.
      try:
        del obj.__dict__[attr]

      except KeyError:
        raise AttributeError(attr)

      return

    try:
      del obj[vattr]

    except KeyError:
      raise AttributeError(attr)


## ---[ Example ]------------------------------------------------------

if __name__ == '__main__':

  class MyDict(dict):
    ## We're wrapping dict here, but it would work with any dict-like
    ## object.
    __metaclass__ = MetaDictProxy

  d = MyDict()

  d["foo"] = "bar"
  assert d._foo == "bar"
  del d._foo
  assert "foo" not in d.keys()

  ## Non-special attributes aren't affected by this behavior:
  d.baz   = "bux"
  d.__baz = "bux too"
  assert "baz" not in d.keys()
  assert "_baz" not in d.keys()
  assert "__baz" not in d.keys()

While this class was designed to make a set of dict-like configuration classes more convenient to use, it is also primarily an exercise of style. Accessing conf._somekey is a tiny little bit more convenient and readable than conf["somekey"], or at least sufficiently so as to make the exercise of cleanly encapsulating syntactical magic into a metaclass worth attempting.

This recipe can obviously not access keys that are not valid attribute names. For instance, there will be no way to access d["a.b"] as an attribute. Interestingly, the native setattr() function suffers the same shortcoming: setattr(someobject, "a.b", "some value") will work, but there will be no way to access that attribute directly. This is one case where the Python abstraction of storing an object's attributes in its __dict__ dictionary shows through the seams.

Created by Sundance Greydragon on Wed, 17 May 2006 (PSF)
Python recipes (4591)
Sundance Greydragon's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks