さまざまな深さのネストされた辞書の値を更新する


162

レベル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}}}

updateはlevel2の値を削除することを知っています。これは、最も低いキーlevel1を更新しているためです。

dictionary1とupdateは任意の長さにできるので、これにどのように取り組むことができますか?


ネストは常に3レベルの深さですか、それとも任意の深さのネストが可能ですか?
ChristopheD

深さ/長さは任意です。
jay_t

私が間違っている場合は修正してください。ただし、ここでの理想的なソリューションには、複合設計パターンの実装が必要と思われます。
Alexander McNulty

回答:


263

@FMの答えには、正しい一般的なアイデア、つまり再帰的な解決策がありますが、多少独特のコーディングと少なくとも1つのバグがあります。代わりに、私はお勧めします:

Python 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

Python 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

「更新」を持っていたときにアップバグショーkvアイテムvであるdictk元々更新される辞書内のキーではありません-それは空の新しい上でそれを実行するため、(FMのコード「スキップ」@更新のこの部分をdictどのどこにも保存または返されず、再帰呼び出しが戻ったときに失われるだけです)。

私の他の変更はマイナーです。同じ処理をより速く、よりきれいにするときにif/ else構成に理由はなく、一般性のために(具体的なものではなく)抽象基本クラスに適用するのが最適です。.getisinstance


7
+1バグをうまくキャッチ-doh!私は誰かがisinstanceテストを処理するより良い方法を持っているだろうと考えましたが、私はそれを試してみようと思いました。
FMc、2013

6
もう1つのマイナーな「機能」により、これが発生したTypeError: 'int' object does not support item assignment.ときに発生しますupdate({'k1': 1}, {'k1': {'k2': 2}})。この動作を変更し、代わりにディクショナリの深さを拡張してより深いディクショナリのための余地を作るには、条件のelif isinstance(d, Mapping):前後にを追加できます。また、更新する辞書が元の辞書よりも深い場合に対処するためにを追加する必要があります。回答を編集して満足していますが、OPの問題を解決する簡潔なコードを汚したくありません。d[k] = u[k]isinstanceelse: d = {k: u[k]}
ホブ、

1
なぜ使うisinstance(v, collections.Mapping)のではなくisinstance(v, dict)?OPがコレクションの使用を開始することを決定した場合?
マット

2
@Matt Yea、またはその他のマッピング派生オブジェクト(モノのペアのリスト)。関数をより一般的にし、マッピングから派生したオブジェクトを静かに無視し、更新しないままにします(OPがこれまでに見たりキャッチしたりしないかもしれない潜伏的なエラー)。ほとんどの場合、Mappingを使用してdictタイプを検索し、basestringを使用してstrタイプを検索します。
ホブ

2
あなたは、Python 3+の変化の下でこれを実行している場合u.iteritems()u.items()、そうでなければ、発生します:AttributeError: 'dict' object has no attribute 'iteritems'
グレッグK

23

これについて少し教えてくれましたが、@ 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

3
これはおそらく(少し安全になる)はずですorig_dict.get(key, []) + val
Andy Hayden

2
辞書は変更可能であるため、引数として渡すインスタンスを変更します。その後、orig_dictを返す必要はありません。
gabrielhpugliese 2015

3
ほとんどの人は、定義が更新されても、更新された辞書を返すと期待していると思います。
Kel Solaar

onosendiのコードのデフォルトのロジックは、更新されたリストを元のリストに追加することです。元のリストを上書きして更新する必要がある場合は、orig_dict [key] = val
intijk

1
@gabrielhpuglieseが辞書リテラルで呼び出された場合、元を返す必要があります。例 merged_tree = update({'default': {'initialvalue': 1}}, other_tree)
EoghanM

18

@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}}})

そうですか。elif元のオブジェクトタイプの私のチェックを、その辞書/マッピングの値とキーの両方のチェックを含む「囲んでいる」条件付きにしました。賢い。
ホブ、2015

内部の辞書に複数のキーがある場合、これは機能しません。
Wlerin 2017

