เรียบพจนานุกรมที่ซ้อนกันและแป้นบีบอัด


172

สมมติว่าคุณมีพจนานุกรมเช่น:

{'a': 1,
 'c': {'a': 2,
       'b': {'x': 5,
             'y' : 10}},
 'd': [1, 2, 3]}

คุณจะทำเรื่องแบนให้เป็นอย่างไร:

{'a': 1,
 'c_a': 2,
 'c_b_x': 5,
 'c_b_y': 10,
 'd': [1, 2, 3]}

2
นอกจากนี้ยังมีห้องสมุดสำหรับมัน: github.com/ianlini/flatten-dict
Ufos

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

คำตอบ:


220

โดยพื้นฐานเช่นเดียวกับที่คุณจะทำให้รายการซ้อนกันคุณต้องทำงานพิเศษสำหรับการทำซ้ำ dict ตามคีย์ / ค่าการสร้างคีย์ใหม่สำหรับพจนานุกรมใหม่ของคุณและการสร้างพจนานุกรมในขั้นตอนสุดท้าย

import collections

def flatten(d, parent_key='', sep='_'):
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, collections.MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

7
หากคุณแทนที่isinstanceด้วยtry..exceptบล็อกสิ่งนี้จะใช้ได้กับการแมปใด ๆ แม้ว่าจะไม่ได้มาจากdictก็ตาม
Björn Pollex

1
เปลี่ยนเพื่อทดสอบcollections.MutableMappingเพื่อให้เป็นแบบทั่วไปมากขึ้น แต่สำหรับ Python <2.6 try..exceptน่าจะเป็นตัวเลือกที่ดีที่สุด
Imran

5
หากคุณต้องการพจนานุกรมว่างเปล่าที่เก็บรักษาไว้ในเวอร์ชันแบนคุณอาจต้องการเปลี่ยนif isinstance(v, collections.MutableMapping):เป็นif v and isinstance(v, collections.MutableMapping):
tarequeh

3
โปรดทราบว่าสมมติว่ากุญแจอยู่เสมอสตริงมิฉะนั้นมันจะยกระดับnew_key = parent_key + sep + k if parent_key else k TypeError: cannot concatenate 'str' and [other] objectsอย่างไรก็ตามคุณสามารถแก้ไขได้โดยเพียงแค่บังคับkให้ string ( str(k)) หรือการต่อคีย์เข้ากับ tuple แทนที่จะเป็นสตริง (tuples สามารถเป็นปุ่ม dict ได้เช่นกัน)
Scott H

1
และฟังก์ชั่นเติมลมอยู่ที่นี่
แม่

65

มีข้อควรพิจารณาสองประการที่สำคัญที่โปสเตอร์ต้นฉบับต้องพิจารณา:

  1. มีปัญหาการอุดตันของ Keyspace หรือไม่ ยกตัวอย่างเช่นจะส่งผลให้{'a_b':{'c':1}, 'a':{'b_c':2}} {'a_b_c':???}วิธีการแก้ปัญหาด้านล่างนี้แก้ปัญหาโดยการคืนคู่ iterable
  2. หากประสิทธิภาพเป็นปัญหาฟังก์ชั่นลดคีย์ (ซึ่งฉันอ้างถึงในที่นี้ว่า 'เข้าร่วม') จำเป็นต้องเข้าถึงคีย์พา ธ ทั้งหมดหรือสามารถใช้ O (1) ทำงานกับโหนดทุกโหนดในต้นไม้ได้หรือไม่ ถ้าคุณต้องการที่จะพูดjoinedKey = '_'.join(*keys)นั่นจะทำให้คุณเสียเวลา O (N ^ 2) อย่างไรก็ตามหากคุณยินดีที่จะบอกnextKey = previousKey+'_'+thisKeyว่านั่นทำให้คุณได้เวลา O (N) วิธีการแก้ปัญหาด้านล่างช่วยให้คุณทำทั้งสองอย่าง (เนื่องจากคุณสามารถเชื่อมคีย์ทั้งหมดแล้วทำการประมวลผลภายหลัง)

