เข้าถึงรายการพจนานุกรมที่ซ้อนกันผ่านรายการคีย์หรือไม่


143

ฉันมีโครงสร้างพจนานุกรมที่ซับซ้อนซึ่งฉันต้องการเข้าถึงผ่านรายการปุ่มเพื่อระบุรายการที่ถูกต้อง

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}    

maplist = ["a", "r"]

หรือ

maplist = ["b", "v", "y"]

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

# Get a given data from a dictionary with position provided as a list
def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

# Set a given data in a dictionary with position provided as a list
def setInDict(dataDict, mapList, value): 
    for k in mapList[:-1]: dataDict = dataDict[k]
    dataDict[mapList[-1]] = value

คำตอบ:


230

ใช้reduce()เพื่อสำรวจพจนานุกรม:

from functools import reduce  # forward compatibility for Python 3
import operator

def getFromDict(dataDict, mapList):
    return reduce(operator.getitem, mapList, dataDict)

และนำมาใช้ซ้ำgetFromDictเพื่อค้นหาตำแหน่งเพื่อเก็บค่าสำหรับsetInDict():

def setInDict(dataDict, mapList, value):
    getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value

ทั้งหมดยกเว้นองค์ประกอบสุดท้ายในmapListการค้นหาพจนานุกรม 'พาเรนต์' เพื่อเพิ่มค่าไปยังจากนั้นใช้องค์ประกอบสุดท้ายเพื่อตั้งค่าเป็นคีย์ขวา

การสาธิต:

>>> getFromDict(dataDict, ["a", "r"])
1
>>> getFromDict(dataDict, ["b", "v", "y"])
2
>>> setInDict(dataDict, ["b", "v", "w"], 4)
>>> import pprint
>>> pprint.pprint(dataDict)
{'a': {'r': 1, 's': 2, 't': 3},
 'b': {'u': 1, 'v': {'w': 4, 'x': 1, 'y': 2, 'z': 3}, 'w': 3}}

โปรดสังเกตว่างูหลาม PEP8 สไตล์คู่มือกำหนด snake_case ชื่อสำหรับฟังก์ชั่น รายการด้านบนใช้งานได้ดีสำหรับรายการหรือพจนานุกรมและรายการต่าง ๆ ดังนั้นชื่อควรจะเป็นget_by_path()และset_by_path():

from functools import reduce  # forward compatibility for Python 3
import operator

def get_by_path(root, items):
    """Access a nested object in root by item sequence."""
    return reduce(operator.getitem, items, root)

def set_by_path(root, items, value):
    """Set a value in a nested object in root by item sequence."""
    get_by_path(root, items[:-1])[items[-1]] = value

1
การสำรวจภายในมีความน่าเชื่อถือเพียงใดสำหรับโครงสร้างที่ซ้อนกันตามอำเภอใจ? มันจะใช้ได้กับพจนานุกรมที่มีรายการซ้อนกันหรือไม่ ฉันจะแก้ไข getFromDict () เพื่อให้ default_value และมี default_value เริ่มต้นเป็น None ได้อย่างไร ฉันเป็นสามเณรใน Python ที่มีการพัฒนา PHP มาหลายปีและก่อนการพัฒนา C
Dmitriy Sintsov

2
ชุดที่แมปที่ซ้อนกันควรสร้างโหนดที่ไม่มีอยู่ imo: รายการสำหรับคีย์จำนวนเต็มพจนานุกรมสำหรับคีย์สตริง
Dmitriy Sintsov

1
@ user1353510: เมื่อมันเกิดขึ้นจะมีการใช้ไวยากรณ์การทำดัชนีปกติที่นี่ดังนั้นมันจะรองรับรายการในพจนานุกรมด้วย เพียงผ่านดัชนีจำนวนเต็มสำหรับสิ่งเหล่านั้น
Martijn Pieters

