32

I'd like to be able to do something like this:

from dotDict import dotdictify

life = {'bigBang':
           {'stars':
               {'planets': []}
           }
       }

dotdictify(life)

# This would be the regular way:
life['bigBang']['stars']['planets'] = {'earth': {'singleCellLife': {}}}
# But how can we make this work?
life.bigBang.stars.planets.earth = {'singleCellLife': {}}

#Also creating new child objects if none exist, using the following syntax:
life.bigBang.stars.planets.earth.multiCellLife = {'reptiles':{},'mammals':{}}

My motivations are to improve the succinctness of the code, and if possible use similar syntax to Javascript for accessing JSON objects for efficient cross platform development. (I also use Py2JS and similar.)

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Luke Stanley
  • 1,274
  • 1
  • 16
  • 32
  • `life.bigBang.stars.planets` is defined a `list`, so assigning to its `.earth` attribute will result in an `AttributeError: 'list' object has no attribute 'earth'`. I'm guessing you probably want/meant `{'planets': {}}` in the definition of the `life` dictionary (as is shown in the accepted answer). – martineau Oct 17 '17 at 19:29
  • **See also:** https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary – dreftymac Jun 03 '22 at 20:25

5 Answers5

55

Here's one way to create that kind of experience:

class DotDictify(dict):
    MARKER = object()

    def __init__(self, value=None):
        if value is None:
            pass
        elif isinstance(value, dict):
            for key in value:
                self.__setitem__(key, value[key])
        else:
            raise TypeError('expected dict')

    def __setitem__(self, key, value):
        if isinstance(value, dict) and not isinstance(value, DotDictify):
            value = DotDictify(value)
        super(DotDictify, self).__setitem__(key, value)

    def __getitem__(self, key):
        found = self.get(key, DotDictify.MARKER)
        if found is DotDictify.MARKER:
            found = DotDictify()
            super(DotDictify, self).__setitem__(key, found)
        return found

    __setattr__, __getattr__ = __setitem__, __getitem__


if __name__ == '__main__':

    life = {'bigBang':
               {'stars':
                   {'planets': {}  # Value changed from []
                   }
               }
           }

    life = DotDictify(life)
    print(life.bigBang.stars.planets)  # -> []
    life.bigBang.stars.planets.earth = {'singleCellLife' : {}}
    print(life.bigBang.stars.planets)  # -> {'earth': {'singleCellLife': {}}}
martineau
  • 119,623
  • 25
  • 170
  • 301
Curt Hagenlocher
  • 20,680
  • 8
  • 60
  • 50
  • 5
    Calling dict.__setitem__(self, key, value) is the trick to the recursion I was missing! Quite elegant code there sir :) – Luke Stanley Jun 13 '10 at 06:39
  • 1
    You (and @Luke) should use `super(dotdictify, self).__setitem__(key, value)` instead of `dict.__setitem__(self, key, value)` (otherwise subclasses of yours with multiple ancestors will run into trouble). And (except for builtins) classes should start with a capital letter – Tobias Kienzler Jun 20 '14 at 09:44
  • @LukeStanley there is an error in the script with python 3. Lines containing `super(dotdictify, self).__setitem__(key, value)` and `super(dotdictify, self).__setitem__(key, found)` do require only 2 arguments in `__setitem__`, so `self` is not required as first argument – aturegano Aug 02 '17 at 08:00
  • 3
    @aturegano feel free to suggest a working update. These days I would probably just pip install attrdict https://pypi.python.org/pypi/attrdict – Luke Stanley Aug 02 '17 at 12:38
  • @LukeStanley attrdict looks like the package I was looking for. Thanks! – aturegano Aug 02 '17 at 14:03
  • Just a small change in @Curt Hagenlocher answer: if you want to raise a KeyError in case the key does not exist, you can just override `__getattr__(self, key)` with `return self[key]`. Because the way it is it returns an empty dict which can lead to weird bugs hard to debug. In this approach you don't need to `__getitem__` and the `MARKER`. – caiolopes Nov 16 '17 at 18:13
  • Is there a solution for dicts containing lists containing dicts etc. ....? like `cfg.vservers[0].vname` – Marki Dec 25 '20 at 02:18
7

Below another implementation of a nested attribute dictionary (inspired by the answer of Curt Hagenlocher, stripped down to the essential):

