วิธีการที่ปลอดภัย Python เพื่อรับค่าของพจนานุกรมที่ซ้อนกัน


145

ฉันมีพจนานุกรมที่ซ้อนกัน มีวิธีเดียวเท่านั้นที่จะนำค่าออกมาใช้อย่างปลอดภัยหรือไม่?

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

หรือบางทีหลามอาจมีวิธีการที่เหมือนget()กันสำหรับพจนานุกรมซ้อนกัน?


ดูเพิ่มเติมที่: stackoverflow.com/questions/14692690/…
dreftymac

1
รหัสในคำถามของคุณคือในมุมมองของฉันแล้ววิธีที่ดีที่สุดในการรับค่าซ้อนจากพจนานุกรม คุณสามารถระบุค่าเริ่มต้นในexcept keyerror:ข้อได้เสมอ
Peter Schorn

คำตอบ:


281

คุณสามารถใช้getสองครั้ง:

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

สิ่งนี้จะส่งคืนNoneหากมีkey1หรือkey2ไม่มีอยู่

โปรดทราบว่าสิ่งนี้ยังสามารถเพิ่มAttributeErrorถ้าexample_dict['key1']มีอยู่ แต่ไม่ใช่ dict (หรือวัตถุที่คล้าย dict ด้วยgetเมธอด) 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เป็น subclass ของdictคุณสามารถใช้ Hasher dictในลักษณะเดียวกับที่คุณสามารถใช้ มีวิธีการและไวยากรณ์ที่เหมือนกันทั้งหมดที่มีอยู่ Hashers เพียงจัดการกับคีย์ที่หายไปต่างกัน

คุณสามารถแปลงค่าปกติdictเป็นแบบHasherนี้:

hasher = Hasher(example_dict)

และแปลงเป็นHasherค่าปกติ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
ดังนั้น python ไม่มีวิธีแก้ปัญหาที่สวยงามสำหรับกรณีนี้ :(
Arti

ฉันพบปัญหาในการใช้งานที่คล้ายกัน หากคุณมี d = {key1: ไม่มี} การรับครั้งแรกจะส่งคืน None จากนั้นคุณจะได้รับการยกเว้น): ฉันกำลังพยายามหาวิธีแก้ปัญหาสำหรับเรื่องนี้
Huercio

1
วิธีการอยู่ในหลายวิธีไม่ปลอดภัยมากตั้งแต่มันเขียนทับพจนานุกรมเดิมซึ่งหมายความว่าคุณไม่สามารถได้อย่างปลอดภัยทำสิ่งที่ชอบsafeget safeget(dct, 'a', 'b') or safeget(dct, 'a')
neverfox

4
@KurtBourbaki: dct = dct[key] reassignsค่าใหม่ให้กับตัวแปรท้องถิ่น dctสิ่งนี้จะไม่กลายพันธุ์ dict ดั้งเดิม (ดังนั้น dict ดั้งเดิมจะไม่ได้รับผลกระทบจากsafeget) ถ้าหากdct[key] = ...มีการใช้งานในทางกลับกัน dict เดิมจะได้รับการแก้ไข ในคำอื่น ๆ ในหลามชื่อจะผูกพันกับค่า การกำหนดค่าใหม่ให้กับชื่อจะไม่ส่งผลกระทบต่อมูลค่าเดิม (เว้นแต่จะไม่มีการอ้างอิงไปยังค่าเก่าซึ่งในกรณีนี้ (ใน CPython) จะได้รับการรวบรวมขยะ)
unutbu

1
safegetวิธีการยังจะล้มเหลวในกรณีสำคัญของ Dict ที่ซ้อนกันอยู่ แต่ค่าที่เป็นโมฆะ มันจะTypeError: 'NoneType' object is not subscriptableเข้าสู่รอบถัดไป
สแตนลีย์เอฟ

60

คุณสามารถใช้หลามลด :

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

5
แค่อยากพูดถึงฟังก์ชั่นการใช้งานนั้นไม่ใช่ตัวบิวอินใน Python3 อีกต่อไปและจำเป็นต้องนำเข้าจากฟังก์ชั่นเครื่องมือซึ่งทำให้วิธีการนี้ดูหรูหราน้อยลง
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

1
deep_get({'a': 1}, "a.b")ให้Noneแต่ฉันคาดหวังว่าจะมีข้อยกเว้นเช่นKeyErrorหรืออย่างอื่น
stackunderflow