1
@ user1353510: ค่าเริ่มต้นการใช้งานtry:, except (KeyError, IndexError): return default_valueรอบปัจจุบันreturnสาย
Martijn Pieters

1
@Georgy: ใช้dict.get()การเปลี่ยนแปลงซีแมนทิกส์ซึ่งจะส่งคืนNoneแทนที่จะเพิ่มKeyErrorสำหรับชื่อที่ขาด ชื่อที่ตามมาใด ๆ AttributeErrorแล้วเรียก operatorเป็นไลบรารีมาตรฐานไม่จำเป็นต้องหลีกเลี่ยงที่นี่
Martijn Pieters

40
  1. วิธีการแก้ปัญหาได้รับการยอมรับจะไม่ทำงานโดยตรง python3 - from functools import reduceมันจะต้องมี
  2. นอกจากนี้ยังดูเหมือน pythonic เพิ่มเติมที่จะใช้forวน ดูอ้างจากอะไรใหม่ในหลาม 3.0

    ลบออกreduce()แล้ว ใช้functools.reduce()ถ้าคุณต้องการจริงๆ อย่างไรก็ตาม 99 เปอร์เซ็นต์ของเวลาที่forวนลูปชัดเจนสามารถอ่านได้มากขึ้น

  3. ถัดไปโซลูชันที่ยอมรับไม่ได้ตั้งค่าคีย์ที่ซ้อนกันที่ไม่มีอยู่ (จะส่งคืนกKeyError) - ดูคำตอบของ @ eafit เพื่อหาวิธีแก้ไข

ดังนั้นทำไมไม่ใช้วิธีการที่แนะนำจากคำถามของ kolergy เพื่อรับค่า:

def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

และรหัสจากคำตอบของ @ eafit สำหรับการตั้งค่า:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

ทั้งสองทำงานตรงในหลาม 2 และ 3


6
ฉันชอบวิธีนี้ - แต่ระวัง ถ้าฉันไม่ผิดตั้งแต่พจนานุกรมหลามไม่ได้เปลี่ยนรูปมีศักยภาพในการทำลายของผู้โทรgetFromDict dataDictฉันจะcopy.deepcopy(dataDict)ก่อน แน่นอนว่าพฤติกรรมนี้เป็นที่ต้องการในฟังก์ชั่นที่สอง
Dylan F

15

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

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

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

นอกจากนี้ยังสามารถสะดวกในการมีวิธีที่ลัดต้นไม้และได้รับเส้นทางที่สำคัญทั้งหมดที่ฉันได้สร้าง:

def keysInDict(dataDict, parent=[]):
    if not isinstance(dataDict, dict):
        return [tuple(parent)]
    else:
        return reduce(list.__add__, 
            [keysInDict(v,parent+[k]) for k,v in dataDict.items()], [])

การใช้งานอย่างหนึ่งของมันคือการแปลงแผนผังที่ซ้อนกันเป็น DataFrame แพนด้าโดยใช้รหัสต่อไปนี้ (สมมติว่าใบไม้ทั้งหมดในพจนานุกรมที่ซ้อนกันมีความลึกเท่ากัน)

def dict_to_df(dataDict):
    ret = []
    for k in keysInDict(dataDict):
        v = np.array( getFromDict(dataDict, k), )
        v = pd.DataFrame(v)
        v.columns = pd.MultiIndex.from_product(list(k) + [v.columns])
        ret.append(v)
    return reduce(pd.DataFrame.join, ret)

ทำไมพล จำกัด ระยะเวลาในการโต้แย้ง 'คีย์' 2 หรือมากกว่าnested_set?
alancalvitti

10

ห้องสมุดนี้อาจมีประโยชน์: https://github.com/akesterson/dpath-python

ห้องสมุดไพ ธ อนสำหรับการเข้าถึงและค้นหาพจนานุกรมผ่าน / slashed / path ala xpath