@Wlerin、それでも動作します。その時点でdはマッピングになります。複数のキーを使用したテストケースは次のとおりupdate({'A1': 1, 'A2':2}, {'A1': {'B1': {'C1': 3, 'C2':4}, 'B2':2}, 'A3':5})です。あなたが望むことをしない例がありますか?
bscan 2017

なぜif isinstance(d, collections.Mapping)evey反復でテストするのですか?私の答えをください。
ジェローム

13

受け入れられたものと同じソリューションですが、変数の命名、ドキュメント文字列がより明確になり{}、値がオーバーライドされないバグを修正しました。

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


7

誰かがそれを必要とする場合に備えて、再帰的な辞書マージの不変バージョンです。

@Alex Martelliの回答に基づく。

Python 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

Python 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

6

深さが異なる辞書の更新を可能にし、更新が元のネストされた辞書に割り込む深さを制限できるようにする@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

1
これをありがとう!depthパラメータはどのような場合に適用されますか?
マット

マージ/更新したくない既知の深さにいくつかのオブジェクト/辞書がある場合の@Mattは、新しいオブジェクトで上書きされるだけです(辞書を文字列やフロートなどで置き換えるなど、辞書の奥深く)
ホブ

1
これは、更新が元のレベルよりも最大で1レベル深い場合にのみ機能します。たとえば、これは失敗update({'k1': 1}, {'k1': {'k2': {'k3': 3}}})します。これに対処する回答を追加しました
bscan

@bscan良いキャッチ!そのユースケースを考えたことはありません。elifブランチでより深く再帰する必要があると思います。何か案は?
ホブ

なぜif isinstance(d, Mapping)evey反復でテストするのですか?私の答えをください。(また、あなたのことd = {k: u[k]}
ジェローム

4

この質問は古いですが、「ディープマージ」ソリューションを探しているときに私はここに着きました。上記の答えは、次のことに影響を与えました。私がテストしたすべてのバージョンにバグがあったため、結局自分で作成しました。見逃された重要な点は、2つの入力辞書の任意の深さで、いくつかのキーkについて、d [k]またはu [k]がdictではない場合の決定木に誤りがあったことです。

また、このソリューションは再帰を必要としません。再帰は、がどのように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))

4

python-benedict (私がやった)を使うだけで、merge(deepupdate)ユーティリティメソッドや他の多くのメソッドがあります。python 2 / python 3で動作し、十分にテストされています。

from benedict import benedict

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

インストール: pip install python-benedict

ドキュメント:https : //github.com/fabiocaccamo/python-benedict


2

これらの回答のいずれにおいても、著者は、辞書に格納されたオブジェクトを更新する概​​念や、(キーではなく)辞書項目を反復する概念さえ理解していないようです。だから私は意味のないトートロジーの辞書を保存したり検索したりしないものを書かなければならなかった。辞書は、他の辞書または単純なタイプを格納すると想定されています。

def update_nested_dict(d, other):
    for k, v in other.items():
        if isinstance(v, collections.Mapping):
            d_v = d.get(k)
            if isinstance(d_v, collections.Mapping):
                update_nested_dict(d_v, v)
            else:
                d[k] = v.copy()
        else:
            d[k] = v

または、任意のタイプを処理するさらに簡単なもの:

def update_nested_dict(d, other):
    for k, v in other.items():
        d_v = d.get(k)
        if isinstance(v, collections.Mapping) and isinstance(d_v, collections.Mapping):
            update_nested_dict(d_v, v)
        else:
            d[k] = deepcopy(v) # or d[k] = v if you know what you're doing

2

@Alex Martelliの回答を更新して、コードのバグを修正し、ソリューションをより堅牢にします。

def update_dict(d, u):
    for k, v in u.items():
        if isinstance(v, collections.Mapping):
            default = v.copy()
            default.clear()
            r = update_dict(d.get(k, default), v)
            d[k] = r
        else:
            d[k] = v
    return d

