11

Brand new to python, Let's say I have a dict:

kidshair = {'allkids':{'child1':{'hair':'blonde'},
                      'child2':{'hair':'black'},
                      'child3':{'hair':'red'},
                      'child4':{'hair':'brown'}}}

If child3 changes their hair colour regularly, I might want to write an application to speed up the data maintenance. In this example i'd use:

kidshair['allkids']['child3']['hair'] = ...

Is there any way to store this path as a variable so I can access it at my leasure? Obviously

mypath = kidshair['allkids']['child3']['hair']

results in mypath = 'red'. Is there any possible way to hard code the path itself so I could use:

mypath = 'blue' 

to reperesent

kidshair['allkids']['child3']['hair'] = 'blue'

Kind thanks, ATfPT

Dom Vinyard
  • 2,038
  • 1
  • 23
  • 36

5 Answers5

11

Depending on what you need, the easiest option may be to use tuples as dictionary keys instead of nested dictionaries:

kidshair['allkids', 'child3', 'hair']
mypath = ('allkids', 'child3', 'hair')
kidshair[mypath]

The only issue with this is that you can't get a portion of the dictionary, so, for example, you can't (easily/efficiently) access everything to do with 'child3'. This may or may not be an appropriate solution for you depending on your usage.

An alternative with your current structure is to do something like this:

>>> from functools import partial
>>> test = {"a": {"b": {"c": 1}}}
>>> def itemsetter(item):
...     def f(name, value):
...         item[name] = value
...     return f
...
>>> mypath = partial(itemsetter(test["a"]["b"]), "c")
>>> mypath(2)
>>> test
{'a': {'b': {'c': 2}}}

Here we make a function itemsetter(), which (in the vein of operator.itemgetter()) gives us a function that that sets the relevant key in the given dictionary. We then use functools.partial to generate a version of this function with the key we want pre-filled. It's not quite mypath = blue either, but it's not bad.

If you don't want to bother with making something consistent to the operator module, you could simply do:

def dictsetter(item, name):
     def f(value):
         item[name] = value
     return f

mypath = dictsetter(test["a"]["b"], "c")

mypath(2)
Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
10

You could create a set of functions that accesses a 'path' for a given dictionary:

def pathGet(dictionary, path):
    for item in path.split("/"):
        dictionary = dictionary[item]
    return dictionary

def pathSet(dictionary, path, setItem):
    path = path.split("/")
    key = path[-1]
    dictionary = pathGet(dictionary, "/".join(path[:-1]))
    dictionary[key] = setItem

Usage:

>>> pathGet(kidshair, "allkids/child1/hair")
'blonde'
>>> pathSet(kidshair, "allkids/child1/hair", "blue")
>>> kidshair['allkids']['child1']
{'hair': 'blue'}
Joel Cornett
  • 24,192
  • 9
  • 66
  • 88
  • In retrospect, it's probably easier to pass tuples to 'path' instead of joining and splitting strings. – Joel Cornett Apr 29 '12 at 12:04
  • I ended up creating similar code before finding this question. However, as you suggested I created functions that accepted tuples, along with two helper functions that accepted strings and called the path functions with call to split() as the tuple argument. – Matt Jan 21 '13 at 22:25
2

You can save a reference to kidshair['allkids']['child3'] easily:

thiskid = kidshair['allkids']['child3']
thiskid['hair'] = 'blue'

You can't save the entire path, because the assignment changes the hair key in the kidshair['allkids']['child3'] dictionary.
If you want to change a dictionary, you must have a reference to it, not to something it contains.

ugoren
  • 16,023
  • 3
  • 35
  • 65
2

try to use dpath lib https://pypi.org/project/dpath/

>>> help(dpath.util.new)
Help on function new in module dpath.util:

new(obj, path, value)
Set the element at the terminus of path to value, and create
it if it does not exist (as opposed to 'set' that can only
change existing keys).

path will NOT be treated like a glob. If it has globbing
characters in it, they will become part of the resulting
keys

>>> dpath.util.new(x, 'a/b/e/f/g', "Roffle")
>>> print json.dumps(x, indent=4, sort_keys=True)
{
    "a": {
        "b": {
            "3": 2,
            "43": 30,
            "c": "Waffles",
            "d": "Waffles",
            "e": {
                "f": {
                    "g": "Roffle"
                }
            }
        }
    }
}
1

The closest you can do with your current data structure is to get the last dict and use it directly:

child = kidshair['allkids']['child3']

child['hair'] = 'blue'

This is because in Python, assignment of the style

name = value

simply binds a new object to the "variable" name. There is no way to overload this and the binding can't affect any other object (apart from freeing the object that was bound to this name until now).

On the other hand this:

name[key] = value

is entirely different because it performs an action (sets an item) on the object currently in name (dict in your case).

yak
  • 8,851
  • 2
  • 29
  • 23