1

There must be a more graceful way of doing this but I cannot figure out how to create a single function for reading/writing values to different levels of a dict, this is the 'best' that I could come up with:

table = {
    'A': {
        'B': '2',
        'C': {
            'D':'3'
        }
    }
}
first = 'A'
second1 = 'B'
second2 = 'C'
third = 'D'

def oneLevelDict(first):
    x = table[first]
    print(x)

def twoLevelDict(first, second):
    x = table[first][second]
    print(x)

def threeLevelDict(first, second, third):
    x = table[first][second][third]
    print(x)

oneLevelDict(first)
twoLevelDict(first, second1)
threeLevelDict(first, second2, third)
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
MattYYC
  • 13
  • 2
  • Not an exact dupe, but related to an extent: https://stackoverflow.com/questions/1373164/how-do-i-create-a-variable-number-of-variables – TrebledJ Jan 10 '19 at 05:35
  • @TrebuchetMS, both questions are about dictionaries, but this question is specifically about how to get access to a value at an arbitrary level of a dictionary. – Grismar Jan 10 '19 at 05:45

3 Answers3

2

You can use *args to pass an arbitrary number of arguments to a function. You can then use a loop to traverse the levels.

get_any_level(*keys):
    d = table
    for key in keys:
        d = d[key]
    return d

Now you have one function that can replace the three you had before:

print(get_any_level(first))
print(get_any_level(first, second1))
print(get_any_level(first, second2, third))

You can use this function to write to an arbitrary level as well:

get_any_level(first)[second1] = 17

A better way might be to have a separate function to write though:

def put_any_level(value, *keys):
    get_any_level(*keys[:-1])[keys[-1]] = value

put_any_level(17, first, second1)

value has to come first in the argument list unless you want it to be keyword-only because *keys will consume all positional arguments. This is not necessarily a bad alternative:

def put_any_level(*keys, value):
    get_any_level(*keys[:-1])[keys[-1]] = value

The keyword argument adds clarity:

put_any_level(first, second1, value=17)

But it will also lead to an error if you attempt to pass it as a positional argument, e.g. put_any_level(first, second1, 17).

Couple of minor points:

  1. It's conventional to use CamelCase only for class names. Variables and functions are conventionally written in lowercase_with_underscores.
  2. A function should generally do one thing, and do it well. In this case, I've split the task of finding the nested value from the task of displaying it by giving the function a return value.
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
0

This can be achieved using *args. Read more about it here

And this is how to do it:

def allLevelDict(*argv):
  if len(argv) == 1:
    x  = table[argv[0]]
    print (x)
  elif len(argv) == 2:
    x  = table[argv[0]][argv[1]]
    print (x)
  elif len(argv) == 3:
    x  = table[argv[0]][argv[1]][argv[2]]
    print (x)
allLevelDict(first)
allLevelDict(first, second1)
allLevelDict(first, second2, third)
Aditya
  • 1,357
  • 1
  • 9
  • 19
0

Similar to the other suggestions, but perhaps even more graceful, if you like recursion:

table = {'A':{'B':'2','C':{'D':'3'}}}
first = 'A'
second1 = 'B'
second2 = 'C'
third = 'D'


def get_from(x, *keys):
    return get_from(x[keys[0]], *keys[1:]) if len(keys) > 0 else x


print(get_from(table, first))
print(get_from(table, first, second1))
print(get_from(table, first, second2, third))

Note: I'm also passing in the table, since I imagine you'd want to be able to use it on other dictionaries also.

Or, if you think shorter isn't always better:

def get_from(x, *keys):
    if len(keys) > 0 
        return get_from(x[keys[0]], *keys[1:])
    else:
        return x

Normally, recursion can be dangerous, as it's expensive - but since you're unlikely to have incredibly deep dictionaries, I feel it is the right solution here.

Grismar
  • 27,561
  • 4
  • 31
  • 54
  • Thanks @Grismar - very succinct. Is the [1:] equivalent to a popitems()? Not sure what this means. – MattYYC Jan 11 '19 at 00:08
  • `[1:]` means 'all elements from the sequence, starting with the element at index 1'. So, for `x = [1,2,3,4]`, `x[1:]` would be `[2,3,4]` - read more here on this, called 'slicing' https://www.pythoncentral.io/how-to-slice-listsarrays-and-tuples-in-python/ It's not equivalent to `popitem()`, not just because it returns a larger part of the sequence, but also more importantly because it does not modify the original - it returns a copy of its contents. – Grismar Jan 11 '19 at 01:38