Phương pháp an toàn Python để lấy giá trị của từ điển lồng nhau


144

Tôi có một từ điển lồng nhau. Có cách nào để lấy giá trị ra một cách an toàn không?

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

Hoặc có thể python có một phương thức như get()cho từ điển lồng nhau?



1
Mã trong câu hỏi của bạn, theo quan điểm của tôi, đã là cách tốt nhất để đưa các giá trị lồng nhau ra khỏi từ điển. Bạn luôn có thể chỉ định một giá trị mặc định trong except keyerror:mệnh đề.
Peter Schorn

Câu trả lời:


280

Bạn có thể sử dụng gethai lần:

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

Điều này sẽ trở lại Nonenếu một trong hai key1hoặckey2 không tồn tại.

Lưu ý rằng điều này vẫn có thể tăng AttributeErrornếu example_dict['key1']tồn tại nhưng không phải là một dict (hoặc một đối tượng giống như dict với một getphương thức). Các try..exceptmã bạn đăng tải sẽ nâng cao một TypeErrorthay vì nếuexample_dict['key1'] là unsubscriptable.

Một điểm khác biệt là try...exceptngắn mạch ngay sau phím bị thiếu đầu tiên. Chuỗi getcuộc gọi không.


Nếu bạn muốn giữ nguyên cú pháp, example_dict['key1']['key2']nhưng không muốn nó tăng KeyErrors, thì bạn có thể sử dụng công thức 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'>

Lưu ý rằng điều này trả về một Hasher trống khi thiếu khóa.

Hasherlà một lớp con của dictbạn có thể sử dụng Hasher theo cách tương tự như bạn có thể sử dụngdict . Tất cả các phương thức và cú pháp giống nhau đều có sẵn, Hashers chỉ xử lý các khóa bị thiếu khác nhau.

Bạn có thể chuyển đổi thường xuyên dictthành Hashernhư thế này:

hasher = Hasher(example_dict)

và chuyển đổi Hasherthành một thông thường dictdễ dàng như sau:

regular_dict = dict(hasher)

Một cách khác là che giấu sự xấu xí trong chức năng của người trợ giúp:

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

Vì vậy, phần còn lại của mã của bạn có thể ở mức tương đối dễ đọc:

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

37
vì vậy, trăn không có giải pháp đẹp cho trường hợp này? :(
Arti

Tôi gặp phải một vấn đề với việc thực hiện tương tự. Nếu bạn có d = {key1: Không}, lần nhận đầu tiên sẽ trả về Không và sau đó bạn sẽ có ngoại lệ): Tôi đang cố gắng tìm ra giải pháp cho việc này
Huercio

1
Các safegetphương pháp là trong rất nhiều cách khác nhau không phải là rất an toàn vì nó ghi đè điển gốc, có nghĩa là bạn không thể làm một cách an toàn những thứ như safeget(dct, 'a', 'b') or safeget(dct, 'a').
neverfox

safegetkhông bao giờ ghi đè từ điển gốc. Nó sẽ trả về từ điển gốc, một giá trị từ từ điển gốc hoặc None.
unutbu

4
@KurtBourbaki: dct = dct[key] gán lại một giá trị mới cho biến cục bộ dct . Điều này không làm thay đổi chính tả ban đầu (do đó, chính tả ban đầu không bị ảnh hưởng bởi safeget.) Nếu, mặt khác, dct[key] = ...đã được sử dụng, thì chính tả ban đầu sẽ bị sửa đổi. Nói cách khác, trong tên Python bị ràng buộc với các giá trị . Việc gán giá trị mới cho tên không ảnh hưởng đến giá trị cũ (trừ khi không có thêm tham chiếu đến giá trị cũ, trong trường hợp đó (trong CPython), nó sẽ nhận được rác được thu thập.)
unutbu

60

Bạn cũng có thể sử dụng python giảm :

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

5
Chỉ muốn đề cập rằng funcools không còn được tích hợp sẵn trong Python3 và cần được nhập từ funcools, điều này làm cho cách tiếp cận này hơi kém thanh lịch.
yoniLavi