(ผลการดำเนินงานไม่น่าจะเป็นปัญหา แต่ฉันจะทำอย่างละเอียดในจุดที่สองในกรณีที่คนอื่นใส่ใจ. ในการดำเนินการนี้มีทางเลือกที่เป็นอันตรายจำนวนมากหากคุณทำเช่นนี้ซ้ำและผลผลิตและอีกครั้งผลตอบแทนหรืออะไรที่เทียบเท่าซึ่งสัมผัส โหนดมากกว่าหนึ่งครั้ง (ซึ่งค่อนข้างง่ายที่จะตั้งใจทำ) คุณกำลังทำอาจ O (n ^ 2) การทำงานมากกว่า O (N). นี้เป็นเพราะบางทีคุณอาจจะมีการคำนวณที่สำคัญaนั้นa_1แล้วa_1_i... และจากนั้นการคำนวณaจากa_1นั้นa_1_ii... แต่จริงๆแล้วคุณไม่ควรคำนวณa_1อีกครั้งแม้ว่าคุณจะไม่ได้คำนวณใหม่ แต่การให้ผลลัพธ์ใหม่ (วิธีการ 'ทีละระดับ') นั้นไม่ดีตัวอย่างที่ดีคือ คิดเกี่ยวกับประสิทธิภาพการทำงานบน{1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

ด้านล่างเป็นฟังก์ชั่นที่ฉันเขียนflattenDict(d, join=..., lift=...)ซึ่งสามารถปรับให้เข้ากับวัตถุประสงค์ได้หลายอย่างและสามารถทำสิ่งที่คุณต้องการได้ น่าเสียดายที่มันค่อนข้างยากที่จะสร้างรุ่นที่ขี้เกียจของฟังก์ชั่นนี้โดยไม่เกิดการลงโทษด้านบน (python builtins จำนวนมากเช่น chain.from_iterable ไม่มีประสิทธิภาพจริง ๆ ซึ่งฉันรู้หลังจากทำการทดสอบโค้ดนี้สามรุ่นก่อนที่จะลงหลักปักฐานบน อันนี้).

from collections import Mapping
from itertools import chain
from operator import add

_FLAG_FIRST = object()

def flattenDict(d, join=add, lift=lambda x:x):
    results = []
    def visit(subdict, results, partialKey):
        for k,v in subdict.items():
            newKey = lift(k) if partialKey==_FLAG_FIRST else join(partialKey,lift(k))
            if isinstance(v,Mapping):
                visit(v, results, newKey)
            else:
                results.append((newKey,v))
    visit(d, results, _FLAG_FIRST)
    return results

เพื่อให้เข้าใจสิ่งที่เกิดขึ้นได้ดียิ่งขึ้นด้านล่างนี้เป็นแผนภาพสำหรับผู้ที่ไม่คุ้นเคยreduce(ซ้าย) หรือที่รู้จักกันในชื่อ "fold left" บางครั้งมันถูกวาดด้วยค่าเริ่มต้นแทน k0 (ไม่ใช่ส่วนหนึ่งของรายการส่งผ่านไปยังฟังก์ชัน) นี่Jคือjoinหน้าที่ของเรา เรา preprocess แต่ละ k nlift(k)กับ

               [k0,k1,...,kN].foldleft(J)
                           /    \
                         ...    kN
                         /
       J(k0,J(k1,J(k2,k3)))
                       /  \
                      /    \
           J(J(k0,k1),k2)   k3
                    /   \
                   /     \
             J(k0,k1)    k2
                 /  \
                /    \
               k0     k1

อันที่จริงแล้วเป็นเช่นเดียวกับfunctools.reduceแต่ที่ฟังก์ชั่นของเราทำสิ่งนี้กับทุกเส้นทางที่สำคัญของต้นไม้

>>> reduce(lambda a,b:(a,b), range(5))
((((0, 1), 2), 3), 4)

การสาธิต (ซึ่งฉันจะใส่ไว้ใน docstring):

>>> testData = {
        'a':1,
        'b':2,
        'c':{
            'aa':11,
            'bb':22,
            'cc':{
                'aaa':111
            }
        }
    }
from pprint import pprint as pp

>>> pp(dict( flattenDict(testData, lift=lambda x:(x,)) ))
{('a',): 1,
 ('b',): 2,
 ('c', 'aa'): 11,
 ('c', 'bb'): 22,
 ('c', 'cc', 'aaa'): 111}

>>> pp(dict( flattenDict(testData, join=lambda a,b:a+'_'+b) ))
{'a': 1, 'b': 2, 'c_aa': 11, 'c_bb': 22, 'c_cc_aaa': 111}    

>>> pp(dict( (v,k) for k,v in flattenDict(testData, lift=hash, join=lambda a,b:hash((a,b))) ))
{1: 12416037344,
 2: 12544037731,
 11: 5470935132935744593,
 22: 4885734186131977315,
 111: 3461911260025554326}

ประสิทธิภาพ:

from functools import reduce
def makeEvilDict(n):
    return reduce(lambda acc,x:{x:acc}, [{i:0 for i in range(n)}]+range(n))

import timeit
def time(runnable):
    t0 = timeit.default_timer()
    _ = runnable()
    t1 = timeit.default_timer()
    print('took {:.2f} seconds'.format(t1-t0))

>>> pp(makeEvilDict(8))
{7: {6: {5: {4: {3: {2: {1: {0: {0: 0,
                                 1: 0,
                                 2: 0,
                                 3: 0,
                                 4: 0,
                                 5: 0,
                                 6: 0,
                                 7: 0}}}}}}}}}

import sys
sys.setrecursionlimit(1000000)

forget = lambda a,b:''

>>> time(lambda: dict(flattenDict(makeEvilDict(10000), join=forget)) )
took 0.10 seconds
>>> time(lambda: dict(flattenDict(makeEvilDict(100000), join=forget)) )
[1]    12569 segmentation fault  python

... ถอนหายใจอย่าคิดว่าเป็นความผิดของฉัน ...


[บันทึกประวัติศาสตร์ที่ไม่สำคัญเนื่องจากปัญหาด้านการกลั่นกรอง]

เกี่ยวกับการกล่าวหาซ้ำ ๆ ของการเรียบพจนานุกรมพจนานุกรม (2 ระดับความลึก) ของรายการใน Python :

sorted( sum(flatten(...),[]) )วิธีการแก้ปัญหาของคำถามที่สามารถดำเนินการในแง่ของคนนี้ด้วยการทำ ไม่สามารถย้อนกลับได้: ในขณะที่เป็นจริงที่ค่าของflatten(...)สามารถกู้คืนได้จากการทำซ้ำที่ถูกกล่าวหาโดยการแมปตัวสะสมลำดับสูงกว่าหนึ่งไม่สามารถกู้คืนคีย์ (แก้ไข: นอกจากนี้ยังปรากฎว่าคำถามของเจ้าของซ้ำที่ถูกกล่าวหานั้นแตกต่างกันโดยสิ้นเชิงซึ่งเกี่ยวข้องกับพจนานุกรมที่มีความลึก 2 ระดับเท่านั้นแม้ว่าคำตอบหนึ่งในหน้านั้นจะให้คำตอบทั่วไป)


2
ฉันไม่แน่ใจว่าสิ่งนี้เกี่ยวข้องกับคำถามหรือไม่ วิธีการแก้ปัญหานี้ไม่ทำให้รายการพจนานุกรมของรายการพจนานุกรมแบนราบเช่น {'a': [{'aa': 1}, {'ab': 2}]} ฟังก์ชั่น flattenDict สามารถเปลี่ยนแปลงได้อย่างง่ายดายเพื่อรองรับกรณีนี้
Stewbaca

55

หรือถ้าคุณใช้แพนด้าอยู่แล้วคุณสามารถทำได้ด้วยวิธีjson_normalize()ดังนี้:

import pandas as pd

d = {'a': 1,
     'c': {'a': 2, 'b': {'x': 5, 'y' : 10}},
     'd': [1, 2, 3]}

df = pd.io.json.json_normalize(d, sep='_')

print(df.to_dict(orient='records')[0])

เอาท์พุท:

{'a': 1, 'c_a': 2, 'c_b_x': 5, 'c_b_y': 10, 'd': [1, 2, 3]}

4
หรือเพียงแค่ผ่านการโต้แย้งกันยายน)
บลูมูน

2
น่าเสียดาย
นิดหน่อยที่

31

หากคุณกำลังใช้งานpandasจะมีฟังก์ชั่นที่ซ่อนอยู่ในpandas.io.json._normalize1เรียกว่าnested_to_recordทำสิ่งนี้

from pandas.io.json._normalize import nested_to_record    

flat = nested_to_record(my_dict, sep='_')

1ในเวอร์ชันของแพนด้า0.24.xและการใช้งานที่เก่ากว่าpandas.io.json.normalize(ไม่มี_)


1
from pandas.io.json._normalize import nested_to_recordสิ่งที่ทำงานสำหรับฉันคือ ขอให้สังเกตขีดล่าง ( _) normalizeก่อน
Eyal Levin

2
@EyLLevin ดีจัง! การเปลี่ยนแปลงนี้เกิดขึ้น0.25.xฉันได้อัปเดตคำตอบแล้ว :)
Aaron N. Brock

