Làm phẳng từ điển lồng nhau, nén phím


172

Giả sử bạn có một từ điển như:

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

Làm thế nào bạn sẽ đi về làm phẳng nó thành một cái gì đó như:

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

2
Ngoài ra, có một thư viện cho nó: github.com/ianlini/flatten-dict
Ufos

Câu trả lời:


219

Về cơ bản giống như cách bạn làm phẳng một danh sách lồng nhau, bạn chỉ cần thực hiện thêm công việc để lặp lại chính tả bằng khóa / giá trị, tạo khóa mới cho từ điển mới của bạn và tạo từ điển ở bước cuối cùng.

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
Nếu bạn thay thế isinstancebằng một try..exceptkhối, điều này sẽ hoạt động cho bất kỳ ánh xạ nào, ngay cả khi nó không xuất phát từ dict.
Bjorn Pollex

1
Thay đổi nó để kiểm tra collections.MutableMappingđể làm cho nó chung chung hơn. Nhưng đối với Python <2.6, try..exceptcó lẽ là lựa chọn tốt nhất.
Imran

5
Nếu bạn muốn từ điển trống được bảo quản trong phiên bản dẹt, bạn có thể muốn đổi if isinstance(v, collections.MutableMapping):thànhif v and isinstance(v, collections.MutableMapping):
tarequeh

3
Lưu ý rằng new_key = parent_key + sep + k if parent_key else kgiả định rằng các khóa luôn là chuỗi, nếu không nó sẽ tăng TypeError: cannot concatenate 'str' and [other] objects. Tuy nhiên, bạn có thể khắc phục điều đó bằng cách đơn giản là ép buộc kchuỗi ( str(k)) hoặc nối các khóa thành một tuple thay vì một chuỗi (tuples cũng có thể là các phím chính tả).
Scott H

1
Và chức năng lạm phát là ở đây
mitch

65

Có hai cân nhắc lớn mà người đăng ban đầu cần xem xét:

  1. Có vấn đề ghi đè không gian phím? Ví dụ, {'a_b':{'c':1}, 'a':{'b_c':2}}sẽ dẫn đến {'a_b_c':???}. Giải pháp dưới đây tránh được vấn đề bằng cách trả về một cặp lặp.
  2. Nếu hiệu suất là một vấn đề, thì chức năng giảm khóa (mà tôi gọi là 'tham gia') có yêu cầu quyền truy cập vào toàn bộ đường dẫn khóa hay chỉ có thể làm O (1) hoạt động ở mọi nút trong cây? Nếu bạn muốn có thể nói joinedKey = '_'.join(*keys), điều đó sẽ khiến bạn mất O (N ^ 2) thời gian chạy. Tuy nhiên, nếu bạn sẵn sàng nói nextKey = previousKey+'_'+thisKey, điều đó giúp bạn có thời gian O (N). Giải pháp dưới đây cho phép bạn thực hiện cả hai (vì bạn chỉ có thể ghép tất cả các khóa, sau đó xử lý hậu kỳ chúng).

(Hiệu suất không phải là vấn đề, nhưng tôi sẽ giải thích về điểm thứ hai trong trường hợp bất kỳ ai khác quan tâm: Khi thực hiện điều này, có rất nhiều lựa chọn nguy hiểm. Nếu bạn thực hiện điều này một cách đệ quy và mang lại và tái sản xuất, hoặc bất cứ điều gì tương đương nút nhiều hơn một lần (đó là khá dễ dàng để vô tình làm), bạn đang làm có khả năng (^ 2 N) công việc chứ không phải là O O (N). Đây là vì có thể bạn đang tính toán một chìa khóa arồi a_1sau đó a_1_i..., và sau đó tính toán asau a_1đó a_1_ii..., nhưng thực sự bạn không cần phải tính toán a_1lại. Ngay cả khi bạn không tính toán lại, thì việc đánh giá lại nó (một cách tiếp cận 'theo cấp độ') cũng tệ như vậy. Một ví dụ điển hình là suy nghĩ về hiệu suất trên {1:{1:{1:{1:...(N times)...{1:SOME_LARGE_DICTIONARY_OF_SIZE_N}...}}}})