3
Điều chỉnh nhẹ cho nhận xét này: giảm không còn được tích hợp trong Py3. Nhưng tôi không thấy lý do tại sao điều này làm cho điều này ít thanh lịch hơn. Nó không làm cho nó ít thích hợp cho một lớp lót, nhưng là một lớp lót không tự động đủ điều kiện hoặc không đủ điều kiện một cái gì đó như là "thanh lịch".
PaulMcG

30

Bằng cách kết hợp tất cả các câu trả lời ở đây và những thay đổi nhỏ mà tôi đã thực hiện, tôi nghĩ chức năng này sẽ hữu ích. nó an toàn, nhanh chóng, dễ bảo trì.

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)

Thí dụ :

>>> 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
Hoàn hảo cho các mẫu Jinja2
Thomas

Đây là một giải pháp tốt mặc dù cũng có một nhược điểm: ngay cả khi khóa đầu tiên không có sẵn hoặc giá trị được chuyển làm đối số từ điển cho hàm không phải là từ điển, hàm sẽ chuyển từ phần tử đầu tiên sang phần tử cuối cùng. Về cơ bản, nó làm điều này trong mọi trường hợp.
Asen

1
deep_get({'a': 1}, "a.b")cho Nonenhưng tôi sẽ mong đợi một ngoại lệ như KeyErrorhoặc một cái gì đó khác.
stackunderflow 27/2/19

@edityouprofile. sau đó bạn chỉ cần thực hiện sửa đổi nhỏ để thay đổi giá trị trả về từ NonethànhRaise KeyError
Yuda Prawira

15

Dựa trên câu trả lời của Yoav, một cách tiếp cận thậm chí an toàn hơn:

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

12

Một giải pháp đệ quy. Nó không phải là hiệu quả nhất nhưng tôi thấy nó dễ đọc hơn một chút so với các ví dụ khác và nó không dựa vào funcools.

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

Thí dụ

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

Một phiên bản bóng bẩy hơn

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

Trong khi phương pháp rút gọn là gọn gàng và ngắn gọn, tôi nghĩ rằng một vòng lặp đơn giản sẽ dễ dàng hơn để tìm kiếm. Tôi cũng đã bao gồm một tham số mặc định.

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

Như một bài tập để hiểu làm thế nào giảm một lớp lót hoạt động, tôi đã làm như sau. Nhưng cuối cùng, cách tiếp cận vòng lặp có vẻ trực quan hơn đối với tôi.

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)

Sử dụng

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

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

5

Tôi đề nghị bạn thử python-benedict.

Nó là một dictlớp con cung cấp hỗ trợ keypath và nhiều hơn nữa.

Cài đặt: pip install python-benedict

from benedict import benedict

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

bây giờ bạn có thể truy cập các giá trị lồng nhau bằng cách sử dụng keypath :

val = example_dict['key1.key2']

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

hoặc truy cập các giá trị lồng nhau bằng cách sử dụng danh sách khóa :

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

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

Nó được kiểm tra tốt và mã nguồn mở trên GitHub :

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


@ perfecto25 cảm ơn bạn! Tôi sẽ sớm phát hành các tính năng mới, hãy theo dõi
Fabio Caccamo

@ perfecto25 Tôi đã thêm hỗ trợ vào danh sách chỉ mục, vd. d.get('a.b[0].c[-1]')
Fabio Caccamo

4

Một lớp đơn giản có thể bao bọc một lệnh và truy xuất dựa trên khóa:

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

Ví dụ:

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

