중첩 된 사전의 가치를 얻는 파이썬 안전 방법


145

중첩 된 사전이 있습니다. 안전하게 가치를 얻는 방법은 하나뿐입니까?

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

아니면 파이썬에는 get()중첩 사전 과 같은 방법이 있습니까?



1
귀하의 질문에 코드는 이미 내 사전에 중첩 된 값을 얻는 가장 좋은 방법입니다. except keyerror:절 에서 항상 기본값을 지정할 수 있습니다 .
Peter Schorn

답변:


281

get두 번 사용할 수 있습니다 .

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

존재 하거나 존재하지 않으면 반환 None됩니다 .key1key2

이것은 여전히 존재하지만 dict (또는 메소드가 있는 dict-like 객체)가 아닌 AttributeError경우 발생할 수 있습니다 . 설명 할 수없는 경우 게시 한 코드가 대신 나타납니다 .example_dict['key1']gettry..exceptTypeErrorexample_dict['key1']

또 다른 차이점은 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를 사용할 수있는 것과 거의 같은 방식으로 Hasher를 사용할 수 있습니다 dict. 동일한 방법과 구문을 모두 사용할 수 있으며 해셔는 누락 된 키를 다르게 취급합니다.

정규식 dict을 다음 Hasher과 같이 변환 할 수 있습니다 .

hasher = Hasher(example_dict)

쉽게 Hashera를 일반으로 변환하십시오 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')

38
따라서 파이썬은이 경우에 대한 아름다운 해결책을 가지고 있지 않습니까? :(
Arti

비슷한 구현에 문제가 발생했습니다. d = {key1 : None} 인 경우 첫 번째 get은 None을 반환하고 예외가 발생합니다) : 이에 대한 해결책을
찾으려고

1
safeget방법은 원래 사전을 덮어 쓰기 때문에 매우 안전하지 않은 방법으로 여러 가지가 있습니다. 이는 다음과 같은 작업을 안전하게 수행 할 수 없음을 의미합니다 safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox

4
@KurtBourbaki : 새로운 값을 지역 변수에 dct = dct[key] 다시 할당 합니다 dct . 이렇게하면 원래 dict이 변경되지 않습니다. 따라서 원래 dict는 영향을받지 않습니다 safeget. 반면에 dct[key] = ...dict가 사용 된 경우 원래 dict가 수정되었을 것입니다. 즉, 파이썬에서 이름은 values에 바인딩됩니다 . 이름에 새 값을 할당해도 이전 값에 영향을 미치지 않습니다 (이전 값에 대한 참조가 더 이상없는 경우 (CPython에서 가비지 수집 됨))
unutbu

1
safeget또한 경우에 중첩 딕셔너리의 키가 실패 할 방법은 존재하지만,이 값은 널 (null)이다. TypeError: 'NoneType' object is not subscriptable다음 번 반복에 던져 질 것입니다
Stanley F.

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

3
이 의견에 대한 약간의 수정 : 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

첫 번째 키를 사용할 수 없거나 함수에 사전 인수로 전달 된 값이 사전이 아닌 경우에도 함수는 첫 번째 요소에서 마지막 요소로 이동합니다. 기본적으로 모든 경우에이 작업을 수행합니다.
Arseny 2012

1
deep_get({'a': 1}, "a.b")제공 None하지만 나는 같은 예외 기대 KeyError또는 뭔가.
stackunderflow

@edityouprofile. 그런 다음 반환 값을 다음으로 변경하기 위해 작은 수정 NoneRaise KeyError
만하면

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)

8

감소 접근 방식은 깔끔하고 짧지 만 간단한 루프가 더 쉽다고 생각합니다. 기본 매개 변수도 포함했습니다.

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

one-liner 감소 작동 방식을 이해하기위한 연습으로 다음을 수행했습니다. 그러나 궁극적으로 루프 접근법은 더 직관적 인 것처럼 보입니다.

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 감사합니다! 곧 새로운 기능을 출시하겠습니다. 계속 지켜봐 주시기 바랍니다
Fabio

@ perfecto25 예를 들어 색인을 나열하는 지원을 추가했습니다. d.get('a.b[0].c[-1]')
Fabio Caccamo

from_toml 함수가 구현되지 않은 것 같습니다. BeneDict를 가져 오기가 어려울 수 있습니다.
DLyons

@DLyons는 틀 렸습니다. 어쨌든 GitHub에서 문제를 자유롭게여십시오.
Fabio Caccamo

4

dict을 감싸고 키를 기반으로 검색 할 수있는 간단한 클래스입니다.

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

두 번째 레벨 키 검색의 경우 다음을 수행 할 수 있습니다.

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

2

속성을 깊이 얻는 것에 대해 이것을 본 후에 dict도트 표기법을 사용하여 중첩 된 값 을 안전하게 얻도록 다음을 만들었습니다 . dictsdeserialized MongoDB 객체 이기 때문에 키 이름에 .s가 포함되어 있지 않기 때문에 이것이 효과적입니다 . 또한 내 맥락 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를 원한다면, fallback, None을 반환하지 마십시오)
denvar

감사합니다 @ 153957. 나는 그것을 고쳤다. 그리고 예 @JW, 이것은 내 유스 케이스에서 작동합니다. sep=','주어진 (sep, fallback) 조건에 대해 일반화 할 키워드 인수를 추가 할 수 있습니다 . 그리고 @denvar 는 축소 시퀀스 후에 objtype이라고 하면 intobj [name]은 TypeError를 발생시킵니다. obj.get (name) 또는 obj.get (name, fallback)을 대신 사용하면 AttributeError가 발생하므로 잡을 필요가 있습니다.
도니 윈스턴

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', ''))

(참, '홀라')

(거짓, '')



0

내 코드에서 유용하다고 생각한 unutbu의 답변에 대한 적응 :

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

KeyError를 피하기 위해 해당 키가없는 경우 key1에 대한 사전 항목을 생성합니다. 어쨌든 그 키 쌍을 포함하는 중첩 된 사전을 끝내려면 이것이 가장 쉬운 해결책 인 것 같습니다.


0

키 중 하나가 누락 된 경우 키 오류를 발생시키는 것이 합리적이므로 키를 확인하고 단일 키를 얻을 수도 없습니다.

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이라는 함수 를 생각해 냈습니다.

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.