[python] 다양한 깊이의 중첩 된 사전 값 업데이트

덮어 쓰기 레벨 A를 사용하여 dict update1의 내용으로 dict dictionary1을 업데이트하는 방법을 찾고 있습니다.

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}}}
update={'level1':{'level2':{'levelB':10}}}
dictionary1.update(update)
print dictionary1
{'level1': {'level2': {'levelB': 10}}}

업데이트는 가장 낮은 키 level1을 업데이트하기 때문에 level2의 값을 삭제한다는 것을 알고 있습니다.

dictionary1과 update가 길이를 가질 수 있다면 어떻게 해결할 수 있습니까?



답변

@FM의 대답은 올바른 일반적인 아이디어, 즉 재귀 솔루션이지만 다소 독특한 코딩과 적어도 하나의 버그를 가지고 있습니다. 대신 권장합니다.

파이썬 2 :

import collections

def update(d, u):
    for k, v in u.iteritems():
        if isinstance(v, collections.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d

파이썬 3 :

import collections.abc

def update(d, u):
    for k, v in u.items():
        if isinstance(v, collections.abc.Mapping):
            d[k] = update(d.get(k, {}), v)
        else:
            d[k] = v
    return d

“업데이트”가있을 때까지 버그 쇼 k, v항목 vA는 dict하고 k원래 업데이트되는 사전에서 키가 아닌 – 그것은 비어있는 새에 그것을 수행하기 때문에 (FM의 코드 “건너 뜀”@ 업데이트의이 부분을 dict어떤 재귀 호출이 반환되면 손실되거나 저장되지 않습니다).

내 다른 변경 사항은 사소합니다. 동일한 작업을 더 빠르고 깔끔하게 수행 할 때 if/ else구문에 대한 이유가 없으며 일반성을 위해 추상 기본 클래스 (구체 클래스가 아닌)에 가장 적합합니다..getisinstance


답변

이것에 대해 조금 알아 차 렸지만 @Alex의 게시물 덕분에 그는 누락 된 격차를 메 웠습니다. 그러나 재귀 내의 값이 인 경우 문제가 dict발생 list했기 때문에 공유하고 답변을 확장 할 것이라고 생각했습니다.

import collections

def update(orig_dict, new_dict):
    for key, val in new_dict.iteritems():
        if isinstance(val, collections.Mapping):
            tmp = update(orig_dict.get(key, { }), val)
            orig_dict[key] = tmp
        elif isinstance(val, list):
            orig_dict[key] = (orig_dict.get(key, []) + val)
        else:
            orig_dict[key] = new_dict[key]
    return orig_dict


답변

@Alex의 대답은 좋지만 정수와 같은 요소를 사전과 같은 사전으로 바꾸면 작동하지 않습니다 update({'foo':0},{'foo':{'bar':1}}). 이 업데이트는 다음을 해결합니다.

import collections
def update(d, u):
    for k, v in u.iteritems():
        if isinstance(d, collections.Mapping):
            if isinstance(v, collections.Mapping):
                r = update(d.get(k, {}), v)
                d[k] = r
            else:
                d[k] = u[k]
        else:
            d = {k: u[k]}
    return d

update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})


답변

허용되는 것과 동일한 솔루션이지만 더 명확한 변수 이름 지정, docstring 및 {}값으로 무시되지 않는 버그가 수정 되었습니다.

import collections


def deep_update(source, overrides):
    """
    Update a nested dictionary or similar mapping.
    Modify ``source`` in place.
    """
    for key, value in overrides.iteritems():
        if isinstance(value, collections.Mapping) and value:
            returned = deep_update(source.get(key, {}), value)
            source[key] = returned
        else:
            source[key] = overrides[key]
    return source

다음은 몇 가지 테스트 사례입니다.

