IT story

중첩 된 사전을 병합하여 키 압축

hot-time 2020. 6. 22. 07:37
반응형

중첩 된 사전을 병합하여 키 압축


다음과 같은 사전이 있다고 가정하십시오.

{'a': 1,
 'c': {'a': 2,
       'b': {'x': 5,
             'y' : 10}},
 'd': [1, 2, 3]}

그것을 다음과 같이 평평하게 만드는 방법은 무엇입니까?

{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}

기본적으로 중첩 목록을 평평하게하는 것과 같은 방식으로 키 / 값으로 dict를 반복하고 새 사전에 대한 새 키를 만들고 최종 단계에서 사전을 만들기 위해 추가 작업을 수행하면됩니다.

import collections

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

원본 포스터에서 고려해야 할 두 가지 큰 고려 사항이 있습니다.

  1. 키 스페이스 클로버 링 문제가 있습니까? 예를 들어 {'a_b':{'c':1}, 'a':{'b_c':2}}결과는 다음과 같습니다 {'a_b_c':???}. 아래 솔루션은 반복 가능한 쌍을 반환하여 문제를 피합니다.
  2. 성능이 문제인 경우 키 감소 기 기능 (여기서 '결합'이라고 함)은 전체 키 경로에 액세스해야합니까, 아니면 트리의 모든 노드에서 O (1)이 작동합니까? 를 말하고 joinedKey = '_'.join(*keys)싶다면 O (N ^ 2) 실행 시간이 걸립니다. 그러나 기꺼이 말하면 nextKey = previousKey+'_'+thisKeyO (N) 시간을 얻습니다. 아래의 솔루션을 사용하면 두 키를 모두 수행 할 수 있습니다 (모든 키를 연결 한 다음 사후 처리 할 수 ​​있기 때문에).

(성능은 가능성이 문제가되지 않습니다,하지만 난 경우 다른 사람 걱정에서 두 번째 점에 정교합니다 :.이 구현에서, 수많은 위험한 선택이있다이 반복적으로 수율 및 재 수율, 또는 않으면 아무것도 하는 터치 동등한 노드가 두 번 이상 (실수로 쉽게 수행 할 수 있음) O (N) 대신 O (N ^ 2) 작업을 수행하는 경우 키 a계산 a_1다음 a_1_i...을 계산하고 계산하기 때문일 수 있습니다 a그런 a_1다음 a_1_ii..., 그러나 실제로 a_1다시 계산할 필요는 없습니다. 다시 계산하지 않아도 다시 계산하는 것 ( '레벨 별'접근 방식)도 나쁩니다. 에 대한 성능을 생각하기 위해 {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

아래는 내가 작성한 기능 flattenDict(d, join=..., lift=...)으로 많은 목적에 적합하고 원하는 것을 할 수 있습니다. 슬프게도 위의 성능 저하를 유발하지 않고이 함수의 게으른 버전을 만드는 것은 상당히 어렵습니다 (chain.from_iterable과 같은 많은 Python 내장 기능은 실제로 효율적이지 않습니다. 이 하나).

from collections import Mapping
from itertools import chain
from operator import add

_FLAG_FIRST = object()

def flattenDict(d, join=add, lift=lambda x:x):
    results = []
    def visit(subdict, results, partialKey):
        for k,v in subdict.items():
            newKey = lift(k) if partialKey==_FLAG_FIRST else join(partialKey,lift(k))
            if isinstance(v,Mapping):
                visit(v, results, newKey)
            else:
                results.append((newKey,v))
    visit(d, results, _FLAG_FIRST)
    return results

무슨 일이 일어나고 있는지 더 잘 이해하기 위해, 아래는 reduce"왼쪽"에 익숙하지 않은 사람들을위한 다이어그램입니다 . 때로는 k0 (목록의 일부가 아닌 함수에 전달됨) 대신 초기 값으로 그려집니다. 여기, J우리의 join기능이 있습니다. 각 k n을사전 처리합니다 lift(k).

               [k0,k1,...,kN].foldleft(J)
                           /    \
                         ...    kN
                         /
       J(k0,J(k1,J(k2,k3)))
                       /  \
                      /    \
           J(J(k0,k1),k2)   k3
                    /   \
                   /     \
             J(k0,k1)    k2
                 /  \
                /    \
               k0     k1

이것은 실제로와 동일 functools.reduce하지만 함수가 트리의 모든 키 경로에 대해 수행하는 위치입니다.

>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)

데모 (문서 문자열에 넣을 것) :

>>> testData = {
        'a':1,
        'b':2,
        'c':{
            'aa':11,
            'bb':22,
            'cc':{
                'aaa':111
            }
        }
    }
from pprint import pprint as pp

>>> pp(dict( flattenDict(testData, lift=lambda x:(x,)) ))
{('a',): 1,
 ('b',): 2,
 ('c', 'aa'): 11,
 ('c', 'bb'): 22,
 ('c', 'cc', 'aaa'): 111}

>>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b) ))
{'a': 1, 'b': 2, 'c_aa': 11, 'c_bb': 22, 'c_cc_aaa': 111}    