Dưới đây là một chức năng tôi đã viết flattenDict(d, join=..., lift=...)có thể được điều chỉnh cho nhiều mục đích và có thể làm những gì bạn muốn. Đáng buồn là khá khó để tạo ra một phiên bản lười biếng của chức năng này mà không phải chịu các hình phạt về hiệu năng ở trên (nhiều nội dung python như chain.from_iterable không thực sự hiệu quả, mà tôi chỉ nhận ra sau khi thử nghiệm rộng rãi ba phiên bản khác nhau của mã này trước khi xử lý cái này).

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

Để hiểu rõ hơn những gì đang diễn ra, bên dưới là sơ đồ cho những người không quen thuộc reduce(bên trái), còn được gọi là "gấp bên trái". Đôi khi nó được vẽ với giá trị ban đầu thay cho k0 (không phải là một phần của danh sách, được truyền vào hàm). Đây Jjoinchức năng của chúng tôi . Chúng tôi tiền xử lý mỗi k n với lift(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

Thực tế điều này giống như functools.reduce, nhưng chức năng của chúng ta thực hiện điều này với tất cả các đường dẫn chính của cây.

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

Trình diễn (mà nếu không tôi sẽ đặt trong chuỗi):

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

Hiệu suất:

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

... thở dài, đừng nghĩ rằng đó là lỗi của tôi ...


[ghi chú lịch sử không quan trọng do vấn đề kiểm duyệt]

Liên quan đến bản sao được cho là của Flatten một từ điển từ điển (sâu 2 cấp) của danh sách trong Python :

Giải pháp cho câu hỏi đó có thể được thực hiện theo cách này sorted( sum(flatten(...),[]) ). Điều ngược lại là không thể: mặc dù đúng là các giá trị của flatten(...)có thể được phục hồi từ bản sao bị cáo buộc bằng cách ánh xạ bộ tích lũy bậc cao hơn, người ta không thể khôi phục các khóa. (chỉnh sửa: Ngoài ra, câu hỏi của chủ sở hữu trùng lặp bị cáo buộc là hoàn toàn khác nhau, ở chỗ nó chỉ liên quan đến từ điển chính xác sâu 2 cấp, mặc dù một trong những câu trả lời trên trang đó đưa ra giải pháp chung.)


2
Tôi không chắc chắn nếu điều này có liên quan đến câu hỏi. Giải pháp này không làm phẳng một mục từ điển của danh sách từ điển, tức là {'a': [{'aa': 1}, {'ab': 2}]}. Hàm flattenDict có thể được thay đổi dễ dàng để phù hợp với trường hợp này.
Stewbaca

55

Hoặc nếu bạn đã sử dụng gấu trúc, bạn có thể làm json_normalize()như vậy:

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

Đầu ra:

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

4
hoặc chỉ cần vượt qua đối số sep :)
Blue Moon

2
Hơi xấu hổ vì nó không xử lý danh sách :)
Roelant

31

Nếu bạn đang sử dụng, pandascó một chức năng ẩn trong pandas.io.json._normalize1 được gọi là nested_to_recordchính xác.

from pandas.io.json._normalize import nested_to_record    

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

1 Trong các phiên bản gấu trúc 0.24.xvà sử dụng cũ hơn pandas.io.json.normalize(không có _)


1
Những gì làm việc cho tôi là from pandas.io.json._normalize import nested_to_record. Lưu ý dấu gạch dưới ( _) trước normalize.
Mắt Levin

2
@EyalLevin Bắt tốt! Điều này đã thay đổi 0.25.x, tôi đã cập nhật câu trả lời. :)
Aaron N. Brock

28

Đây là một loại thực hiện "chức năng", "một lớp". Nó là đệ quy, và dựa trên một biểu thức có điều kiện và một sự hiểu biết chính tả.

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 }

Kiểm tra:

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}

Điều này không hoạt động đối với từ điển chung, cụ thể, với các phím tuple, ví dụ: thay thế ('hgf',2)cho khóa thứ 2 trong các lần ném thử nghiệm của bạnTypeError
alancalvitti

@alancalvitti Điều này giả sử nó là một chuỗi hoặc một cái gì đó hỗ trợ +toán tử. Đối với bất cứ điều gì khác, bạn sẽ cần phải thích ứng prefix + separator + kvới lời gọi hàm thích hợp để soạn các đối tượng.
splititherzero