def test_deep_update():
    source = {'hello1': 1}
    overrides = {'hello2': 2}
    deep_update(source, overrides)
    assert source == {'hello1': 1, 'hello2': 2}

    source = {'hello': 'to_override'}
    overrides = {'hello': 'over'}
    deep_update(source, overrides)
    assert source == {'hello': 'over'}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': 'over'}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 'over', 'no_change': 1}}

    source = {'hello': {'value': 'to_override', 'no_change': 1}}
    overrides = {'hello': {'value': {}}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': {}, 'no_change': 1}}

    source = {'hello': {'value': {}, 'no_change': 1}}
    overrides = {'hello': {'value': 2}}
    deep_update(source, overrides)
    assert source == {'hello': {'value': 2, 'no_change': 1}}

이 기능은의 charlatan 패키지 에서 사용할 수 있습니다 charlatan.utils.


답변

누군가가 필요로 할 경우를 대비하여 재귀 사전 병합의 불변 버전이 있습니다.

@Alex Martelli의 답변을 바탕으로 .

파이썬 2.x :

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.iteritems():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result

파이썬 3.x :

import collections
from copy import deepcopy


def merge(dict1, dict2):
    ''' Return a new dictionary by merging two dictionaries recursively. '''

    result = deepcopy(dict1)

    for key, value in dict2.items():
        if isinstance(value, collections.Mapping):
            result[key] = merge(result.get(key, {}), value)
        else:
            result[key] = deepcopy(dict2[key])

    return result


답변

깊이가 다른 사전을 업데이트하고 업데이트가 원래 중첩 사전으로 다이빙하는 깊이를 제한 할 수있는 @Alex의 답변 이 약간 개선되었습니다 (업데이트 사전 깊이는 제한되지 않음). 몇 가지 사례 만 테스트되었습니다.

def update(d, u, depth=-1):
    """
    Recursively merge or update dict-like objects.
    >>> update({'k1': {'k2': 2}}, {'k1': {'k2': {'k3': 3}}, 'k4': 4})
    {'k1': {'k2': {'k3': 3}}, 'k4': 4}
    """

    for k, v in u.iteritems():
        if isinstance(v, Mapping) and not depth == 0:
            r = update(d.get(k, {}), v, depth=max(depth - 1, -1))
            d[k] = r
        elif isinstance(d, Mapping):
            d[k] = u[k]
        else:
            d = {k: u[k]}
    return d


답변

이 질문은 오래되었지만 “깊은 병합”솔루션을 검색 할 때 여기에 왔습니다. 위의 답변은 다음과 같은 내용에 영감을주었습니다. 테스트 한 모든 버전에 버그가 있었기 때문에 직접 작성했습니다. 누락 된 임계점은 d [k] 또는 u [k]가 아닌 경우 일부 입력 키의 경우 k에 대한 결정 트리의 임의의 깊이에서 받아쓰기 였습니다.

또한이 솔루션에는 재귀가 필요하지 않습니다. 재귀는 dict.update()작동 방식 과 더 대칭이며을 반환합니다 None.

import collections
def deep_merge(d, u):
   """Do a deep merge of one dict into another.

   This will update d with values in u, but will not delete keys in d
   not found in u at some arbitrary depth of d. That is, u is deeply
   merged into d.

   Args -
     d, u: dicts

   Note: this is destructive to d, but not u.

   Returns: None
   """
   stack = [(d,u)]
   while stack:
      d,u = stack.pop(0)
      for k,v in u.items():
         if not isinstance(v, collections.Mapping):
            # u[k] is not a dict, nothing to merge, so just set it,
            # regardless if d[k] *was* a dict
            d[k] = v
         else:
            # note: u[k] is a dict

            # get d[k], defaulting to a dict, if it doesn't previously
            # exist
            dv = d.setdefault(k, {})

            if not isinstance(dv, collections.Mapping):
               # d[k] is not a dict, so just set it to u[k],
               # overriding whatever it was
               d[k] = v
            else:
               # both d[k] and u[k] are dicts, push them on the stack
               # to merge
               stack.append((dv, v))