28

นี่คือประเภทของ "การทำงาน", "หนึ่งซับ" การใช้งาน มันเป็นแบบเรียกซ้ำและอยู่บนพื้นฐานของการแสดงออกตามเงื่อนไขและความเข้าใจ Dict

def flatten_dict(dd, separator='_', prefix=''):
    return { prefix + separator + k if prefix else k : v
             for kk, vv in dd.items()
             for k, v in flatten_dict(vv, separator, kk).items()
             } if isinstance(dd, dict) else { prefix : dd }

ทดสอบ:

In [2]: flatten_dict({'abc':123, 'hgf':{'gh':432, 'yu':433}, 'gfd':902, 'xzxzxz':{"432":{'0b0b0b':231}, "43234":1321}}, '.')
Out[2]: 
{'abc': 123,
 'gfd': 902,
 'hgf.gh': 432,
 'hgf.yu': 433,
 'xzxzxz.432.0b0b0b': 231,
 'xzxzxz.43234': 1321}

นี้ไม่ได้ผลสำหรับพจนานุกรมทั่วไปโดยเฉพาะด้วยปุ่ม tuple เช่นแทน('hgf',2)สำหรับคีย์ครั้งที่ 2 ในการทดสอบของคุณพ่นTypeError
alancalvitti

@alancalvitti นี่ถือว่าเป็นสตริงหรืออย่างอื่นที่สนับสนุน+โอเปอเรเตอร์ สำหรับสิ่งอื่นคุณจะต้องปรับให้เข้าprefix + separator + kกับการเรียกใช้ฟังก์ชันที่เหมาะสมเพื่อเขียนวัตถุ
dividebyzero

ปัญหาอื่นที่เกี่ยวข้องกับปุ่มทูเปิล ฉันได้โพสต์วิธีการพูดคุยแยกต่างหากตามวิธีการของคุณ อย่างไรก็ตามมันไม่สามารถจัดการกับตัวอย่างของ ninjageko ได้อย่างถูกต้อง:{'a_b':{'c':1}, 'a':{'b_c':2}}
34425