@edityouprofile จากนั้นคุณเพียงแค่ต้องทำการปรับเปลี่ยนเล็กน้อยเพื่อเปลี่ยนค่าตอบแทนจากNoneเป็น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

โซลูชันแบบเรียกซ้ำ มันไม่ได้มีประสิทธิภาพมากที่สุด แต่ฉันคิดว่ามันสามารถอ่านได้มากกว่าตัวอย่างอื่น ๆ เล็กน้อยและไม่ได้พึ่งพาฟังก์ชั่น

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

ในการออกกำลังกายเพื่อทำความเข้าใจว่าการลดหนึ่งซับทำงานได้อย่างไรฉันทำสิ่งต่อไปนี้ แต่ในที่สุดวิธีการวนกลับดูเหมือนฉันจะเข้าใจง่ายขึ้น

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คลาสย่อยที่ให้การสนับสนุน keypath และอีกมากมาย

การติดตั้ง: 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 Caccamo

@ perfecto25 ฉันได้เพิ่มการสนับสนุนในรายการดัชนีเช่น d.get('a.b[0].c[-1]')
Fabio Caccamo

ดูเหมือนว่าฟังก์ชัน from_toml จะไม่ได้รับการติดตั้ง และการนำเข้าเบเนดิกต์อาจเป็นเรื่องยาก
DLyons

@DLyons คุณผิดในกรณีใด ๆ อย่าลังเลที่จะเปิดปัญหาใน GitHub
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=คีย์ในFindDictwrapper - ตัวอย่างเช่น:

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

3

สำหรับการดึงกุญแจระดับที่สองคุณสามารถทำได้:

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

2

หลังจากได้เห็นสิ่งนี้เพื่อรับคุณสมบัติที่ลึกล้ำฉันทำสิ่งต่อไปนี้เพื่อรับdictค่าที่ซ้อนกันอย่างปลอดภัยโดยใช้เครื่องหมายจุด สิ่งนี้ใช้งานได้สำหรับฉันเพราะฉันdictsเป็นวัตถุ MongoDB ที่ถูก deserialized ดังนั้นฉันรู้ว่าชื่อคีย์ไม่มี.s นอกจากนี้ในบริบทของฉันฉันสามารถระบุค่าทางเลือกที่ผิดพลาด ( None) ที่ฉันไม่มีในข้อมูลของฉันดังนั้นฉันสามารถหลีกเลี่ยงรูปแบบลอง / ยกเว้นเมื่อเรียกใช้ฟังก์ชัน

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 [ชื่อ] ทำไมไม่ obj.get (ชื่อสำรอง) และหลีกเลี่ยงการลองจับ (ถ้าคุณต้องการลองจับแล้วกลับสำรองไม่ได้ไม่มี)
denvar

ขอบคุณ @ 153957 ฉันซ่อมมัน. และใช่ @JW สิ่งนี้ใช้ได้กับกรณีการใช้งานของฉัน คุณสามารถเพิ่มsep=','คำหลักเพื่อหาข้อสรุปสำหรับเงื่อนไขที่กำหนด (sep, fallback) และ @denvar ถ้าobjพูดถึงชนิดintหลังจากลำดับของการลดแล้ว obj [ชื่อ] เพิ่ม TypeError ซึ่งฉันจับ ถ้าฉันใช้ obj.get (ชื่อ) หรือ obj.get (ชื่อทางเลือก) แทนมันจะยกระดับ 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')

มันสร้างรายการพจนานุกรมสำหรับ key1 หากไม่มีคีย์นั้นอยู่แล้วเพื่อให้คุณหลีกเลี่ยง KeyError หากคุณต้องการท้ายพจนานุกรมที่ซ้อนกันซึ่งรวมถึงการจับคู่คีย์นั้นเหมือนที่ฉันทำนี่เป็นวิธีที่ง่ายที่สุด


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 แต่มีความสามารถเพิ่มเติมเพื่อหลีกเลี่ยง TypeError โดยใช้ตรรกะอื่น:

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

อย่างไรก็ตามยิ่งมีพจนานุกรมซ้อนกันมากเท่าใด


0

สำหรับการค้นหาพจนานุกรม / JSON ที่ซ้อนกันคุณสามารถใช้ dictor

pip ติดตั้ง dictor

วัตถุ 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

มีคำตอบที่ดีอยู่แล้ว แต่ฉันได้ฟังก์ชั่นที่เรียกว่า getคล้ายกับ 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.