33

Supposing we have this dict:

d = {'a':1, 'b': {'c':{}}}

What would be the most straightforward way of knowing the nesting depth of it?

tutuca
  • 3,444
  • 6
  • 32
  • 54
  • 1
    Might it branch, or only one key per layer? – mhlester May 06 '14 at 15:36
  • 3
    is it only dicts nested that you're worried about, or could the dict have values which are other containers as well? – mgilson May 06 '14 at 15:38
  • 3
    I'll be the idiot to say that the (most straightforward) answer for the example you gave would be to look at it. Also, I can't believe this isn't a duplicate (but it seems to check out!) – keyser May 06 '14 at 15:42
  • 1
    possible duplicate of [Recursive depth of python dictionary](http://stackoverflow.com/questions/9538875/recursive-depth-of-python-dictionary) – thefourtheye May 07 '14 at 09:18

4 Answers4

38

You need to create a recursive function:

>>> def depth(d):
...     if isinstance(d, dict):
...         return 1 + (max(map(depth, d.values())) if d else 0)
...     return 0
...
>>> d = {'a':1, 'b': {'c':{}}}
>>> depth(d)
3
falsetru
  • 357,413
  • 63
  • 732
  • 636
34

You'll have to traverse the dictionary. You could do so with a queue; the following should be safe from circular references:

from collections import deque

def depth(d):
    queue = deque([(id(d), d, 1)])
    memo = set()
    while queue:
        id_, o, level = queue.popleft()
        if id_ in memo:
            continue
        memo.add(id_)
        if isinstance(o, dict):
            queue += ((id(v), v, level + 1) for v in o.values())
    return level

Note that because we visit all dictionary values in breath-first order, the level value only ever goes up. The memo set is used to ensure we don't try to traverse a circular reference, endlessly.

Or you could traverse the tree with recursion (which effectively uses function calls as a stack). I've used functools.singledispatch() for easy expansion to other container types:

from functools import singledispatch, wraps

@singledispatch
def depth(_, _level=1, _memo=None):
    return _level

def _protect(f):
    """Protect against circular references"""
    @wraps(f)
    def wrapper(o, _level=1, _memo=None, **kwargs):
        _memo, id_ = _memo or set(), id(o)
        if id_ in _memo: return _level
        _memo.add(id_)
        return f(o, _level=_level, _memo=_memo, **kwargs)
    return wrapper

def _protected_register(cls, func=None, _orig=depth.register):
    """Include the _protect decorator when registering"""
    if func is None and isinstance(cls, type):
        return lambda f: _orig(cls, _protect(f))
    return _orig(cls, _protect(func)) if func is not None else _orig(_protect(cls))
depth.register = _protected_register

@depth.register
def _dict_depth(d: dict, _level=1, **kw):
    return max(depth(v, _level=_level + 1, **kw) for v in d.values())

This is as depth-first search, so now max() is needed to pick the greatest depth for the current object under scrutiny at each level. A dictionary with 3 keys of each different depths should reflect the greatest depth at that level.

The memo set used in either version tracks object ids, so we don't run is circles if you did something like foo = {}; foo["bar"] = foo.

Demo:

>>> d = {'a':1, 'b': {'c':{}}}
>>> depth(d)
3
>>> d = {'foo': {'bar': {'baz': 0}, 'spam': {'ham': {'monty': 1}, 'eric': 'idle'}}, 'john': 'cleese'}
>>> depth(d)
5
>>> circular = {}
>>> circular["self"] = circular
>>> depth(circular)
2

The recursive singledispatch version can be expanded to cover more containers, such as lists:

@depth.register
def _list_depth(l: list, _level=1, **kw):
    return max(depth(v, _level=_level + 1, **kw) for v in l)

Because I've augmented the standard .register decorator to handle circular-reference testing, implementing additional container support is relatively trivial. Just remember to pass along any extra keyword arguments to the recursive call!

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • +1 the one caveat: should None and {} return 1? But that's a matter of convention. – Lukasz Madon May 06 '14 at 15:48
  • 1
    @lukas: The technique can be tweaked; the point was more to show what needs to be done. The key points here are traversal (and using `max() when going depth-first), I'd say. – Martijn Pieters May 06 '14 at 15:49
  • The default value of level should be 0 and not 1. A simple dict is returning 2 as depth which is not correct. Also for None and empty dicts, the depth should be 0 and not 1. – Samy Arous May 06 '14 at 16:40
  • @SamyArous: That's all interpretation; what if the top-level dictionary is empty? Is that a depth of 0 or 1? The fact that the dictionary has got values could be seen as another level. Empty dictionary -> 1, dictionary with values -> 2 (the dictionary itself requires one level of referencing, the values another). – Martijn Pieters May 06 '14 at 16:42
  • @SamyArous: You can get *your* interpretation by adjusting the start value of `level` to 0 and returning `level` for non-dictionary values, and `level + 1` for empty dictionaries. But note that the *OP did not specify how depth should be calculated*, so we are all free to bring our own interpretation. – Martijn Pieters May 06 '14 at 16:43
  • ```{ "name": "positive", "children": [{ "name": "product service", "children": [{ "name": "price", "children": [{ "name": "cost", "size": 8 }] }, { "name": "quality", "children": [{ "name": "messaging", "size": 4 }] }] }, { "name": "customer service", "children": [{ "name": "Personnel", "children": [{ "name": "CEO", "size": 7 }] }] }, { "name": "product", "children": [{ "name": "Apple", "children": [{ "name": "iPhone 4", "size": 10 }] }] }] }``` check depth of this? dictionary doesn't mean it can't have nested list. – jayprakashstar Jul 05 '19 at 09:54
  • @jayprakashstar: Indeed, dictionary values can be *any* container type, and not just lists either. It's too broad to start covering every possibility, you'll have to extend the code to account for other containers. I've updated my answer to use `singledispatch` to make it easier to add more types as you need them, and included an implementation for lists. – Martijn Pieters Jul 05 '19 at 12:07
5

A non-recursive solution:

def depth(d):

    depth=0
    q = [(i, depth+1) for i in d.values() if isinstance(i, dict)]
    max_depth = 0
    while (q):
        n, depth = q.pop()
        max_depth = max(max_depth, depth)
        q = q + [(i, depth+1) for i in n.values() if isinstance(i, dict)]

    print max_depth
grdvnl
  • 636
  • 6
  • 9
3

Iterative solution:

from collections import deque


def depth(d):
    q = deque([d])
    q2 = deque()
    max_depth = 0
    while q:
        curr_dict = q.popleft()
        if isinstance(curr_dict, dict):
            for di in curr_dict.itervalues():
                q2.append(di)
        if not q:
            q, q2 = q2, q
            max_depth += 1
    return max_depth

print depth(None)
print depth({})
print depth({"a": "b"})
print depth({"a": "b", "c": {"d": "e"}, "f": {"g": "h"}, "i": {"j": "k"}, "x": {}, "z": {} })
print depth({'a':1, 'b': {'c':{}}})
print depth({'foo': {'bar': {'baz': 0}, 'spam': {'ham': {'monty': 1}, 'eric': 'idle'}}, 'john': 'cleese'})
Lukasz Madon
  • 14,664
  • 14
  • 64
  • 108
  • Why are you using *two* queues here? The very nature of appending to the right and popping from the left means a single queue would behave exactly the same. – Martijn Pieters Jul 05 '19 at 12:29
  • @MartijnPieters for calculating the max, the second queue is not needed. It would be useful if you want to print the each level with along with its max depth. – Lukasz Madon Jul 05 '19 at 14:05
  • You don’t need to use two queues for that either, though. Just include a depth in the queue, and print new values as you process the queue (e.g. when the next level is higher than the previous). – Martijn Pieters Jul 05 '19 at 14:43