重要な点は、再帰時に同じ型を作成することがよくあるため、ここでは使用しますv.copy().clear()が使用しません{}。そして、これは、dictここがcollections.defaultdictさまざまな種類のdefault_factorysを持つことができるタイプの場合に特に便利です。

また、u.iteritems()がに変更さu.items()れていることにも注意してくださいPython3


2

@Alex Martelliが提案するソリューションを使用しましたが、失敗しました

TypeError 'bool' object does not support item assignment

2つのディクショナリのデータタイプがある程度異なる場合。

同じレベルでディクショナリの要素dが単なるスカラー(つまりBool)である場合、ディクショナリの要素uはまだディクショナリですが、スカラへのディクショナリの割り当ては不可能であるため、再割り当ては失敗します(などTrue[k])。

追加された条件の1つは、以下を修正します。

from collections import Mapping

def update_deep(d, u):
    for k, v in u.items():
        # this condition handles the problem
        if not isinstance(d, Mapping):
            d = u
        elif isinstance(v, Mapping):
            r = update_deep(d.get(k, {}), v)
            d[k] = r
        else:
            d[k] = u[k]

    return d

2

以下のコードはupdate({'k1': 1}, {'k1': {'k2': 2}})、@ Alex Martelliの答えの正しい方法で問題を解決するはずです。

def deepupdate(original, update):
    """Recursively update a dict.

    Subdict's won't be overwritten but also updated.
    """
    if not isinstance(original, abc.Mapping):
        return update
    for key, value in update.items():
        if isinstance(value, abc.Mapping):
            original[key] = deepupdate(original.get(key, {}), value)
        else:
            original[key] = value
    return original

1
def update(value, nvalue):
    if not isinstance(value, dict) or not isinstance(nvalue, dict):
        return nvalue
    for k, v in nvalue.items():
        value.setdefault(k, dict())
        if isinstance(v, dict):
            v = update(value[k], v)
        value[k] = v
    return value

使用dictまたはcollections.Mapping


1

私はこの質問がかなり古いことを知っていますが、ネストされた辞書を更新する必要があるときに私がすることをまだ投稿しています。キーのパスが既知であり、ドットで区切られていると仮定すると、Pythonでディクショナリが参照によって渡されるという事実を使用できます。データという名前の辞書がある場合、外国為替:

{
"log_config_worker": {
    "version": 1, 
    "root": {
        "handlers": [
            "queue"
        ], 
        "level": "DEBUG"
    }, 
    "disable_existing_loggers": true, 
    "handlers": {
        "queue": {
            "queue": null, 
            "class": "myclass1.QueueHandler"
        }
    }
}, 
"number_of_archived_logs": 15, 
"log_max_size": "300M", 
"cron_job_dir": "/etc/cron.hourly/", 
"logs_dir": "/var/log/patternex/", 
"log_rotate_dir": "/etc/logrotate.d/"
}

そして、キュークラスを更新したいのですが、キーのパスは- log_config_worker.handlers.queue.class

次の関数を使用して値を更新できます。

def get_updated_dict(obj, path, value):
    key_list = path.split(".")

    for k in key_list[:-1]:
        obj = obj[k]

    obj[key_list[-1]] = value

get_updated_dict(data, "log_config_worker.handlers.queue.class", "myclass2.QueueHandler")

これにより、辞書が正しく更新されます。


1

iteritems-Attributeを持たない、今日のような非標準の辞書を見つけたかもしれません。この場合、このタイプの辞書を標準の辞書として解釈するのは簡単です。例: Python 2.7:

    import collections
    def update(orig_dict, new_dict):
        for key, val in dict(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[key] + val)
            else:
                orig_dict[key] = new_dict[key]
        return orig_dict

    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234}

    x=update(d, u)
    x.items()

Python 3.8:

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

    import collections
    import multiprocessing
    d=multiprocessing.Manager().dict({'sample':'data'})
    u={'other': 1234, "deeper": {'very': 'deep'}}

    x=update(d, u)
    x.items()

0

