ネストされた辞書の値を取得するためのPythonセーフメソッド


144

ネストされた辞書があります。安全に値を取得する方法は1つだけですか?

try:
    example_dict['key1']['key2']
except KeyError:
    pass

それともPythonにはget()ネストされた辞書のようなメソッドがありますか?


次も参照してください: stackoverflow.com/questions/14692690/…–
dreftymac

1
あなたの質問のコードは、私の見解では、ネストされた値を辞書から取得するための最良の方法です。except keyerror:句には常にデフォルト値を指定できます。
Peter Schorn

回答:


280

あなたはget2回使うことができます:

example_dict.get('key1', {}).get('key2')

Noneどちらkey1key2が存在しない場合に返されます。

これはAttributeErrorif example_dict['key1']が存在する場合でも発生する可能性がありますが、dict(またはgetメソッドを持つdictのようなオブジェクト)ではないことに注意してください。try..exceptあなたが投稿したコードは、引き上げるTypeError場合には代わりにexample_dict['key1']unsubscriptableです。

別の違いはtry...except、最初の欠落したキーの直後に短絡することです。get呼び出しのチェーンはありません。


構文を保持しexample_dict['key1']['key2']たいがKeyErrorsを発生させたくない場合は、Hasherレシピを使用できます。

class Hasher(dict):
    # https://stackoverflow.com/a/3405143/190597
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

example_dict = Hasher()
print(example_dict['key1'])
# {}
print(example_dict['key1']['key2'])
# {}
print(type(example_dict['key1']['key2']))
# <class '__main__.Hasher'>

キーがない場合、これは空のHasherを返すことに注意してください。

はのHasherサブクラスなので、dictを使用するのとほぼ同じ方法でHasherを使用できますdict。すべて同じメソッドと構文を使用できます。ハッシャーは欠落しているキーを異なる方法で処理します。

あなたはこのようにレギュラーdictを変換することができますHasher

hasher = Hasher(example_dict)

同様に簡単Hasherにaを通常のものに変換dictします:

regular_dict = dict(hasher)

別の方法は、ヘルパー関数の醜さを隠すことです:

def safeget(dct, *keys):
    for key in keys:
        try:
            dct = dct[key]
        except KeyError:
            return None
    return dct

したがって、コードの残りの部分は比較的読みやすいままにすることができます。

safeget(example_dict, 'key1', 'key2')

37
そのため、pythonにはこのケースに対する美しい解決策はありません?:(
Arti

同様の実装で問題が発生しました。d = {key1:None}の場合、最初のgetはNoneを返し、それから例外が発生します):これの解決策を理解しようとしています
Huercio

1
このsafegetメソッドは、元の辞書を上書きするため、多くの点で安全ではありません。つまり、のようなことを安全に行うことはできませんsafeget(dct, 'a', 'b') or safeget(dct, 'a')
-neverfox

safeget元の辞書を上書きすることはありません。元の辞書、元の辞書の値、またはを返しNoneます。
unutbu

4
@KurtBourbaki:新しい値をローカル変数にdct = dct[key] 再度割り当てます dct。これは、元の辞書を変更しません(したがって、元の辞書はの影響を受けませんsafeget)。一方、dct[key] = ...使用されていた場合、元の辞書は変更されていたはずです。つまり、Pythonでは名前は値にバインドされます。名前への新しい値の割り当ては、古い値への参照がない場合を除いて、古い値に影響を与えません(その場合(CPythonでは)ガベージコレクションされます。)
unutbu

60

python reduceを使用することもできます:

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key) if d else None, keys, dictionary)

5
functoolsはPython3 の組み込みでなく、functoolsからインポートする必要があるため、このアプローチは少しエレガントではなくなっています。
yoniLavi 2018

3
このコメントへのわずかな修正:reduceはもはやPy3に組み込まれていません。しかし、これがなぜこれをエレガントさに欠けるのかわかりません。それはないワンライナーのために、それはあまり適していますが、ワンライナーであることは、自動的に「エレガント」であるとして何かを修飾するか失格しません。
PaulMcG

30