2
ฉันเริ่มเป็นกังวลเมื่อเห็นว่าไม่มีคำตอบที่ใช้การสอบถามซ้ำ วันนี้มีอะไรผิดปกติกับเยาวชนของเรา?
Jakov

ไม่ทำอะไรเลยถ้า dict มีรายการ dicts ซ้อนกันเช่นนี้:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M

12

รหัส:

test = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}

def parse_dict(init, lkey=''):
    ret = {}
    for rkey,val in init.items():
        key = lkey+rkey
        if isinstance(val, dict):
            ret.update(parse_dict(val, key+'_'))
        else:
            ret[key] = val
    return ret

print(parse_dict(test,''))

ผล:

$ python test.py
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

ฉันใช้ python3.2 อัปเดตสำหรับ python เวอร์ชันของคุณ


คุณอาจต้องการระบุค่าเริ่มต้นของlkey=''ในนิยามฟังก์ชันของคุณแทนเมื่อเรียกใช้ฟังก์ชัน ดูคำตอบอื่น ๆ ในเรื่องนี้
คิวเมนตัส

6

วิธีการแก้ปัญหาเกี่ยวกับหน้าที่และประสิทธิภาพใน Python3.5

from functools import reduce


def _reducer(items, key, val, pref):
    if isinstance(val, dict):
        return {**items, **flatten(val, pref + key)}
    else:
        return {**items, pref + key: val}

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: _reducer(new_d, *kv, pref), 
        d.items(), 
        {}
    ))

นี่เป็นนักแสดงที่มากยิ่งขึ้น:

def flatten(d, pref=''):
    return(reduce(
        lambda new_d, kv: \
            isinstance(kv[1], dict) and \
            {**new_d, **flatten(kv[1], pref + kv[0])} or \
            {**new_d, pref + kv[0]: kv[1]}, 
        d.items(), 
        {}
    ))

ในการใช้งาน:

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

print(flatten(my_obj)) 
# {'d': [1, 2, 3], 'cby': 10, 'cbx': 5, 'ca': 2, 'a': 1}

2
วิธีการเกี่ยวกับวิธีการอ่านและการทำงาน? ;) เวอร์ชันใดที่คุณทดสอบสิ่งนี้ ฉันได้รับ "ข้อผิดพลาดทางไวยากรณ์" เมื่อลองใช้ Python 3.4.3 ดูเหมือนว่าการใช้ "** ทั้งหมด" นั้นไม่ถูกต้อง
Ingo Fischer

ฉันทำงานตั้งแต่ Python 3.5 ไม่รู้ว่ามันใช้ไม่ได้กับ 3.4 คุณพูดถูกมันไม่สามารถอ่านได้มาก ฉันอัพเดตคำตอบแล้ว หวังว่าจะอ่านได้มากขึ้นในขณะนี้ :)
Rotareti

1
เพิ่มการนำเข้าลดที่หายไป ยังพบรหัสยากที่จะเข้าใจและฉันคิดว่ามันเป็นตัวอย่างที่ดีว่าทำไม Guido van Rossum เองจึงไม่สนับสนุนการใช้แลมบ์ดาลดตัวกรองและแผนที่ในปี 2548: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer

ฉันเห็นด้วย. Python ไม่ได้ออกแบบมาเพื่อการเขียนโปรแกรมใช้งานจริง ถึงกระนั้นฉันคิดว่าreduceมันยอดเยี่ยมในกรณีที่คุณต้องการลดพจนานุกรม ฉันอัพเดตคำตอบแล้ว น่าจะดูไพเราะมากกว่านี้แล้ว
Rotareti

6

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

def flatten(d, parent_key=''):
    items = []
    for k, v in d.items():
        try:
            items.extend(flatten(v, '%s%s_' % (parent_key, k)).items())
        except AttributeError:
            items.append(('%s%s' % (parent_key, k), v))
    return dict(items)

1
หากdไม่ใช่dictแต่เป็นประเภทการแมปที่กำหนดเองที่ไม่ได้ใช้itemsฟังก์ชันของคุณจะล้มเหลวทันที ดังนั้นจึงไม่ได้ทำงานสำหรับประเภทการทำแผนที่ทุก items()แต่เฉพาะผู้ที่ใช้
user6037143

@ user6037143 คุณเคยพบประเภทการแมปที่ไม่ได้ใช้งานitemsหรือไม่ ฉันอยากรู้อยากเห็น
แต้ม Hunner

1
@ user6037143 ไม่คุณยังไม่ได้นิยามถ้ารายการไม่ได้ใช้มันไม่มีประเภทแผนที่
Davoud Taghawi-Nejad

@ DavoudTaghawi-Nejad คุณสามารถแก้ไขสิ่งนี้เพื่อจัดการกับปุ่มทั่วไปเช่น tuples ซึ่งไม่ควรแบนภายใน
alancalvitti

5

My Python 3.3 Solution โดยใช้เครื่องกำเนิดไฟฟ้า:

def flattenit(pyobj, keystring=''):
   if type(pyobj) is dict:
     if (type(pyobj) is dict):
         keystring = keystring + "_" if keystring else keystring
         for k in pyobj:
             yield from flattenit(pyobj[k], keystring + k)
     elif (type(pyobj) is list):
         for lelm in pyobj:
             yield from flatten(lelm, keystring)
   else:
      yield keystring, pyobj