Một vấn đề khác liên quan đến khóa tuple. Tôi đã đăng riêng cách tổng quát hóa dựa trên phương pháp của bạn. Tuy nhiên, nó không thể xử lý chính xác ví dụ của ninjageko:{'a_b':{'c':1}, 'a':{'b_c':2}}
alancalvitti

2
Tôi đã lo lắng, không thấy câu trả lời sử dụng đệ quy. Những gì sai với tuổi trẻ của chúng ta những ngày này?
Jakov

không làm gì nếu một dict có danh sách các dicts lồng nhau, như thế này:{'name': 'Steven', 'children': [{'name': 'Jessica', 'children': []}, {'name': 'George', 'children': []}]}
Gergely M

12

Mã số:

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

Các kết quả:

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

Tôi đang sử dụng python3.2, cập nhật cho phiên bản python của bạn.


Bạn có thể muốn chỉ định giá trị mặc định lkey=''trong định nghĩa hàm của bạn thay vì khi gọi hàm. Xem câu trả lời khác về vấn đề này.
Acumenus

6

Làm thế nào về một giải pháp chức năng và hiệu suất trong 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(), 
        {}
    ))

Điều này thậm chí còn hiệu quả hơn:

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(), 
        {}
    ))

Đang sử dụng:

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
Làm thế nào về một giải pháp dễ đọc và làm việc? ;) Phiên bản nào bạn đã thử nghiệm này? Tôi đang gặp "Lỗi cú pháp" khi dùng thử trong Python 3.4.3. Có vẻ như việc sử dụng "** tất cả" là không hợp pháp.
Ingo Fischer

Tôi làm việc kể từ Python 3.5. Không biết nó không hoạt động với 3.4. Bạn nói đúng điều này không dễ đọc. Tôi cập nhật câu trả lời. Hy vọng nó dễ đọc hơn bây giờ. :)
Rotareti

1
Thêm thiếu giảm nhập. Vẫn thấy mã khó hiểu và tôi nghĩ đó là một ví dụ tốt tại sao chính Guido van Rossum không khuyến khích việc sử dụng lambda, giảm, lọc và bản đồ vào năm 2005: artima.com/weblogs/viewpost.jsp?thread=98196
Ingo Fischer

Tôi đồng ý. Python không thực sự được thiết kế để lập trình chức năng . Tuy nhiên tôi nghĩ reducelà tuyệt vời trong trường hợp bạn cần giảm từ điển. Tôi cập nhật câu trả lời. Nên nhìn thêm một chút pythonic bây giờ.
Rotareti

6

Điều này không bị giới hạn trong từ điển, nhưng mọi loại ánh xạ thực hiện .items (). Hơn nữa ist nhanh hơn vì nó tránh một điều kiện if. Tuy nhiên, các khoản tín dụng đến 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
Nếu dkhông phải là dictmột loại ánh xạ tùy chỉnh không thực hiện items, chức năng của bạn sẽ thất bại ngay lúc đó và ở đó. Vì vậy, nó không hoạt động cho mọi loại ánh xạ mà chỉ những loại thực hiện items().
dùng6037143

@ user6037143 Bạn đã bao giờ gặp phải loại ánh xạ không triển khai itemschưa? Tôi tò mò muốn xem một cái.
Trey Hunner

1
@ user6037143, không có định nghĩa nào nếu bạn không thực hiện nếu không có loại ánh xạ.
Davoud Taghawi-Nejad

@ DavoudTaghawi-Nejad, bạn có thể sửa đổi điều này để xử lý các khóa chung không, ví dụ như các bộ dữ liệu không nên làm phẳng trong nội bộ.
alancalvitti

5

Giải pháp Python 3.3 của tôi sử dụng máy phát điện:

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}

bạn có thể mở rộng để xử lý bất kỳ loại khóa hợp lệ nào ngoài str (bao gồm cả tuple) không? Thay vì nối chuỗi, tham gia chúng trong một tuple.
alancalvitti

4

Chức năng đơn giản để làm phẳng từ điển lồng nhau. Đối với Python 3, thay thế .iteritems()bằng.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

Ý tưởng / yêu cầu là: Nhận từ điển phẳng mà không giữ khóa cha.