>>> pp(dict( (v,k) for k,v in flattenDict(testData, lift=hash, join=lambda a,b:hash((a,b))) ))
{1: 12416037344,
 2: 12544037731,
 11: 5470935132935744593,
 22: 4885734186131977315,
 111: 3461911260025554326}

공연:

from functools import reduce
def makeEvilDict(n):
    return reduce(lambda acc,x:{x:acc}, [{i:0 for i in range(n)}]+range(n))

import timeit
def time(runnable):
    t0 = timeit.default_timer()
    _ = runnable()
    t1 = timeit.default_timer()
    print('took {:.2f} seconds'.format(t1-t0))

>>> pp(makeEvilDict(8))
{7: {6: {5: {4: {3: {2: {1: {0: {0: 0,
                                 1: 0,
                                 2: 0,
                                 3: 0,
                                 4: 0,
                                 5: 0,
                                 6: 0,
                                 7: 0}}}}}}}}}

import sys
sys.setrecursionlimit(1000000)

forget = lambda a,b:''

>>> time(lambda: dict(flattenDict(makeEvilDict(10000), join=forget)) )
took 0.10 seconds
>>> time(lambda: dict(flattenDict(makeEvilDict(100000), join=forget)) )
[1]    12569 segmentation fault  python

... 한숨, 내 잘못이라고 생각하지 마십시오 ...


[조정 문제로 인한 중요하지 않은 기록]

Flatten의 중복 주장과 관련하여 Python의 사전 사전 (2 레벨) 사전 :

이 질문의 해결책은을 수행 하여이 질문의 관점에서 구현할 수 있습니다 sorted( sum(flatten(...),[]) ). 그 반대의 경우는 불가능합니다. 고차 누산기를 매핑하여 의심되는 복제본에서 flatten(...)복구 할 수 있지만 키를 복구 할 수는 없습니다. (편집 : 또한 중복 된 소유자의 질문은 완전히 다른 것으로 나타났습니다. 단, 해당 페이지의 답변 중 하나가 일반적인 해결책을 제공하지만 정확히 2 단계 깊이의 사전 만 처리한다는 점입니다.)


또는 이미 팬더를 사용하고 있다면 다음과 json_normalize()같이 할 수 있습니다 .

import pandas as pd

d = {'a': 1,
     'c': {'a': 2, 'b': {'x': 5, 'y' : 10}},
     'd': [1, 2, 3]}

df = pd.io.json.json_normalize(d, sep='_')

print(df.to_dict(orient='records')[0])

산출:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}

다음은 일종의 "기능적", "한 줄짜리"구현입니다. 재귀 적이며 조건식과 dict 이해력을 기반으로합니다.

def flatten_dict(dd, separator='_', prefix=''):
    return { prefix + separator + k if prefix else k : v
             for kk, vv in dd.items()
             for k, v in flatten_dict(vv, separator, kk).items()
             } if isinstance(dd, dict) else { prefix : dd }

테스트:

In [2]: flatten_dict({'abc':123, 'hgf':{'gh':432, 'yu':433}, 'gfd':902, 'xzxzxz':{"432":{'0b0b0b':231}, "43234":1321}}, '.')
Out[2]: 
{'abc': 123,
 'gfd': 902,
 'hgf.gh': 432,
 'hgf.yu': 433,
 'xzxzxz.432.0b0b0b': 231,
 'xzxzxz.43234': 1321}

당신이 사용하는 경우 pandas에 숨겨진 기능이 pandas.io.json._normalize하나 라는 nested_to_record정확히이 일을합니다.

from pandas.io.json._normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')

1 에서 판다 버전 0.24.x이상을 사용 pandas.io.json.normalize합니다 (없는 _)


암호:

test = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}

def parse_dict(init, lkey=''):
    ret = {}
    for rkey,val in init.items():
        key = lkey+rkey
        if isinstance(val, dict):
            ret.update(parse_dict(val, key+'_'))
        else:
            ret[key] = val
    return ret

print(parse_dict(test,''))

결과 :

$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

python3.2를 사용하고 있습니다. 파이썬 버전을 업데이트하십시오.