my_obj = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y': 10}}, 'd': [1, 2, 3]}

#your flattened dictionary object
flattened={k:v for k,v in flattenit(my_obj)}
print(flattened)

# result: {'c_b_y': 10, 'd': [1, 2, 3], 'c_a': 2, 'a': 1, 'c_b_x': 5}

คุณสามารถขยายเพื่อจัดการประเภทคีย์ที่ถูกต้องนอกเหนือจาก str (รวมถึง tuple) ได้หรือไม่ แทนที่จะเชื่อมต่อสตริงเข้าร่วมในสิ่งอันดับ
alancalvitti

4

ฟังก์ชั่นง่าย ๆ เพื่อแผ่พจนานุกรมที่ซ้อนกัน สำหรับ Python 3 ให้แทนที่.iteritems()ด้วย.items()

def flatten_dict(init_dict):
    res_dict = {}
    if type(init_dict) is not dict:
        return res_dict

    for k, v in init_dict.iteritems():
        if type(v) == dict:
            res_dict.update(flatten_dict(v))
        else:
            res_dict[k] = v

    return res_dict

แนวคิด / ข้อกำหนดคือ: รับพจนานุกรมแบบแบนโดยไม่เก็บคีย์พาเรนต์

ตัวอย่างการใช้งาน:

dd = {'a': 3, 
      'b': {'c': 4, 'd': 5}, 
      'e': {'f': 
                 {'g': 1, 'h': 2}
           }, 
      'i': 9,
     }

flatten_dict(dd)

>> {'a': 3, 'c': 4, 'd': 5, 'g': 1, 'h': 2, 'i': 9}

การรักษาคีย์หลักนั้นง่ายเช่นกัน


4

ใช้การเรียกซ้ำทำให้มันง่ายและอ่านได้โดยมนุษย์

def flatten_dict(dictionary, accumulator=None, parent_key=None, separator="."):
    if accumulator is None:
        accumulator = {}

    for k, v in dictionary.items():
        k = f"{parent_key}{separator}{k}" if parent_key else k
        if isinstance(v, dict):
            flatten_dict(dictionary=v, accumulator=accumulator, parent_key=k)
            continue

        accumulator[k] = v

    return accumulator

การโทรเป็นเรื่องง่าย:

new_dict = flatten_dict(dictionary)

หรือ

new_dict = flatten_dict(dictionary, separator="_")

ถ้าเราต้องการเปลี่ยนตัวคั่นเริ่มต้น

รายละเอียดเล็ก ๆ น้อย ๆ :

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

if accumulator is None:
    accumulator = {}

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

k = f"{parent_key}{separator}{k}" if parent_key else k

ในกรณีที่ค่าvกุญแจkชี้ไปที่เป็นพจนานุกรมฟังก์ชั่นเรียกตัวเองผ่านพจนานุกรมที่ซ้อนกัน, accumulator(ซึ่งถูกส่งผ่านโดยอ้างอิงดังนั้นการเปลี่ยนแปลงทั้งหมดที่ทำกับมันจะทำในอินสแตนซ์เดียวกัน) และที่สำคัญkเพื่อให้เรา สามารถสร้างรหัสตัดแบ่ง สังเกตcontinueคำแถลง เราต้องการที่จะข้ามเส้นถัดไปด้านนอกของifบล็อกเพื่อให้พจนานุกรมซ้อนกันไม่ได้จบที่ขึ้นมาในภายใต้คีย์accumulatork

if isinstance(v, dict):
    flatten_dict(dict=v, accumulator=accumulator, parent_key=k)
    continue

แล้วเราจะทำอย่างไรในกรณีที่มูลค่าvไม่ใช่พจนานุกรม? accumulatorเพียงแค่ใส่มันไว้ภายในไม่เปลี่ยนแปลง

accumulator[k] = v

เมื่อเราทำเสร็จแล้วเราแค่คืนค่าaccumulatorทิ้งdictionaryอาร์กิวเมนต์เดิมไว้โดยไม่แตะต้อง

บันทึก

สิ่งนี้จะใช้ได้กับพจนานุกรมที่มีสตริงเป็นกุญแจเท่านั้น มันจะทำงานกับวัตถุที่แฮชใช้__repr__วิธีการ แต่จะให้ผลลัพธ์ที่ไม่พึงประสงค์


3

สิ่งนี้คล้ายกับคำตอบของ imran และ ralu มันไม่ได้ใช้ตัวกำเนิด แต่แทนที่จะใช้การเรียกซ้ำด้วยการปิด:

def flatten_dict(d, separator='_'):
  final = {}
  def _flatten_dict(obj, parent_keys=[]):
    for k, v in obj.iteritems():
      if isinstance(v, dict):
        _flatten_dict(v, parent_keys + [k])
      else:
        key = separator.join(parent_keys + [k])
        final[key] = v
  _flatten_dict(d)
  return final