Ví dụ về cách sử dụng:

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}

Giữ chìa khóa cha mẹ cũng đơn giản.


3

Điều này tương tự với câu trả lời của cả imran và ralu. Nó không sử dụng một trình tạo, mà thay vào đó sử dụng đệ quy với một bao đóng:

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}

Tôi không chắc chắn nếu sử dụng thuật ngữ " đóng cửa " là chính xác ở đây, vì chức năng _flatten_dictkhông bao giờ được trả lại, và nó cũng không bao giờ được trả lại. Nó có lẽ có thể được gọi là một chức năng con hoặc một chức năng kèm theo thay thế.
Acumenus

3

Giải pháp của Davoud rất hay nhưng không cho kết quả khả quan khi chính tả lồng nhau cũng chứa danh sách các ký tự, nhưng mã của anh ta được điều chỉnh cho trường hợp đó:

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)

Bạn có thể lưu trữ kết quả của bộ đệm type([])để tránh lệnh gọi hàm cho mọi mục của dict.
bfontaine

2
Vui lòng sử dụng isinstance(v, list)thay thế
Druska

3

Sử dụng đệ quy, giữ cho nó đơn giản và dễ đọc với con người:

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

Gọi rất đơn giản:

new_dict = flatten_dict(dictionary)

hoặc là

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

nếu chúng ta muốn thay đổi dấu phân cách mặc định.

Một chút đổ vỡ:

Khi hàm được gọi lần đầu tiên, nó được gọi chỉ truyền qua dictionarychúng ta muốn làm phẳng. Các accumulatortham số là ở đây để hỗ trợ đệ quy, mà chúng ta thấy sau đó. Vì vậy, chúng tôi khởi tạo accumulatormột từ điển trống, nơi chúng tôi sẽ đặt tất cả các giá trị lồng nhau từ bản gốc dictionary.

if accumulator is None:
    accumulator = {}

Khi chúng tôi lặp lại các giá trị của từ điển, chúng tôi xây dựng một khóa cho mọi giá trị. Đối parent_keysố sẽ Nonedành cho cuộc gọi đầu tiên, trong khi đối với mọi từ điển lồng nhau, nó sẽ chứa khóa trỏ đến nó, vì vậy chúng tôi bổ sung khóa đó.

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

Trong trường hợp giá trị vmà khóa kđang trỏ đến là một từ điển, hàm sẽ tự gọi nó, chuyển từ điển lồng nhau, accumulator(được truyền bằng tham chiếu, do đó, tất cả các thay đổi được thực hiện trên cùng một ví dụ) và khóa kđể chúng ta có thể xây dựng khóa ghép. Chú ý continuetuyên bố. Chúng tôi muốn bỏ qua dòng tiếp theo, bên ngoài ifkhối, để từ điển lồng nhau không kết thúc trong accumulatorkhóa dưới k.

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

Vậy, chúng ta phải làm gì trong trường hợp giá trị vkhông phải là từ điển? Chỉ cần đặt nó không thay đổi bên trong accumulator.

accumulator[k] = v

Khi chúng ta đã hoàn thành, chúng ta chỉ cần trả lại accumulator, không để lại dictionaryđối số ban đầu .

GHI CHÚ

Điều này sẽ chỉ hoạt động với các từ điển có chuỗi là khóa. Nó sẽ làm việc với các đối tượng có thể băm thực hiện __repr__phương thức, nhưng sẽ mang lại kết quả không mong muốn.


2

Các câu trả lời trên hoạt động thực sự tốt. Chỉ cần nghĩ rằng tôi đã thêm chức năng unflatten mà tôi đã viết:

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

Lưu ý: Điều này không giải thích cho '_' đã có trong các khóa, giống như các đối tác làm phẳng.


2

Đây là một thuật toán để thay thế thanh lịch, tại chỗ. Đã thử nghiệm với Python 2.7 và Python 3.5. Sử dụng ký tự dấu chấm làm dấu phân cách.

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

Thí dụ:

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

Đầu ra:

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

Tôi đã xuất bản mã này ở đây cùng với unflatten_jsonchức năng phù hợp .


2

Nếu bạn muốn từ điển lồng nhau và muốn tất cả danh sách các khóa duy nhất thì đây là giải pháp:

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