โดยทั่วไปจะช่วยให้คุณกลมกว่าพจนานุกรมราวกับว่ามันเป็นระบบไฟล์


3

วิธีการใช้ฟังก์ชั่นวนซ้ำ?

ในการรับค่า:

def getFromDict(dataDict, maplist):
    first, rest = maplist[0], maplist[1:]

    if rest: 
        # if `rest` is not empty, run the function recursively
        return getFromDict(dataDict[first], rest)
    else:
        return dataDict[first]

และเพื่อตั้งค่า:

def setInDict(dataDict, maplist, value):
    first, rest = maplist[0], maplist[1:]

    if rest:
        try:
            if not isinstance(dataDict[first], dict):
                # if the key is not a dict, then make it a dict
                dataDict[first] = {}
        except KeyError:
            # if key doesn't exist, create one
            dataDict[first] = {}

        setInDict(dataDict[first], rest, value)
    else:
        dataDict[first] = value

2

สไตล์ Pure Python โดยไม่ต้องนำเข้า:

def nested_set(element, value, *keys):
    if type(element) is not dict:
        raise AttributeError('nested_set() expects dict as first argument.')
    if len(keys) < 2:
        raise AttributeError('nested_set() expects at least three arguments, not enough given.')

    _keys = keys[:-1]
    _element = element
    for key in _keys:
        _element = _element[key]
    _element[keys[-1]] = value

example = {"foo": { "bar": { "baz": "ok" } } }
keys = ['foo', 'bar']
nested_set(example, "yay", *keys)
print(example)

เอาท์พุต

{'foo': {'bar': 'yay'}}

2

ทางเลือกอื่นหากคุณไม่ต้องการให้เกิดข้อผิดพลาดหากปุ่มใดปุ่มหนึ่งขาด (เพื่อให้รหัสหลักของคุณสามารถทำงานได้โดยไม่หยุดชะงัก):

def get_value(self,your_dict,*keys):
    curr_dict_ = your_dict
    for k in keys:
        v = curr_dict.get(k,None)
        if v is None:
            break
        if isinstance(v,dict):
            curr_dict = v
    return v

ในกรณีนี้หากไม่มีคีย์อินพุตใด ๆ จะไม่มีการส่งคืนซึ่งสามารถใช้เป็นการตรวจสอบในรหัสหลักของคุณเพื่อดำเนินการทางเลือกอื่น


1

แทนที่จะบันทึกการแสดงผลในแต่ละครั้งที่คุณต้องการค้นหาค่าคุณจะทำให้พจนานุกรมแบนราบเพียงครั้งเดียวแล้วค้นหาคีย์เช่น b:v:y

def flatten(mydict):
  new_dict = {}
  for key,value in mydict.items():
    if type(value) == dict:
      _dict = {':'.join([key, _key]):_value for _key, _value in flatten(value).items()}
      new_dict.update(_dict)
    else:
      new_dict[key]=value
  return new_dict

dataDict = {
"a":{
    "r": 1,
    "s": 2,
    "t": 3
    },
"b":{
    "u": 1,
    "v": {
        "x": 1,
        "y": 2,
        "z": 3
    },
    "w": 3
    }
}    

flat_dict = flatten(dataDict)
print flat_dict
{'b:w': 3, 'b:u': 1, 'b:v:y': 2, 'b:v:x': 1, 'b:v:z': 3, 'a:r': 1, 'a:s': 2, 'a:t': 3}

วิธีนี้คุณสามารถค้นหารายการโดยใช้flat_dict['b:v:y']ซึ่งจะให้คุณ1ซึ่งจะทำให้คุณ

และแทนที่จะสำรวจพจนานุกรมในการค้นหาแต่ละครั้งคุณอาจเพิ่มความเร็วได้โดยการทำให้พจนานุกรมแบนและบันทึกผลลัพธ์เพื่อให้การค้นหาตั้งแต่เริ่มเย็นจะหมายถึงการโหลดพจนานุกรมที่แบนแล้วทำการค้นหาคีย์ / ค่าโดยไม่มี สำรวจเส้นทาง