>>> print flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]})
{'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

ฉันไม่แน่ใจว่าการใช้คำว่า " closure " ถูกต้องหรือไม่เนื่องจากฟังก์ชั่น_flatten_dictไม่เคยถูกส่งคืนและไม่คาดว่าจะได้รับคืน มันอาจจะเรียกว่าเป็นฟังก์ชั่นย่อยหรือฟังก์ชั่นที่ปิดล้อมแทน
คิวเมนตัส

3

โซลูชันของ Davoud นั้นดีมาก แต่ไม่ได้ผลลัพธ์ที่น่าพอใจเมื่อ dict ที่ซ้อนกันยังมีรายการของ dicts แต่รหัสของเขาจะถูกปรับให้เหมาะกับกรณีนั้น:

def flatten_dict(d):
    items = []
    for k, v in d.items():
        try:
            if (type(v)==type([])): 
                for l in v: items.extend(flatten_dict(l).items())
            else: 
                items.extend(flatten_dict(v).items())
        except AttributeError:
            items.append((k, v))
    return dict(items)

คุณสามารถแคชผลจากการที่จะหลีกเลี่ยงการเรียกฟังก์ชั่นสำหรับรายการของทุกtype([]) dict
bfontaine

2
โปรดใช้isinstance(v, list)แทน
Druska

2

คำตอบข้างต้นใช้ได้ดีจริงๆ แค่คิดว่าฉันจะเพิ่มฟังก์ชั่นไม่แบนที่ฉันเขียน:

def unflatten(d):
    ud = {}
    for k, v in d.items():
        context = ud
        for sub_key in k.split('_')[:-1]:
            if sub_key not in context:
                context[sub_key] = {}
            context = context[sub_key]
        context[k.split('_')[-1]] = v
    return ud

หมายเหตุ: นี่ไม่ได้หมายความว่า '_' มีอยู่ในคีย์อยู่แล้วเช่นเดียวกับลูกเล่นที่เรียบ


2

นี่คืออัลกอริทึมสำหรับการแทนที่ที่หรูหราและสวยงาม ทดสอบกับ Python 2.7 และ Python 3.5 การใช้อักขระจุดเป็นตัวคั่น

def flatten_json(json):
    if type(json) == dict:
        for k, v in list(json.items()):
            if type(v) == dict:
                flatten_json(v)
                json.pop(k)
                for k2, v2 in v.items():
                    json[k+"."+k2] = v2

ตัวอย่าง:

d = {'a': {'b': 'c'}}                   
flatten_json(d)
print(d)
unflatten_json(d)
print(d)

เอาท์พุท:

{'a.b': 'c'}
{'a': {'b': 'c'}}

ฉันเผยแพร่โค้ดนี้ที่นี่พร้อมกับunflatten_jsonฟังก์ชั่นการจับคู่


2

หากคุณต้องการแบนพจนานุกรมที่ซ้อนกันและต้องการรายการคีย์ที่ไม่ซ้ำกันทั้งหมดนี่คือคำตอบ:

def flat_dict_return_unique_key(data, unique_keys=set()):
    if isinstance(data, dict):
        [unique_keys.add(i) for i in data.keys()]
        for each_v in data.values():
            if isinstance(each_v, dict):
                flat_dict_return_unique_key(each_v, unique_keys)
    return list(set(unique_keys))

2
def flatten(unflattened_dict, separator='_'):
    flattened_dict = {}

    for k, v in unflattened_dict.items():
        if isinstance(v, dict):
            sub_flattened_dict = flatten(v, separator)
            for k2, v2 in sub_flattened_dict.items():
                flattened_dict[k + separator + k2] = v2
        else:
            flattened_dict[k] = v

    return flattened_dict

2
def flatten_nested_dict(_dict, _str=''):
    '''
    recursive function to flatten a nested dictionary json
    '''
    ret_dict = {}
    for k, v in _dict.items():
        if isinstance(v, dict):
            ret_dict.update(flatten_nested_dict(v, _str = '_'.join([_str, k]).strip('_')))
        elif isinstance(v, list):
            for index, item in enumerate(v):
                if isinstance(item, dict):
                    ret_dict.update(flatten_nested_dict(item,  _str= '_'.join([_str, k, str(index)]).strip('_')))
                else:
                    ret_dict['_'.join([_str, k, str(index)]).strip('_')] = item
        else:
            ret_dict['_'.join([_str, k]).strip('_')] = v
    return ret_dict

สิ่งนี้ใช้ได้กับรายการต่างๆภายใน dict ที่ซ้อนกันของเรา แต่ไม่มีตัวเลือกตัวคั่นที่กำหนดเอง
Nikhil VJ

2

ฉันกำลังคิดว่าคลาสย่อยของ UserDict แบนปุ่มให้โดยอัตโนมัติ

class FlatDict(UserDict):
    def __init__(self, *args, separator='.', **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        if isinstance(value, dict):
            for k1, v1 in FlatDict(value, separator=self.separator).items():
                super().__setitem__(f"{key}{self.separator}{k1}", v1)
        else:
            super().__setitem__(key, value)

advantages ข้อดีคือสามารถเพิ่มคีย์ได้ทันทีหรือใช้การติดตั้งตามมาตรฐานโดยไม่ต้องแปลกใจ:

>>> fd = FlatDict(
...    {
...        'person': {
...            'sexe': 'male', 
...            'name': {
...                'first': 'jacques',
...                'last': 'dupond'
...            }
...        }
...    }
... )
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond'}
>>> fd['person'] = {'name': {'nickname': 'Bob'}}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob'}
>>> fd['person.name'] = {'civility': 'Dr'}
>>> fd
{'person.sexe': 'male', 'person.name.first': 'jacques', 'person.name.last': 'dupond', 'person.name.nickname': 'Bob', 'person.name.civility': 'Dr'}

1
การกำหนดให้กับ fd ['person'] แต่การรักษามูลค่าที่มีอยู่นั้นค่อนข้างน่าแปลกใจ นั่นไม่ใช่วิธีการทำงานปกติ dicts
tbm

1

ใช้เครื่องกำเนิดไฟฟ้า:

def flat_dic_helper(prepand,d):
    if len(prepand) > 0:
        prepand = prepand + "_"
    for k in d:
        i=d[k]
        if type(i).__name__=='dict':
            r = flat_dic_helper(prepand+k,i)
            for j in r:
                yield j
        else:
            yield (prepand+k,i)

def flat_dic(d): return dict(flat_dic_helper("",d))

d={'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
print(flat_dic(d))


>> {'a': 1, 'c_a': 2, 'c_b_x': 5, 'd': [1, 2, 3], 'c_b_y': 10}

2
type(i).__name__=='dict'อาจถูกแทนที่ด้วยtype(i) is dictหรืออาจจะดีกว่าisinstance(d, dict)(หรือMapping/ MutableMapping)
Cristian Ciupitu

1

ใช้ dict.popitem () ในการเรียกซ้ำแบบรายการซ้อนแบบตรงไปตรงมา:

def flatten(d):
    if d == {}:
        return d
    else:
        k,v = d.popitem()
        if (dict != type(v)):
            return {k:v, **flatten(d)}
        else:
            flat_kv = flatten(v)
            for k1 in list(flat_kv.keys()):
                flat_kv[k + '_' + k1] = flat_kv[k1]
                del flat_kv[k1]
            return {**flat_kv, **flatten(d)}

1

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

ฉันพบการใช้งานความคิดเห็น list-inclusion โดย @roneoกับคำตอบที่โพสต์โดย @Imran :

https://github.com/ScriptSmith/socialreaper/blob/master/socialreaper/tools.py#L8

import collections
def flatten(dictionary, parent_key=False, separator='.'):
    """
    Turn a nested dictionary into a flattened dictionary
    :param dictionary: The dictionary to flatten
    :param parent_key: The string to prepend to dictionary's keys
    :param separator: The string used to separate flattened keys
    :return: A flattened dictionary
    """

    items = []
    for key, value in dictionary.items():
        new_key = str(parent_key) + separator + key if parent_key else key
        if isinstance(value, collections.MutableMapping):
            items.extend(flatten(value, new_key, separator).items())
        elif isinstance(value, list):
            for k, v in enumerate(value):
                items.extend(flatten({str(k): v}, new_key).items())
        else:
            items.append((new_key, value))
    return dict(items)

ทดสอบมัน

flatten({'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3] })

>> {'a': 1, 'c.a': 2, 'c.b.x': 5, 'c.b.y': 10, 'd.0': 1, 'd.1': 2, 'd.2': 3}

Annd ที่ทำงานที่ฉันต้องทำ: ฉันโยน json ที่ซับซ้อนใด ๆ ที่นี่และทำให้แบนสำหรับฉัน

เครดิตทั้งหมดในhttps://github.com/ScriptSmith


1

จริง ๆ แล้วฉันได้เขียนแพคเกจที่เรียกว่า cherrypicker เมื่อเร็ว ๆ นี้เพื่อจัดการกับสิ่งที่แน่นอนตั้งแต่ฉันต้องทำบ่อย ๆ !

ฉันคิดว่ารหัสต่อไปนี้จะให้สิ่งที่คุณเป็นหลัง:

from cherrypicker import CherryPicker

dct = {
    'a': 1,
    'c': {
        'a': 2,
        'b': {
            'x': 5,
            'y' : 10
        }
    },
    'd': [1, 2, 3]
}

picker = CherryPicker(dct)
picker.flatten().get()

คุณสามารถติดตั้งแพ็คเกจด้วย:

pip install cherrypicker

... และมีเอกสารเพิ่มเติมและคำแนะนำที่https://cherrypicker.readthedocs.io

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


ฉันชอบวิธีการทางเลือก
Gergely M

0

ฉันมักจะต้องการการเข้าถึงdictวัตถุผ่าน.items()ดังนั้นสำหรับแฟบ dicts flat_items(d)ผมใช้เครื่องกำเนิดไฟฟ้า หากคุณต้องการที่จะมีdictอีกครั้งเพียงแค่ห่อมันเช่นนี้:flat = dict(flat_items(d))

def flat_items(d, key_separator='.'):
    """
    Flattens the dictionary containing other dictionaries like here: /programming/6027558/flatten-nested-python-dictionaries-compressing-keys

    >>> example = {'a': 1, 'c': {'a': 2, 'b': {'x': 5, 'y' : 10}}, 'd': [1, 2, 3]}
    >>> flat = dict(flat_items(example, key_separator='_'))
    >>> assert flat['c_b_y'] == 10
    """
    for k, v in d.items():
        if type(v) is dict:
            for k1, v1 in flat_items(v, key_separator=key_separator):
                yield key_separator.join((k, k1)), v1
        else:
            yield k, v

0

รูปแบบของพจนานุกรมที่ซ้อนกันแบบราบนี้การบีบอัดแป้นด้วย max_level และตัวลดที่กำหนดเอง

  def flatten(d, max_level=None, reducer='tuple'):
      if reducer == 'tuple':
          reducer_seed = tuple()
          reducer_func = lambda x, y: (*x, y)
      else:
          raise ValueError(f'Unknown reducer: {reducer}')

      def impl(d, pref, level):
        return reduce(
            lambda new_d, kv:
                (max_level is None or level < max_level)
                and isinstance(kv[1], dict)
                and {**new_d, **impl(kv[1], reducer_func(pref, kv[0]), level + 1)}
                or {**new_d, reducer_func(pref, kv[0]): kv[1]},
                d.items(),
            {}
        )

      return impl(d, reducer_seed, 0)

0

หากคุณไม่คำนึงถึงฟังก์ชั่นวนซ้ำนี่คือวิธีแก้ปัญหา ฉันยังมีเสรีภาพที่จะรวมการยกเว้นพารามิเตอร์ในกรณีที่มีค่าอย่างน้อยหนึ่งค่าที่คุณต้องการบำรุงรักษา

รหัส:

def flatten_dict(dictionary, exclude = [], delimiter ='_'):
    flat_dict = dict()
    for key, value in dictionary.items():
        if isinstance(value, dict) and key not in exclude:
            flatten_value_dict = flatten_dict(value, exclude, delimiter)
            for k, v in flatten_value_dict.items():
                flat_dict[f"{key}{delimiter}{k}"] = v
        else:
            flat_dict[key] = value
    return flat_dict

การใช้งาน:

d = {'a':1, 'b':[1, 2], 'c':3, 'd':{'a':4, 'b':{'a':7, 'b':8}, 'c':6}, 'e':{'a':1,'b':2}}
flat_d = flatten_dict(dictionary=d, exclude=['e'], delimiter='.')
print(flat_d)

เอาท์พุท:

{'a': 1, 'b': [1, 2], 'c': 3, 'd.a': 4, 'd.b.a': 7, 'd.b.b': 8, 'd.c': 6, 'e': {'a': 1, 'b': 2}}

0

ฉันลองวิธีแก้ปัญหาบางอย่างในหน้านี้ - แม้ว่าจะไม่ใช่ทั้งหมด - แต่ฉันพยายามแล้วที่ล้มเหลวในการจัดการรายการ dict ที่ซ้อนกัน

พิจารณา dict แบบนี้:

d = {
        'owner': {
            'name': {'first_name': 'Steven', 'last_name': 'Smith'},
            'lottery_nums': [1, 2, 3, 'four', '11', None],
            'address': {},
            'tuple': (1, 2, 'three'),
            'tuple_with_dict': (1, 2, 'three', {'is_valid': False}),
            'set': {1, 2, 3, 4, 'five'},
            'children': [
                {'name': {'first_name': 'Jessica',
                          'last_name': 'Smith', },
                 'children': []
                 },
                {'name': {'first_name': 'George',
                          'last_name': 'Smith'},
                 'children': []
                 }
            ]
        }
    }

นี่คือโซลูชันชั่วคราวของฉัน:

def flatten_dict(input_node: dict, key_: str = '', output_dict: dict = {}):
    if isinstance(input_node, dict):
        for key, val in input_node.items():
            new_key = f"{key_}.{key}" if key_ else f"{key}"
            flatten_dict(val, new_key, output_dict)
    elif isinstance(input_node, list):
        for idx, item in enumerate(input_node):
            flatten_dict(item, f"{key_}.{idx}", output_dict)
    else:
        output_dict[key_] = input_node
    return output_dict

ซึ่งผลิต:

{
  owner.name.first_name: Steven,
  owner.name.last_name: Smith,
  owner.lottery_nums.0: 1,
  owner.lottery_nums.1: 2,
  owner.lottery_nums.2: 3,
  owner.lottery_nums.3: four,
  owner.lottery_nums.4: 11,
  owner.lottery_nums.5: None,
  owner.tuple: (1, 2, 'three'),
  owner.tuple_with_dict: (1, 2, 'three', {'is_valid': False}),
  owner.set: {1, 2, 3, 4, 'five'},
  owner.children.0.name.first_name: Jessica,
  owner.children.0.name.last_name: Smith,
  owner.children.1.name.first_name: George,
  owner.children.1.name.last_name: Smith,
}

วิธีการแก้ปัญหาชั่วคราวและมันไม่สมบูรณ์แบบ
บันทึก:

  • มันไม่เก็บ dicts ที่ว่างเปล่าเช่นaddress: {}คู่ k / v

  • มันจะไม่แบน dicts ใน tuples ที่ซ้อนกัน - แม้ว่ามันจะง่ายต่อการเพิ่มโดยใช้ความจริงที่ว่า python tuples ทำหน้าที่คล้ายกับรายการ


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