Nếu khóa không tồn tại, nó sẽ trả về Nonetheo mặc định. Bạn có thể ghi đè bằng cách sử dụng một default=khóa trong FindDicttrình bao bọc - ví dụ`:

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

3

để lấy khóa cấp hai, bạn có thể làm điều này:

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

2

Sau khi thấy điều này để nhận được các thuộc tính sâu sắc, tôi đã thực hiện như sau để có được các dictgiá trị lồng nhau một cách an toàn bằng cách sử dụng ký hiệu dấu chấm. Điều này làm việc cho tôi vì dictscác đối tượng MongoDB đã được khử lưu huỳnh của tôi , vì vậy tôi biết các tên khóa không chứa .s. Ngoài ra, trong ngữ cảnh của tôi, tôi có thể chỉ định giá trị dự phòng giả ( None) mà tôi không có trong dữ liệu của mình, vì vậy tôi có thể tránh mẫu thử / ngoại trừ khi gọi hàm.

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
fallbackkhông thực sự được sử dụng trong hàm.
153957

Lưu ý rằng điều này không hoạt động đối với các khóa có chứa.
JW.

Khi chúng tôi gọi obj [name] tại sao không obj.get (tên, dự phòng) và tránh thử bắt (nếu bạn muốn thử bắt, sau đó quay lại dự phòng, không phải là Không)
denvar

Cảm ơn @ 153957. Tôi sửa nó rồi. Và có @JW, điều này hoạt động cho trường hợp sử dụng của tôi. Bạn có thể thêm một sep=','từ khóa arg để khái quát hóa cho các điều kiện (sep, dự phòng) nhất định. Và @denvar, nếuobj nói về loại intsau một chuỗi giảm, thì obj [name] sẽ tăng TypeError, mà tôi bắt được. Thay vào đó, nếu tôi sử dụng obj.get (tên) hoặc obj.get (tên, dự phòng), nó sẽ tăng AttributionError, do đó, dù sao tôi cũng cần phải nắm bắt.
Donny Winston

1

Tuy nhiên, một chức năng khác cho điều tương tự, cũng trả về một boolean để thể hiện xem khóa có được tìm thấy hay không và xử lý một số lỗi không mong muốn.

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

sử dụng ví dụ:

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

(Đúng, 'holla')

(Sai, '')



0

Một bản phóng tác của câu trả lời của unutbu mà tôi thấy hữu ích trong mã của riêng mình:

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

Nó tạo ra một mục từ điển cho key1 nếu nó chưa có khóa đó để bạn tránh KeyError. Nếu bạn muốn kết thúc một từ điển lồng nhau bao gồm cả việc ghép khóa đó như tôi đã làm, thì đây có vẻ là giải pháp đơn giản nhất.


0

Vì việc đưa ra một lỗi khóa nếu thiếu một trong các khóa là điều hợp lý, chúng tôi thậm chí không thể kiểm tra và nhận nó như một:

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

0

Cải thiện ít để reducetiếp cận để làm cho nó hoạt động với danh sách. Cũng sử dụng đường dẫn dữ liệu dưới dạng chuỗi chia cho các dấu chấm thay vì mảng.

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

Một giải pháp tôi đã sử dụng tương tự như nhận kép nhưng với khả năng bổ sung để tránh TypeError sử dụng nếu logic khác:

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

Tuy nhiên, từ điển càng lồng nhau thì điều này càng trở nên cồng kềnh.


0

Đối với tra cứu từ điển / JSON lồng nhau, bạn có thể sử dụng dictor

Pip cài đặt độc tài

đối tượng chính tả

{
    "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"
            ]
        }
    }
}

để có được các vật phẩm của Lonestar, chỉ cần cung cấp một đường dẫn được phân tách bằng dấu chấm, nghĩa là

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']

bạn có thể cung cấp giá trị dự phòng trong trường hợp khóa không có trong đường dẫn

Có rất nhiều tùy chọn bạn có thể làm, như bỏ qua vỏ chữ cái và sử dụng các ký tự khác ngoài '.' như một đường phân cách

https://github.com/perinfo25/dictor


0

Tôi ít thay đổi câu trả lời này . Tôi đã thêm kiểm tra nếu chúng tôi sử dụng danh sách với các số. Vì vậy, bây giờ chúng ta có thể sử dụng nó bất cứ cách nào. deep_get(allTemp, [0], {})hoặc deep_get(getMinimalTemp, [0, minimalTemperatureKey], 26)v.v.

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

Đã có rất nhiều câu trả lời hay nhưng tôi đã đưa ra một hàm gọi là get tương tự như lodash get trong vùng đất JavaScript cũng hỗ trợ tiếp cận danh sách theo chỉ mục:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.