1

แก้ปัญหาด้วยการเรียกซ้ำ:

def get(d,l):
    if len(l)==1: return d[l[0]]
    return get(d[l[0]],l[1:])

ใช้ตัวอย่างของคุณ:

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}
maplist1 = ["a", "r"]
maplist2 = ["b", "v", "y"]
print(get(dataDict, maplist1)) # 1
print(get(dataDict, maplist2)) # 2

1

วิธีการเกี่ยวกับการตรวจสอบแล้วกำหนดองค์ประกอบ dict โดยไม่ประมวลผลดัชนีทั้งหมดสองครั้ง?

สารละลาย:

def nested_yield(nested, keys_list):
    """
    Get current nested data by send(None) method. Allows change it to Value by calling send(Value) next time
    :param nested: list or dict of lists or dicts
    :param keys_list: list of indexes/keys
    """
    if not len(keys_list):  # assign to 1st level list
        if isinstance(nested, list):
            while True:
                nested[:] = yield nested
        else:
            raise IndexError('Only lists can take element without key')


    last_key = keys_list.pop()
    for key in keys_list:
        nested = nested[key]

    while True:
        try:
            nested[last_key] = yield nested[last_key]
        except IndexError as e:
            print('no index {} in {}'.format(last_key, nested))
            yield None

ตัวอย่างเวิร์กโฟลว์:

ny = nested_yield(nested_dict, nested_address)
data_element = ny.send(None)
if data_element:
    # process element
    ...
else:
    # extend/update nested data
    ny.send(new_data_element)
    ...
ny.close()

ทดสอบ

>>> cfg= {'Options': [[1,[0]],[2,[4,[8,16]]],[3,[9]]]}
    ny = nested_yield(cfg, ['Options',1,1,1])
    ny.send(None)
[8, 16]
>>> ny.send('Hello!')
'Hello!'
>>> cfg
{'Options': [[1, [0]], [2, [4, 'Hello!']], [3, [9]]]}
>>> ny.close()

1

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

Dictเป็นพจนานุกรมที่มีค่าของเรา

listคือรายการของ "ขั้นตอน" ต่อคุณค่าของเรา

def getnestedvalue(dict, list):

    length = len(list)
    try:
        for depth, key in enumerate(list):
            if depth == length - 1:
                output = dict[key]
                return output
            dict = dict[key]
    except (KeyError, TypeError):
        return None

    return None

1

การเห็นคำตอบเหล่านี้น่าพอใจสำหรับการมีสองวิธีแบบคงที่สำหรับการตั้งค่าและรับแอตทริบิวต์แบบซ้อน วิธีแก้ปัญหาเหล่านี้ดีกว่าการใช้ต้นไม้ซ้อนกัน https://gist.github.com/hrldcpr/2012250

นี่คือการดำเนินการของฉัน

การใช้ :

หากต้องการตั้งค่าการเรียกแอตทริบิวต์ที่ซ้อนกัน sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

เพื่อรับการเรียกแอตทริบิวต์ที่ซ้อนกัน gattr(my_dict, 1, 2)

def gattr(d, *attrs):
    """
    This method receives a dict and list of attributes to return the innermost value of the give dict       
    """
    try:
        for at in attrs:
            d = d[at]
        return d
    except(KeyError, TypeError):
        return None


def sattr(d, *attrs):
    """
    Adds "val" to dict in the hierarchy mentioned via *attrs
    For ex:
    sattr(animals, "cat", "leg","fingers", 4) is equivalent to animals["cat"]["leg"]["fingers"]=4
    This method creates necessary objects until it reaches the final depth
    This behaviour is also known as autovivification and plenty of implementation are around
    This implementation addresses the corner case of replacing existing primitives
    https://gist.github.com/hrldcpr/2012250#gistcomment-1779319
    """
    for attr in attrs[:-2]:
        if type(d.get(attr)) is not dict:
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]