ここでこれらすべての答えと私が行った小さな変更を組み合わせることで、この機能は役立つと思います。安全、迅速、簡単に保守できます。

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

例:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>

1
Jinja2テンプレートに最適
Thomas

これは良い解決策ですが、欠点もあります。最初のキーが利用できない場合や、関数の辞書引数として渡された値が辞書でない場合でも、関数は最初の要素から最後の要素に移動します。基本的に、すべての場合にこれを行います。
アーセニー

1
deep_get({'a': 1}, "a.b")与えるNoneKeyError、何かのような例外を期待します。
stackunderflow

@edityouprofile。次に、戻り値をNoneto に変更するには、わずかな変更を加える必要がありますRaise KeyError
Yuda Prawira

15

より安全なアプローチであるYoavの答えに基づいて構築します。

def deep_get(dictionary, *keys):
    return reduce(lambda d, key: d.get(key, None) if isinstance(d, dict) else None, keys, dictionary)

12

再帰的なソリューション。最も効率的ではありませんが、他の例よりも読みやすく、functoolsに依存していません。

def deep_get(d, keys):
    if not keys or d is None:
        return d
    return deep_get(d.get(keys[0]), keys[1:])

d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code'])     # => 200
deep_get(d, ['garbage', 'status_code'])  # => None

より洗練されたバージョン

def deep_get(d, keys, default=None):
    """
    Example:
        d = {'meta': {'status': 'OK', 'status_code': 200}}
        deep_get(d, ['meta', 'status_code'])          # => 200
        deep_get(d, ['garbage', 'status_code'])       # => None
        deep_get(d, ['meta', 'garbage'], default='-') # => '-'
    """
    assert type(keys) is list
    if d is None:
        return default
    if not keys:
        return d
    return deep_get(d.get(keys[0]), keys[1:], default)

7

削減アプローチは簡潔で短いですが、単純なループの方が簡単に理解できます。デフォルトのパラメーターも含めました。

def deep_get(_dict, keys, default=None):
    for key in keys:
        if isinstance(_dict, dict):
            _dict = _dict.get(key, default)
        else:
            return default
    return _dict

リデュースワンライナーがどのように機能するかを理解するための練習として、次のことを行いました。しかし、結局のところ、ループアプローチは私にとってより直感的に思えます。

def deep_get(_dict, keys, default=None):

    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        return default

    return reduce(_reducer, keys, _dict)

使用法

nested = {'a': {'b': {'c': 42}}}

print deep_get(nested, ['a', 'b'])
print deep_get(nested, ['a', 'b', 'z', 'z'], default='missing')

5

ぜひお試しくださいpython-benedict

dictキーパスのサポートなどを提供するサブクラスです。

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

from benedict import benedict

example_dict = benedict(example_dict, keypath_separator='.')

これで、keypathを使用してネストされた値にアクセスできます。

val = example_dict['key1.key2']

# using 'get' method to avoid a possible KeyError:
val = example_dict.get('key1.key2')

またはキーリストを使用してネストされた値にアクセスします

val = example_dict['key1', 'key2']

# using get to avoid a possible KeyError:
val = example_dict.get(['key1', 'key2'])

GitHubで十分にテストされ、オープンソースです

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


@ perfecto25ありがとうございます!私はすぐに新機能をリリースする予定、😉ご期待
ファビオカッカモ

@ perfecto25インデックスのリストへのサポートを追加しました。d.get('a.b[0].c[-1]')
Fabio Caccamo

4

辞書をラップし、キーに基づいて取得できる単純なクラス:

class FindKey(dict):
    def get(self, path, default=None):
        keys = path.split(".")
        val = None

        for key in keys:
            if val:
                if isinstance(val, list):
                    val = [v.get(key, default) if v else None for v in val]
                else:
                    val = val.get(key, default)
            else:
                val = dict.get(self, key, default)

            if not val:
                break

        return val

例えば:

person = {'person':{'name':{'first':'John'}}}
FindDict(person).get('person.name.first') # == 'John'

