linus
linus

Reputation: 457

Remove subdict based on condition from nested dictionary

I have a nested dictionary containing (sub)dicts for "density" and "temperature". This is the structure:

profiles_dict =

{
    "density": {
        "elements": [
            {
                "layers": [
                    {
                        "top": 54.0,
                        "bottom": 3.75,
                        "value": 218.9
                    }
                ]
            }
        ],
        "type": "density"
    },
    "temperature": {
        "elements": [
            {
                "layers": []
            }
        ],
        "type": "temperature"
    }
}

As one can see in the temperature subdict, the "layers" key contains an empty list as value. My goal is to create a loop/function that removes the dictionary {"layers": []}.

The result should look like this:

profiles_dict_new =

{
    "density": {
        "elements": [
            {
                "layers": [
                    {
                        "top": 54.0,
                        "bottom": 3.75,
                        "value": 218.9
                    }
                ]
            }
        ],
        "type": "density"
    },
    "temperature": {
        "elements": [
            ],
        "type": "temperature"
    }
}

I have tried the following:

for k1, v1 in profiles_dict.items():
        for k2, v2 in v1.items():
            if type(v2) != list:
                continue
            for i in v2:
                for k3, v3 in i.items():
                    if v3 == []:
                        del(i)

With this loop, I can access {'layers': []}, however I don't know how to properly remove it form the dictionray.

The second approach is based on Ajax1234 solution (Deleting Items based on Keys in nested Dictionaries in python)

profiles_dict_new = {d:{e:[{l:v for l, v in e.items() if v != []}]} for d, e in profiles_dict.items()}
print(profiles_dict_new)

This gives me the following error:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-126f19ab7afd> in <module>
----> 1 profiles_dict_new = {d:{e:[{l:v for l, v in e.items() if v != []}]} for d, e in profiles_dict.items()}
      2 print(profiles_dict_new)

<ipython-input-11-126f19ab7afd> in <dictcomp>(.0)
----> 1 profiles_dict_new = {d:{e:[{l:v for l, v in e.items() if v != []}]} for d, e in profiles_dict.items()}
      2 print(profiles_dict_new)

TypeError: unhashable type: 'dict'

Help is appreciated for either of the presented approaches.

Upvotes: 2

Views: 1047

Answers (3)

Mad Physicist
Mad Physicist

Reputation: 114420

del does not delete objects: it deletes names. If multiple names refer to the same object, the other names persist.

Unbinding i, the loop variable, does nothing to the underlying list, and i just gets reassigned on the next iteration. You have to delete the actual list element v2[index_of_i] to get it removed from the list.

To be able to do that safely, you need to replace the original list or iterate over a copy of the list backwards. You can iterate forwards too, but then the index requires extra care to ensure that it accounts for preceding deleted elements.

Secondly, the loop over i.items() is counter-productive. You don't want to find a dictionary that has some key with an empty list in it. You want a dictionary that has exactly one key, so there's no need for a loop.

for k1, v1 in profiles_dict.items():
    for k2, v2 in v1.items():
        if type(v2) != list:
            continue
        v1[k2] = [i for i in v2 if len(i) > 1 or next(i.values())]

Iterating backwards will allow you to keep the original list object:

for k1, v1 in profiles_dict.items():
    for k2, v2 in v1.items():
        if type(v2) != list:
            continue
        n = len(v2) - 1  # capture before modifying
        for i, e in enumerate(reversed(v2)):
            if len(e) == 1 and not next(iter(e.values()))
                del v2[n - i]

If you iterate forward, you'll have to make a copy of the list to do it properly, and do extra bookkeeping for the index:

for k1, v1 in profiles_dict.items():
    for k2, v2 in v1.items():
        if type(v2) != list:
            continue
        count = 0
        for i, e in enumerate(v2[:]):
            if len(e) == 1 and not next(iter(e.values()))
                del v2[i - count]
                count += 1

The expression next(iter(...)) is a common idiom for getting a single element from something that has one element but can't be easily indexed, like a dict or set. Methods like pop are destructive: if you just wanted to check the element, you have to put it back. next(iter(...)) lets you peek into the collection.

Upvotes: 3

Hemera
Hemera

Reputation: 55

Many text but what you need is deleting a dict from a list?! Your list is not i (element of list) it's v2 so make sure to give your vars good names and change the for to a while cause deleting during iteration can cause problems.

for k1, v1 in profiles_dict.items():
    for k2, v2 in v1.items():
        if type(v2) != list:
            continue
        i = 0
        while i < len(v2):
            for k3, v3 in v2[i].items():
                if v3 == []:
                    v2.pop(i) #remove k3 if v3 empty from list v2 at index i
                    i -= 1 #decrease index i
                    break #cause k3, v3 in v2[i].items() does not exist anymore
            i += 1

If an item dict in list v2 contains more than one k3 which can be NOT empty and you do NOT want to REMOVE it TOO you can store the empty ones for later in a new list remove_keys. If remove_keys contains the same amout as the whole dict you can .pop(index) it from v2 else .pop(key) all stored keys only from the current dict v2[i].

for k1, v1 in profiles_dict.items():
    for k2, v2 in v1.items():
        if type(v2) != list:
            continue
        i = 0
        while i < len(v2):
            remove_keys = [] #store k3 when v3 == []
            for k3, v3 in v2[i].items():
                if v3 == []:
                    remove_keys.append(k3)
            if len(remove_keys) == len(v2[i]): #if this would remove all keys
                v2.pop(i) #delete whole "empty" dict again...
                i -= 1
            else:
                for k3 in remove_keys: #remove stored keys from dict in v2 at i
                    v2[i].pop(k3)
            i += 1

Upvotes: 1

Amit Nanaware
Amit Nanaware

Reputation: 3346

instead of deleting assign empty list

you can try below code:

for k1, v1 in profiles_dict.items():
        for k2, v2 in v1.items():
            if type(v2) != list:
                continue
            for i in v2:
                for k3, v3 in i.items():
                    if v3 == []:
                        v1[k2] = []

Upvotes: 1

Related Questions