cái này hoạt động với các danh sách bên trong dict lồng nhau của chúng tôi, nhưng không có tùy chọn phân tách tùy chỉnh
Nikhil VJ

2

Tôi đã nghĩ đến một lớp con của UserDict để tự động làm phẳng các phím.

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)

Những lợi thế mà các phím có thể được thêm vào một cách nhanh chóng, hoặc sử dụng chính tả chính tả, mà không có gì bất ngờ:

>>> 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
Chỉ định cho fd ['người'] nhưng duy trì giá trị hiện tại của nó là khá đáng ngạc nhiên. Đó không phải là cách thức hoạt động thường xuyên.
tbm

1

Sử dụng máy phát điện:

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'có thể được thay thế bằng type(i) is dicthoặc thậm chí tốt hơn isinstance(d, dict)(hoặc Mapping/ MutableMapping).
Cristian Ciupitu

1

Sử dụng dict.popitem () trong đệ quy đơn giản giống như danh sách lồng nhau:

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

Không chính xác những gì OP yêu cầu, nhưng rất nhiều người đang đến đây để tìm cách làm phẳng dữ liệu JSON lồng nhau trong thế giới thực, có thể lồng các đối tượng json giá trị khóa và các mảng và đối tượng json bên trong mảng, v.v. JSON không bao gồm các bộ dữ liệu, vì vậy chúng tôi không phải lo lắng về những thứ đó.

Tôi tìm thấy cách triển khai bình luận bao gồm danh sách của @roneo cho câu trả lời được đăng bởi @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)

Kiểm tra nó:

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}

Và đó là công việc tôi cần thực hiện: Tôi ném bất kỳ json phức tạp nào vào đây và nó làm phẳng nó ra cho tôi.

Tất cả các khoản tín dụng cho https://github.com/ScriptSmith .


1

Tôi thực sự đã viết một gói được gọi là cherrypicker gần đây để đối phó với loại điều chính xác này vì tôi phải làm điều đó thường xuyên!

Tôi nghĩ đoạn mã sau sẽ cung cấp cho bạn chính xác những gì bạn đang theo đuổi:

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()

Bạn có thể cài đặt gói với:

pip install cherrypicker

... và có thêm tài liệu và hướng dẫn tại https://cherrypicker.readthedocs.io .

Các phương pháp khác có thể nhanh hơn, nhưng ưu tiên của gói này là làm cho các tác vụ đó trở nên dễ dàng . Nếu bạn có một danh sách lớn các đối tượng để làm phẳng, bạn cũng có thể yêu cầu CherryPicker sử dụng xử lý song song để tăng tốc mọi thứ.


Tôi thích cách tiếp cận khác.
Gergely M

0

Tôi luôn thích truy cập dictcác đối tượng thông qua .items(), vì vậy để làm phẳng các dicts tôi sử dụng trình tạo đệ quy sau flat_items(d). Nếu bạn muốn có dictmột lần nữa, chỉ cần bọc nó như thế này: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

Biến thể của từ điển lồng nhau Flatten này , nén các phím với max_level và bộ giảm tốc tùy chỉnh.

  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

Nếu bạn không nhớ các hàm đệ quy, đây là một giải pháp. Tôi cũng đã lấy tự do để bao gồm một loại trừ trong trường hợp có một hoặc nhiều giá trị bạn muốn duy trì.

Mã số:

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

Sử dụng:

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)

Đầu ra:

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

Tôi đã thử một số giải pháp trên trang này - mặc dù không phải tất cả - nhưng những giải pháp tôi đã thử không thể xử lý danh sách dict lồng nhau.

Hãy xem xét một dict như thế này:

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': []
                 }
            ]
        }
    }

Đây là giải pháp tạm thời của tôi:

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

sản xuất:

{
  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,
}

Một giải pháp tạm thời và nó không hoàn hảo.
GHI CHÚ:

  • nó không giữ các ký hiệu trống như address: {}cặp k / v.

  • nó sẽ không làm phẳng các ký tự trong các bộ dữ liệu lồng nhau - mặc dù có thể dễ dàng thêm bằng cách sử dụng bộ dữ liệu trăn hoạt động tương tự như danh sách.


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.