キーが存在しない場合、Noneデフォルトで返されます。ラッパーのdefault=キーを使用してそれをオーバーライドできますFindDict-たとえば `:

FindDict(person, default='').get('person.name.last') # == doesn't exist, so ''

3

2番目のレベルのキーを取得するには、次のようにします。

key2_value = (example_dict.get('key1') or {}).get('key2')

2

属性を深く取得するためにこれを確認した後、dictドット表記を使用してネストされた値を安全に取得するために次のことを行いました。私dictsは逆シリアル化されたMongoDBオブジェクトなので、これは私にとってはうまくいき、キー名にが含まれていないことはわかっています.。また、私のコンテキストでNoneは、データにない偽のフォールバック値()を指定できるため、関数を呼び出すときにtry / exceptパターンを回避できます。

from functools import reduce # Python 3
def deepgetitem(obj, item, fallback=None):
    """Steps through an item chain to get the ultimate value.

    If ultimate value or path to value does not exist, does not raise
    an exception and instead returns `fallback`.

    >>> d = {'snl_final': {'about': {'_icsd': {'icsd_id': 1}}}}
    >>> deepgetitem(d, 'snl_final.about._icsd.icsd_id')
    1
    >>> deepgetitem(d, 'snl_final.about._sandbox.sbx_id')
    >>>
    """
    def getitem(obj, name):
        try:
            return obj[name]
        except (KeyError, TypeError):
            return fallback
    return reduce(getitem, item.split('.'), obj)

7
fallback関数では実際には使用されません。
153957

これは.
JW

obj [name]を呼び出すとき、なぜobj.get(name、fallback)ではなく、try-catchを避けます(try-catchが必要な場合は、Noneではなくfallbackを返します)
denvar

@ 153957に感謝します。それを私が直した。はい、@ JW、これは私のユースケースで機能します。sep=','キーワードargを追加して、特定の(9月、フォールバック)条件を一般化できます。そして、@ denvar は、reduceのシーケンスの後objの型の場合int、obj [name]はTypeErrorを発生させます。これは私がキャッチします。代わりにobj.get(name)またはobj.get(name、fallback)を使用した場合、AttributeErrorが発生するため、どちらの方法でもキャッチする必要があります。
Donny Winston、

1

同じことを行う別の関数も、キーが見つかったかどうかを表すブール値を返し、予期しないエラーを処理します。

'''
json : json to extract value from if exists
path : details.detail.first_name
            empty path represents root

returns a tuple (boolean, object)
        boolean : True if path exists, otherwise False
        object : the object if path exists otherwise None

'''
def get_json_value_at_path(json, path=None, default=None):

    if not bool(path):
        return True, json
    if type(json) is not dict :
        raise ValueError(f'json={json}, path={path} not supported, json must be a dict')
    if type(path) is not str and type(path) is not list:
        raise ValueError(f'path format {path} not supported, path can be a list of strings like [x,y,z] or a string like x.y.z')

    if type(path) is str:
        path = path.strip('.').split('.')
    key = path[0]
    if key in json.keys():
        return get_json_value_at_path(json[key], path[1:], default)
    else:
        return False, default

使用例:

my_json = {'details' : {'first_name' : 'holla', 'last_name' : 'holla'}}
print(get_json_value_at_path(my_json, 'details.first_name', ''))
print(get_json_value_at_path(my_json, 'details.phone', ''))

(真、 'holla')

(誤り、 '')



0

私が自分のコードで役立つとわかったunutbuの回答の改作:

example_dict.setdefaut('key1', {}).get('key2')

KeyErrorを回避するためにそのキーがまだない場合は、key1の辞書エントリを生成します。私がしたように、とにかくそのキーのペアを含むネストされた辞書を作成したい場合は、これが最も簡単な解決策のようです。


0

キーの1つが欠落している場合にキーエラーを発生させることは妥当なことなので、それをチェックしてそのように単一にすることもできません。

def get_dict(d, kl):
  cur = d[kl[0]]
  return get_dict(cur, kl[1:]) if len(kl) > 1 else cur

0

reduceリストで動作するようにするためのアプローチに少し改善。また、配列の代わりにドットで区切られた文字列としてデータパスを使用します。

def deep_get(dictionary, path):
    keys = path.split('.')
    return reduce(lambda d, key: d[int(key)] if isinstance(d, list) else d.get(key) if d else None, keys, dictionary)

0

私が使用したソリューションはdouble getに似ていますが、if elseロジックを使用してTypeErrorを回避する追加機能があります。

    value = example_dict['key1']['key2'] if example_dict.get('key1') and example_dict['key1'].get('key2') else default_value

ただし、辞書をネストするほど、これは煩雑になります。


0

ネストされた辞書/ JSONルックアップの場合、ディクターを使用できます

ピップインストールディクター

dictオブジェクト

{
    "characters": {
        "Lonestar": {
            "id": 55923,
            "role": "renegade",
            "items": [
                "space winnebago",
                "leather jacket"
            ]
        },
        "Barfolomew": {
            "id": 55924,
            "role": "mawg",
            "items": [
                "peanut butter jar",
                "waggy tail"
            ]
        },
        "Dark Helmet": {
            "id": 99999,
            "role": "Good is dumb",
            "items": [
                "Shwartz",
                "helmet"
            ]
        },
        "Skroob": {
            "id": 12345,
            "role": "Spaceballs CEO",
            "items": [
                "luggage"
            ]
        }
    }
}

Lonestarのアイテムを取得するには、ドットで区切られたパスを指定するだけです。

import json
from dictor import dictor

with open('test.json') as data: 
    data = json.load(data)

print dictor(data, 'characters.Lonestar.items')

>> [u'space winnebago', u'leather jacket']

キーがパスにない場合のフォールバック値を提供できます

大文字と小文字の区別を無視したり、 '。'以外の他の文字を使用するなど、実行できるオプションがたくさんあります。パスセパレータとして、

https://github.com/perfecto25/dictor


0

私はこの答えを少し変えました。数字のリストを使用しているかどうかのチェックを追加しました。これで、どの方法でも使用できます。deep_get(allTemp, [0], {})またはdeep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)など

def deep_get(_dict, keys, default=None):
    def _reducer(d, key):
        if isinstance(d, dict):
            return d.get(key, default)
        if isinstance(d, list):
            return d[key] if len(d) > 0 else default
        return default
    return reduce(_reducer, keys, _dict)

0

すでに良い答えはたくさんありますが、JavaScriptランドのlodash getに似たget呼ばれる関数を考え出しました。これは、インデックスによるリストへの到達もサポートしています。

def get(value, keys, default_value = None):
'''
    Useful for reaching into nested JSON like data
    Inspired by JavaScript lodash get and Clojure get-in etc.
'''
  if value is None or keys is None:
      return None
  path = keys.split('.') if isinstance(keys, str) else keys
  result = value
  def valid_index(key):
      return re.match('^([1-9][0-9]*|[0-9])$', key) and int(key) >= 0
  def is_dict_like(v):
      return hasattr(v, '__getitem__') and hasattr(v, '__contains__')
  for key in path:
      if isinstance(result, list) and valid_index(key) and int(key) < len(result):
          result = result[int(key)] if int(key) < len(result) else None
      elif is_dict_like(result) and key in result:
          result = result[key]
      else:
          result = default_value
          break
  return result

def test_get():
  assert get(None, ['foo']) == None
  assert get({'foo': 1}, None) == None
  assert get(None, None) == None
  assert get({'foo': 1}, []) == {'foo': 1}
  assert get({'foo': 1}, ['foo']) == 1
  assert get({'foo': 1}, ['bar']) == None
  assert get({'foo': 1}, ['bar'], 'the default') == 'the default'
  assert get({'foo': {'bar': 'hello'}}, ['foo', 'bar']) == 'hello'
  assert get({'foo': {'bar': 'hello'}}, 'foo.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.0.bar') == 'hello'
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1') == None
  assert get({'foo': [{'bar': 'hello'}]}, 'foo.1.bar') == None
  assert get(['foo', 'bar'], '1') == 'bar'
  assert get(['foo', 'bar'], '2') == None
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.