1

ฉันแนะนำให้คุณใช้python-benedictเพื่อเข้าถึงรายการที่ซ้อนอยู่โดยใช้ keypath

ติดตั้งโดยใช้pip:

pip install python-benedict

แล้ว:

from benedict import benedict

dataDict = benedict({
    "a":{
        "r": 1,
        "s": 2,
        "t": 3,
    },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3,
        },
        "w": 3,
    },
}) 

print(dataDict['a.r'])
# or
print(dataDict['a', 'r'])

นี่คือเอกสารฉบับเต็ม: https://github.com/fabiocaccamo/python-benedict


0

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

from functools import reduce


def get_furthest(s, path):
    '''
    Gets the furthest value along a given key path in a subscriptable structure.

    subscriptable, list -> any
    :param s: the subscriptable structure to examine
    :param path: the lookup path to follow
    :return: a tuple of the value at the furthest valid key, and whether the full path is valid
    '''

    def step_key(acc, key):
        s = acc[0]
        if isinstance(s, str):
            return (s, False)
        try:
            return (s[key], acc[1])
        except LookupError:
            return (s, False)

    return reduce(step_key, path, (s, True))


def get_val(s, path):
    val, successful = get_furthest(s, path)
    if successful:
        return val
    else:
        raise LookupError('Invalid lookup path: {}'.format(path))


def set_val(s, path, value):
    get_val(s, path[:-1])[path[-1]] = value

0

วิธีการเชื่อมสตริง:

def get_sub_object_from_path(dict_name, map_list):
    for i in map_list:
        _string = "['%s']" % i
        dict_name += _string
    value = eval(dict_name)
    return value
#Sample:
_dict = {'new': 'person', 'time': {'for': 'one'}}
map_list = ['time', 'for']
print get_sub_object_from_path("_dict",map_list)
#Output:
#one

0

ขยาย @DomTomCat และคนอื่น ๆ วิธีการทำงานเหล่านี้ (เช่นกลับมาแก้ไขข้อมูลผ่าน deepcopy โดยไม่มีผลต่อการป้อนข้อมูล) หมาและผลงาน mapper สำหรับซ้อนกันและ dictlist

หมา:

def set_at_path(data0, keys, value):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(set_by_path(v,keys[1:],value) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [set_by_path(x[1],keys[1:],value) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=value
        return data

mapper:

def map_at_path(data0, keys, f):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(map_at_path(v,keys[1:],f) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [map_at_path(x[1],keys[1:],f) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=f(data[keys[-1]])
        return data

0

คุณสามารถใช้ประโยชน์จากevalฟังก์ชั่นในหลาม

def nested_parse(nest, map_list):
    nestq = "nest['" + "']['".join(map_list) + "']"
    return eval(nestq, {'__builtins__':None}, {'nest':nest})

คำอธิบาย

สำหรับแบบสอบถามตัวอย่างของคุณ: maplist = ["b", "v", "y"]

nestqจะอยู่"nest['b']['v']['y']"ที่ไหนnestพจนานุกรมที่ซ้อนกัน

evalฟังก์ชั่นในตัวรันสตริงที่กำหนด อย่างไรก็ตามสิ่งสำคัญคือต้องระวังช่องโหว่ที่อาจเกิดขึ้นจากการใช้evalฟังก์ชั่น การสนทนาสามารถพบได้ที่นี่:

  1. https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
  2. https://www.journaldev.com/22504/python-eval-function

ในnested_parse()ฟังก์ชั่นนี้ฉันได้ตรวจสอบให้แน่ใจว่าไม่มีพจนานุกรม__builtins__และตัวแปรเฉพาะที่มีอยู่เท่านั้นคือnestพจนานุกรม


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.