이것은 사전으로 제한되지 않지만 .items ()를 구현하는 모든 매핑 유형입니다. if 조건을 피하기 때문에 더 빠릅니다. 그럼에도 불구하고 크레딧은 Imran으로갑니다.

def flatten(d, parent_key=''):
    items = []
    for k, v in d.items():
        try:
            items.extend(flatten(v, '%s%s_' % (parent_key, k)).items())
        except AttributeError:
            items.append(('%s%s' % (parent_key, k), v))
    return dict(items)

Python3.5 기능 및 성능 솔루션은 어떻습니까?

from functools import reduce


def _reducer(items, key, val, pref):
    if isinstance(val, dict):
        return {**items, **flatten(val, pref + key)}
    else:
        return {**items, pref + key: val}

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: _reducer(new_d, *kv, pref), 
        d.items(), 
        {}
    ))

이것은 훨씬 더 성능이 좋습니다.

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: \
            isinstance(kv[1], dict) and \
            {**new_d, **flatten(kv[1], pref + kv[0])} or \
            {**new_d, pref + kv[0]: kv[1]}, 
        d.items(), 
        {}
    ))

사용:

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

print(flatten(my_obj)) 
# {'d': [1, 2, 3], 'cby': 10, 'cbx': 5, 'ca': 2, 'a': 1}

발전기를 사용하는 Python 3.3 솔루션 :

def flattenit(pyobj, keystring=''):
   if type(pyobj) is dict:
     if (type(pyobj) is dict):
         keystring = keystring + "_" if keystring else keystring
         for k in pyobj:
             yield from flattenit(pyobj[k], keystring + k)
     elif (type(pyobj) is list):
         for lelm in pyobj:
             yield from flatten(lelm, keystring)
   else:
      yield keystring, pyobj

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

#your flattened dictionary object
flattened={k:v for k,v in flattenit(my_obj)}
print(flattened)

# result: {'c_b_y': 10, 'd': [1, 2, 3], 'c_a': 2, 'a': 1, 'c_b_x': 5}

중첩 된 사전을 평평하게하는 간단한 기능. 파이썬 3의 교체 .iteritems()와 함께.items()

def flatten_dict(init_dict):
    res_dict = {}
    if type(init_dict) is not dict:
        return res_dict

    for k, v in init_dict.iteritems():
        if type(v) == dict:
            res_dict.update(flatten_dict(v))
        else:
            res_dict[k] = v

    return res_dict

아이디어 / 요구 사항은 다음과 같습니다. 부모 키를 유지하지 않고 플랫 사전을 가져옵니다.

사용 예 :

dd = {'a': 3, 
      'b': {'c': 4, 'd': 5}, 
      'e': {'f': 
                 {'g': 1, 'h': 2}
           }, 
      'i': 9,
     }

flatten_dict(dd)

>> {'a': 3, 'c': 4, 'd': 5, 'g': 1, 'h': 2, 'i': 9}

부모 키를 유지하는 것도 간단합니다.


이것은 imran과 ralu의 대답과 비슷합니다. 생성기를 사용하지 않고 클로저로 재귀를 사용합니다.

def flatten_dict(d, separator='_'):
  final = {}
  def _flatten_dict(obj, parent_keys=[]):
    for k, v in obj.iteritems():
      if isinstance(v, dict):
        _flatten_dict(v, parent_keys + [k])
      else:
        key = separator.join(parent_keys + [k])
        final[key] = v
  _flatten_dict(d)
  return final

>>> print flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

Davoud의 솔루션은 매우 훌륭하지만 중첩 된 dict에 dict 목록이 포함 된 경우 만족스러운 결과를 얻지 못하지만 해당 코드는 해당 경우에 맞게 조정됩니다.

def flatten_dict(d):
    items = []
    for k, v in d.items():
        try:
            if (type(v)==type([])): 
                for l in v: items.extend(flatten_dict(l).items())
            else: 
                items.extend(flatten_dict(v).items())
        except AttributeError:
            items.append((k, v))
    return dict(items)

위의 답변은 실제로 잘 작동합니다. 방금 작성한 평평하지 않은 함수를 추가한다고 생각했습니다.

def unflatten(d):
    ud = {}
    for k, v in d.items():
        context = ud
        for sub_key in k.split('_')[:-1]:
            if sub_key not in context:
                context[sub_key] = {}
            context = context[sub_key]
        context[k.split('_')[-1]] = v
    return ud

참고 : 이것은 평평한 상대방과 마찬가지로 키에 이미 존재하는 '_'을 고려하지 않습니다.


다음은 우아하고 적절한 교체를위한 알고리즘입니다. Python 2.7 및 Python 3.5로 테스트되었습니다. 점 문자를 구분 기호로 사용

