web-dev-qa-db-ja.com

平坦化された辞書からネストされた辞書を作成する

入れ子にしたい、次の形式の辞書を作成します。

flat = {'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}

フォームに変換したい

nested = {'X': {'a': {'one': 10,
                      'two': 20}, 
                'b': {'one': 10,
                      'two': 20}}, 
          'Y': {'a': {'one': 10,
                      'two': 20},
                'b': {'one': 10,
                      'two': 20}}}

フラットディクショナリの構造は、あいまいさに関する問題がないようになっています。任意の深さの辞書で動作するようにしたいのですが、パフォーマンスは本当に問題ではありません。ネストされた辞書をフラット化するための多くの方法を見てきましたが、基本的にフラット化された辞書をネストする方法はありません。ディクショナリに格納される値は、スカラーまたは文字列のいずれかであり、反復可能ではありません。

これまでのところ、入力を受け取ることができるものを持っています

test_dict = {'X_a_one': '10',
             'X_b_one': '10',
             'X_c_one': '10'}

出力へ

test_out = {'X': {'a_one': '10', 
                  'b_one': '10', 
                  'c_one': '10'}}

コードを使用して

def nest_once(inp_dict):
    out = {}
    if isinstance(inp_dict, dict):
        for key, val in inp_dict.items():
            if '_' in key:
                head, tail = key.split('_', 1)

                if head not in out.keys():
                    out[head] = {tail: val}
                else:
                    out[head].update({tail: val})
            else:
                out[key] = val
    return out

test_out = nest_once(test_dict)

しかし、これを辞書のすべてのレベルを再帰的に作成するものにする方法を見つけるのに苦労しています。

助けていただければ幸いです!

(これを行う理由については、ネストされたdictと同等の構造を持つファイルがあり、このファイルの内容をNetCDFファイルの属性ディクショナリに保存し、後で取得する必要があります。ただし、NetCDFでは、属性としてフラットな辞書を配置するため、以前にNetCDFファイルに保存した辞書を平坦化したい。)

50
ThomasNicholas

私の見解は次のとおりです。

def nest_dict(flat):
    result = {}
    for k, v in flat.items():
        _nest_dict_rec(k, v, result)
    return result

def _nest_dict_rec(k, v, out):
    k, *rest = k.split('_', 1)
    if rest:
        _nest_dict_rec(rest[0], v, out.setdefault(k, {}))
    else:
        out[k] = v

flat = {'X_a_one': 10,
        'X_a_two': 20, 
        'X_b_one': 10,
        'X_b_two': 20, 
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}
nested = {'X': {'a': {'one': 10,
                      'two': 20}, 
                'b': {'one': 10,
                      'two': 20}}, 
          'Y': {'a': {'one': 10,
                      'two': 20},
                'b': {'one': 10,
                      'two': 20}}}
print(nest_dict(flat) == nested)
# True
25
jdehesa
output = {}

for k, v in source.items():
    # always start at the root.
    current = output

    # This is the part you're struggling with.
    pieces = k.split('_')

    # iterate from the beginning until the second to last place
    for piece in pieces[:-1]:
       if not piece in current:
          # if a dict doesn't exist at an index, then create one
          current[piece] = {}

       # as you walk into the structure, update your current location
       current = current[piece]

    # The reason you're using the second to last is because the last place
    # represents the place you're actually storing the item
    current[pieces[-1]] = v
24
cwallenpoole

collections.defaultdictを使用する1つの方法は、 この前の回答 から大きく借用しています。 3つのステップがあります。

  1. defaultdictオブジェクトのネストされたdefaultdictを作成します。
  2. flat入力辞書のアイテムを繰り返します。
  3. defaultdictを使用して結果ディクショナリを反復処理し、_でキーを分割して得られた構造に従ってgetFromDictの結果を作成します。

これは完全な例です:

from collections import defaultdict
from functools import reduce
from operator import getitem

def getFromDict(dataDict, mapList):
    """Iterate nested dictionary"""
    return reduce(getitem, mapList, dataDict)

# instantiate nested defaultdict of defaultdicts
tree = lambda: defaultdict(tree)
d = tree()

# iterate input dictionary
for k, v in flat.items():
    *keys, final_key = k.split('_')
    getFromDict(d, keys)[final_key] = v

{'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}},
 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

最後のステップとして、defaultdictを通常のdictに変換できますが、通常このステップは必要ありません。

def default_to_regular_dict(d):
    """Convert nested defaultdict to regular dict of dicts."""
    if isinstance(d, defaultdict):
        d = {k: default_to_regular_dict(v) for k, v in d.items()}
    return d

# convert back to regular dict
res = default_to_regular_dict(d)
13
jpp

他の答えはきれいですが、あなたが再帰について述べたので、他の選択肢があります。

def nest(d):
    _ = {}
    for k in d:
        i = k.find('_')
        if i == -1:
            _[k] = d[k]
            continue
        s, t = k[:i], k[i+1:]
        if s in _:
            _[s][t] = d[k]
        else:
            _[s] = {t:d[k]}
    return {k:(nest(_[k]) if type(_[k])==type(d) else _[k]) for k in _}
4
Hans Musgrave

インポートのない別の非再帰的ソリューション。フラットdictの各キーと値のペアを挿入することと、フラットdictのキーと値のペアをマッピングすることの間のロジックを分割します。

def insert(dct, lst):
    """
    dct: a dict to be modified inplace.
    lst: list of elements representing a hierarchy of keys
    followed by a value.

    dct = {}
    lst = [1, 2, 3]

    resulting value of dct: {1: {2: 3}}
    """
    for x in lst[:-2]:
        dct[x] = dct = dct.get(x, dict())

    dct.update({lst[-2]: lst[-1]})


def unflat(dct):
    # empty dict to store the result
    result = dict()

    # create an iterator of lists representing hierarchical indices followed by the value
    lsts = ([*k.split("_"), v] for k, v in dct.items())

    # insert each list into the result
    for lst in lsts:
        insert(result, lst)

    return result


result = unflat(flat)
# {'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}},
# 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

itertools.groupbyを使用できます:

import itertools, json
flat = {'Y_a_two': 20, 'Y_a_one': 10, 'X_b_two': 20, 'X_b_one': 10, 'X_a_one': 10, 'X_a_two': 20, 'Y_b_two': 20, 'Y_b_one': 10}
_flat = [[*a.split('_'), b] for a, b in flat.items()]
def create_dict(d): 
  _d = {a:list(b) for a, b in itertools.groupby(sorted(d, key=lambda x:x[0]), key=lambda x:x[0])}
  return {a:create_dict([i[1:] for i in b]) if len(b) > 1 else b[0][-1] for a, b in _d.items()}

print(json.dumps(create_dict(_flat), indent=3))

出力:

{
 "Y": {
    "b": {
      "two": 20,
      "one": 10
    },
    "a": {
      "two": 20,
      "one": 10
    }
 },
  "X": {
     "b": {
     "two": 20,
     "one": 10
   },
    "a": {
     "two": 20,
     "one": 10
   }
 }
}
4
Ajax1234

合理的に読み取り可能な再帰的な結果を次に示します。

def unflatten_dict(a, result=None, sep='_'):

    if result is None:
        result = dict()

    for k, v in a.items():
        k, *rest = k.split(sep, 1)
        if rest:
            unflatten_dict({rest[0]: v}, result.setdefault(k, {}), sep=sep)
        else:
            result[k] = v

    return result


flat = {'X_a_one': 10,
        'X_a_two': 20,
        'X_b_one': 10,
        'X_b_two': 20,
        'Y_a_one': 10,
        'Y_a_two': 20,
        'Y_b_one': 10,
        'Y_b_two': 20}

print(unflatten_dict(flat))
{'X': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}, 
 'Y': {'a': {'one': 10, 'two': 20}, 'b': {'one': 10, 'two': 20}}}

これは上記のいくつかの回答に基づいており、インポートを使用せず、python 3でのみテストされています。

1