はい!そして別の解決策。私のソリューションは、チェックされるキーが異なります。他のすべてのソリューションでは、のキーのみを調べますdict_b。しかし、ここでは両方の辞書の結合を調べます。

好きなようにやってください

def update_nested(dict_a, dict_b):
    set_keys = set(dict_a.keys()).union(set(dict_b.keys()))
    for k in set_keys:
        v = dict_a.get(k)
        if isinstance(v, dict):
            new_dict = dict_b.get(k, None)
            if new_dict:
                update_nested(v, new_dict)
        else:
            new_value = dict_b.get(k, None)
            if new_value:
                dict_a[k] = new_value

0

「完全にネストされた辞書を配列で置き換える」場合は、次のスニペットを使用できます。

「old_value」を「new_value」に置き換えます。大まかに、辞書の深さ優先の再構築を行っています。最初のレベルの入力パラメーターとして指定されたListまたはStr / intでも機能します。

def update_values_dict(original_dict, future_dict, old_value, new_value):
    # Recursively updates values of a nested dict by performing recursive calls

    if isinstance(original_dict, Dict):
        # It's a dict
        tmp_dict = {}
        for key, value in original_dict.items():
            tmp_dict[key] = update_values_dict(value, future_dict, old_value, new_value)
        return tmp_dict
    elif isinstance(original_dict, List):
        # It's a List
        tmp_list = []
        for i in original_dict:
            tmp_list.append(update_values_dict(i, future_dict, old_value, new_value))
        return tmp_list
    else:
        # It's not a dict, maybe a int, a string, etc.
        return original_dict if original_dict != old_value else new_value

0

再帰を使用する別の方法:

def updateDict(dict1,dict2):
    keys1 = list(dict1.keys())
    keys2= list(dict2.keys())
    keys2 = [x for x in keys2 if x in keys1]
    for x in keys2:
        if (x in keys1) & (type(dict1[x]) is dict) & (type(dict2[x]) is dict):
            updateDict(dict1[x],dict2[x])
        else:
            dict1.update({x:dict2[x]})
    return(dict1)

0

キーチェーンによる新しいQの方法

dictionary1={'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':{'anotherLevelA':0,'anotherLevelB':1}}}
update={'anotherLevel1':{'anotherLevel2':1014}}
dictionary1.update(update)
print dictionary1
{'level1':{'level2':{'levelA':0,'levelB':1}},'anotherLevel1':{'anotherLevel2':1014}}

0

あなたはこれを試すことができます、それはリストで動作し、純粋です:

def update_keys(newd, dic, mapping):
  def upsingle(d,k,v):
    if k in mapping:
      d[mapping[k]] = v
    else:
      d[k] = v
  for ekey, evalue in dic.items():
    upsingle(newd, ekey, evalue)
    if type(evalue) is dict:
      update_keys(newd, evalue, mapping)
    if type(evalue) is list:
      upsingle(newd, ekey, [update_keys({}, i, mapping) for i in evalue])
  return newd

0

に格納されているが存在しないdictサブクラスのオブジェクトタイプを伝播するために、{}by を置き換えることをお勧めしtype(v)()ます。たとえば、これはcollections.OrderedDictなどの型を保持します。ud

Python 2:

import collections

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

Python 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, type(v)()), v)
        else:
            d[k] = v
    return d

-1

それは少しおかしいですが、ネストされた辞書が本当に必要ですか?問題によっては、フラットな辞書で十分な場合があります...

>>> dict1 = {('level1','level2','levelA'): 0}
>>> dict1['level1','level2','levelB'] = 1
>>> update = {('level1','level2','levelB'): 10}
>>> dict1.update(update)
>>> print dict1
{('level1', 'level2', 'levelB'): 10, ('level1', 'level2', 'levelA'): 0}

5
ネストされた構造は、着信jsonデータセットに由来するため、そのままにしたいのですが...
jay_t

-1

ワンライナーが必要な場合:

{**dictionary1, **{'level1':{**dictionary1['level1'], **{'level2':{**dictionary1['level1']['level2'], **{'levelB':10}}}}}}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.