class AttrDict(dict):
    """ Nested Attribute Dictionary

    A class to convert a nested Dictionary into an object with key-values
    accessible using attribute notation (AttrDict.attribute) in addition to
    key notation (Dict["key"]). This class recursively sets Dicts to objects,
    allowing you to recurse into nested dicts (like: AttrDict.attr.attr)
    """

    def __init__(self, mapping=None):
        super(AttrDict, self).__init__()
        if mapping is not None:
            for key, value in mapping.items():
                self.__setitem__(key, value)

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            value = AttrDict(value)
        super(AttrDict, self).__setitem__(key, value)
        self.__dict__[key] = value  # for code completion in editors

    def __getattr__(self, item):
        try:
            return self.__getitem__(item)
        except KeyError:
            raise AttributeError(item)

    __setattr__ = __setitem__

This works in both Python 2 and 3:

life = AttrDict({'bigBang': {'stars': {'planets': {}}}})
life['bigBang']['stars']['planets'] = {'earth': {'singleCellLife': {}}}
life.bigBang.stars.planets.earth.multiCellLife = {'reptiles': {}, 'mammals': {}}
print(life.bigBang.stars.planets.earth)
# -> {'singleCellLife': {}, 'multiCellLife': {'mammals': {}, 'reptiles': {}}}

Converting KeyError into AttributeError in __getattr__ is required in Python3 such that hasattr works also in case the attribute is not found:

hasattr(life, 'parallelUniverse')
# --> False
Stabledog
  • 3,110
  • 2
  • 32
  • 43
kadee
  • 8,067
  • 1
  • 39
  • 31
4

There is a package doing exactly what you want and also something more and it is called Prodict.

from prodict import Prodict

life_dict = {'bigBang':
                {'stars':
                    {'planets': []}
                }
            }

life = Prodict.from_dict(life_dict)

print(life.bigBang.stars.planets)
# prints []

# you can even add new properties dynamically
life.bigBang.galaxies = []

PS: I'm the author of the Prodict.

ramazan polat
  • 7,111
  • 1
  • 48
  • 76
1

Here is another solution:

from typing import Dict, Any

class PropertyTree: pass

def dict_to_prop_tree(yaml_config: Dict[str, Any]) -> PropertyTree:
    tree = PropertyTree()
    for key, value in yaml_config.items():
        if type(value) == dict:
            setattr(tree, key, dict_to_obj_tree(value))
        elif type(value) == list:
            setattr(tree, key, [dict_to_obj_tree(v) for v in value])
        else:
            setattr(tree, key, value)
    return tree

Then in the python console:

d={'a': 1, 'b': 2, 'c': {'d': 4, 'e': 5, 'f': {'g': 6}, 'h': {}, 'j': 7}}
tree=dict_to_prop_tree(d)
tree.a
tree.c.f.g

prints the correct values

Oliver
  • 27,510
  • 9
  • 72
  • 103
0
#!/usr/bin/env python3
# _*_ coding: utf-8 _*_

# Author: Xingbang Jiang
# E-mail: 1278561590@qq.com
# HomePage: http://www.xingbangsharing.tech

class Dotsdict(dict):
    def __init__(self, args, **kwargs):
        super(Dotsdict, self).__init__(args, **kwargs)
        for obj in [args, kwargs]:
            for k, v in obj.items():
                if isinstance(v, dict):
                    v = Dotsdict(v)
                self.__setitem__(k, v)

    def __setitem__(self, key, val):
        super(Dotsdict, self).__setitem__(key, val)
        # self.__dict__[key] = val

    def __delitem__(self, key):
        super(Dotsdict, self).__delitem__(key)
        # del self.__dict__[key]

    def __getitem__(self, key):
        return super(Dotsdict, self).__getitem__(key)

    def __missing__(self, key):
        dots = Dotsdict()
        self.__setitem__(key, dots)
        return dots

    __setattr__, __delattr__, __getattr__ = __setitem__, __delitem__, __getitem__

# ===================================================================


d = {'k': 'v', 'x': {'y': 'z', 'p': 'q', }, }
print(type(d))
print(d)

dd = Dotsdict(d, i='j')
print(type(dd))
print(dd)

print('========================================')

dd.a = 'b'
dd.x.m = 'n'
print(dd.x.y)

del dd.x['p']
print(dd)
print(len(dd))
蒋兴邦
  • 1
  • 1