def flatten_json(json):
    if type(json) == dict:
        for k, v in list(json.items()):
            if type(v) == dict:
                flatten_json(v)
                json.pop(k)
                for k2, v2 in v.items():
                    json[k+"."+k2] = v2

예:

d = {'a': {'b': 'c'}}                   
flatten_json(d)
print(d)
unflatten_json(d)
print(d)

산출:

{'a.b': 'c'}
{'a': {'b': 'c'}}

이 코드를 일치하는 함수 와 함께 여기에 게시했습니다 unflatten_json.


중첩 된 사전을 플랫 화하고 모든 고유 키 목록을 원하면 여기에 해결책이 있습니다.

def flat_dict_return_unique_key(data, unique_keys=set()):
    if isinstance(data, dict):
        [unique_keys.add(i) for i in data.keys()]
        for each_v in data.values():
            if isinstance(each_v, dict):
                flat_dict_return_unique_key(each_v, unique_keys)
    return list(set(unique_keys))

def flatten(unflattened_dict, separator='_'):
    flattened_dict = {}

    for k, v in unflattened_dict.items():
        if isinstance(v, dict):
            sub_flattened_dict = flatten(v, separator)
            for k2, v2 in sub_flattened_dict.items():
                flattened_dict[k + separator + k2] = v2
        else:
            flattened_dict[k] = v

    return flattened_dict

발전기 사용하기 :

def flat_dic_helper(prepand,d):
    if len(prepand) > 0:
        prepand = prepand + "_"
    for k in d:
        i=d[k]
        if type(i).__name__=='dict':
            r = flat_dic_helper(prepand+k,i)
            for j in r:
                yield j
        else:
            yield (prepand+k,i)

def flat_dic(d): return dict(flat_dic_helper("",d))

d={'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
print(flat_dic(d))


>> {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

간단한 중첩 목록과 같은 재귀에서 dict.popitem () 사용 :

def flatten(d):
    if d == {}:
        return d
    else:
        k,v = d.popitem()
        if (dict != type(v)):
            return {k:v, **flatten(d)}
        else:
            flat_kv = flatten(v)
            for k1 in list(flat_kv.keys()):
                flat_kv[k + '_' + k1] = flat_kv[k1]
                del flat_kv[k1]
            return {**flat_kv, **flatten(d)}

나는 항상을 dict통해 객체에 액세스하는 것을 선호 .items()하기 때문에, 평 평화 dicts에는 다음 재귀 생성기를 사용합니다 flat_items(d). dict다시 갖고 싶다면 간단히 다음과 같이 감싸십시오.flat = dict(flat_items(d))

def flat_items(d, key_separator='.'):
    """
    Flattens the dictionary containing other dictionaries like here: https://stackoverflow.com/questions/6027558/flatten-nested-python-dictionaries-compressing-keys

    >>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
    >>> flat = dict(flat_items(example, key_separator='_'))
    >>> assert flat['c_b_y'] == 10
    """
    for k, v in d.items():
        if type(v) is dict:
            for k1, v1 in flat_items(v, key_separator=key_separator):
                yield key_separator.join((k, k1)), v1
        else:
            yield k, v

나는 실제로 그렇게 자주해야했기 때문에 실제로이 종류의 물건을 다루기 위해 최근 cherrypicker라는 패키지를 작성했습니다!

다음 코드는 당신이 쫓는 것을 정확하게 줄 것이라고 생각합니다.

from cherrypicker import CherryPicker

dct = {
    'a': 1,
    'c': {
        'a': 2,
        'b': {
            'x': 5,
            'y' : 10
        }
    },
    'd': [1, 2, 3]
}

picker = CherryPicker(dct)
picker.flatten().get()

다음을 사용하여 패키지를 설치할 수 있습니다.

pip install cherrypicker

... https : //cherrypicker.readthedocs.io에 더 많은 문서와 지침이 있습니다 .

다른 방법이 더 빠를 수 있지만,이 패키지의 우선 순위는 작업을하는 것입니다 신속 하고 쉽게 . 평면화 할 개체 목록이 많을 경우 병렬 처리를 사용하여 작업 속도를 높이도록 CherryPicker에 지시 할 수도 있습니다.


그냥 사용 python-benedict하면 flatten메소드를 포함하여 많은 기능을 제공하는 dict 서브 클래스입니다 . pip를 사용하여 설치할 수 있습니다.pip install python-benedict

https://github.com/fabiocaccamo/python-benedict#flatten

from benedict import benedict 

d = benedict(data)
f = d.flatten(separator='_')

참고 URL : https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys

반응형