Làm thế nào để JSON serialization bộ?


148

Tôi có một Python setchứa các đối tượng __hash____eq__phương thức để đảm bảo không có bản sao nào được đưa vào bộ sưu tập.

Tôi cần json mã hóa kết quả này set, nhưng chuyển ngay cả một khoảng trống setcho json.dumpsphương thức tăng a TypeError.

  File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable

Tôi biết tôi có thể tạo một phần mở rộng cho json.JSONEncoderlớp có defaultphương thức tùy chỉnh , nhưng tôi thậm chí không chắc bắt đầu chuyển đổi qua đâu set. Tôi có nên tạo một từ điển trong số các setgiá trị trong phương thức mặc định và sau đó trả lại mã hóa trên đó không? Lý tưởng nhất là tôi muốn làm cho phương thức mặc định có thể xử lý tất cả các kiểu dữ liệu mà bộ mã hóa ban đầu mắc phải (Tôi đang sử dụng Mongo làm nguồn dữ liệu để ngày cũng có thể gây ra lỗi này)

Bất kỳ gợi ý theo đúng hướng sẽ được đánh giá cao.

BIÊN TẬP:

Cảm ơn câu trả lời! Có lẽ tôi nên chính xác hơn.

Tôi đã sử dụng (và nâng cấp) các câu trả lời ở đây để khắc phục những hạn chế của việc setdịch, nhưng có những khóa nội bộ cũng là một vấn đề.

Các đối tượng trong setlà các đối tượng phức tạp dịch sang __dict__, nhưng bản thân chúng cũng có thể chứa các giá trị cho các thuộc tính của chúng có thể không đủ điều kiện cho các loại cơ bản trong bộ mã hóa json.

Có rất nhiều loại khác nhau xuất hiện setvà hàm băm tính toán một id duy nhất cho thực thể, nhưng theo tinh thần thực sự của NoQuery thì không thể nói chính xác đối tượng con chứa gì.

Một đối tượng có thể chứa giá trị ngày cho starts, trong khi một đối tượng khác có thể có một số lược đồ khác không chứa khóa nào chứa các đối tượng "không nguyên thủy".

Đó là lý do tại sao giải pháp duy nhất tôi có thể nghĩ đến là mở rộng phương pháp JSONEncoderthay thế defaultphương thức để bật các trường hợp khác nhau - nhưng tôi không chắc chắn làm thế nào để giải quyết vấn đề này và tài liệu không rõ ràng. Trong các đối tượng lồng nhau, giá trị được trả về từ defaultgo by key, hay nó chỉ là một bao gồm / loại bỏ chung nhìn vào toàn bộ đối tượng? Làm thế nào để phương pháp đó chứa các giá trị lồng nhau? Tôi đã xem qua các câu hỏi trước đây và dường như không thể tìm ra cách tiếp cận tốt nhất cho mã hóa theo trường hợp cụ thể (điều không may là dường như những gì tôi sẽ cần phải làm ở đây).


3
tại sao dictvậy Tôi nghĩ rằng bạn muốn tạo ra một bộ listngoài và sau đó chuyển nó cho bộ mã hóa ... ví dụ:encode(list(myset))
Constantinius

2
Thay vì sử dụng JSON, bạn có thể sử dụng YAML (JSON thực chất là một tập hợp con của YAML).
Paolo Moretti

@PaoloMoretti: Nó có mang lại lợi thế gì không? Tôi không nghĩ các bộ nằm trong số các loại dữ liệu được YAML hỗ trợ toàn cầu và nó ít được hỗ trợ rộng rãi, đặc biệt là về các API.

@PaoloMoretti Cảm ơn bạn đã nhập, nhưng giao diện ứng dụng yêu cầu JSON làm kiểu trả về và yêu cầu này dành cho tất cả các mục đích cố định.
DeaconDesperado

2
@delnan Tôi đã đề xuất YAML vì nó có hỗ trợ riêng cho cả bộngày .
Paolo Moretti

Câu trả lời:


116

Ký hiệu JSON chỉ có một số kiểu dữ liệu nguyên gốc (đối tượng, mảng, chuỗi, số, booleans và null), vì vậy mọi thứ được tuần tự hóa trong JSON cần được thể hiện dưới dạng một trong các loại này.

Như đã trình bày trong tài liệu mô-đun json , chuyển đổi này có thể được thực hiện tự động bởi một JSONEncoderJSONDecoder , nhưng sau đó bạn sẽ được đưa lên một số cấu trúc khác mà bạn có thể cần (nếu bạn chuyển đổi tập vào một danh sách, sau đó bạn mất khả năng phục hồi thường xuyên danh sách; nếu bạn chuyển đổi bộ thành từ điển bằng cách sử dụng dict.fromkeys(s)thì bạn sẽ mất khả năng khôi phục từ điển).

Một giải pháp tinh vi hơn là xây dựng một loại tùy chỉnh có thể cùng tồn tại với các loại JSON gốc khác. Điều này cho phép bạn lưu trữ các cấu trúc lồng nhau bao gồm danh sách, bộ, dicts, số thập phân, đối tượng datetime, v.v.:

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
            return JSONEncoder.default(self, obj)
        return {'_python_object': pickle.dumps(obj)}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

Đây là một phiên mẫu cho thấy nó có thể xử lý các danh sách, ký tự và bộ:

>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]

>>> j = dumps(data, cls=PythonObjectEncoder)

>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]

Ngoài ra, có thể hữu ích khi sử dụng một kỹ thuật tuần tự hóa có mục đích chung hơn như YAML , Twisted Jelly hoặc mô-đun dưa chua của Python . Mỗi cái này hỗ trợ một loạt các kiểu dữ liệu lớn hơn nhiều.


11
Đây là lần đầu tiên tôi nghe nói YAML có mục đích chung hơn JSON ... o_O
Karl Knechtel

13
@KarlKnechtel YAML là một siêu bộ của JSON (rất gần). Nó cũng thêm các thẻ cho dữ liệu nhị phân, bộ, bản đồ được đặt hàng và dấu thời gian. Hỗ trợ nhiều kiểu dữ liệu hơn là ý của "mục đích chung hơn". Bạn dường như đang sử dụng cụm từ "mục đích chung" theo một nghĩa khác.
Raymond Hettinger

4
Đừng quên jsonpickle , dự định là một thư viện tổng quát để chọn các đối tượng Python thành JSON, giống như câu trả lời này cho thấy.
Jason R. Coombs

4
Kể từ phiên bản 1.2, YAML là một siêu bộ nghiêm ngặt của JSON. Tất cả JSON hợp pháp bây giờ là YAML hợp pháp. yaml.org/spec/1.2/spec.html
steveha

2
ví dụ mã này nhập JSONDecodernhưng không sử dụng nó
watsonic

115

Bạn có thể tạo một bộ mã hóa tùy chỉnh trả về a listkhi nó gặp a set. Đây là một ví dụ:

>>> import json
>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'

Bạn có thể phát hiện các loại khác theo cách này quá. Nếu bạn cần giữ lại danh sách đó thực sự là một bộ, bạn có thể sử dụng mã hóa tùy chỉnh. Một cái gì đó như return {'type':'set', 'list':list(obj)}có thể làm việc.

Để minh họa các kiểu lồng nhau, hãy xem xét tuần tự hóa điều này:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

Điều này gây ra lỗi sau:

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

Điều này chỉ ra rằng bộ mã hóa sẽ lấy listkết quả trả về và gọi đệ quy serializer trên các phần tử con của nó. Để thêm một serializer tùy chỉnh cho nhiều loại, bạn có thể làm điều này:

>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       if isinstance(obj, Something):
...          return 'CustomSomethingRepresentation'
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'

Cảm ơn, tôi đã chỉnh sửa câu hỏi để xác định rõ hơn rằng đây là loại điều tôi cần. Những gì tôi dường như không thể nắm bắt được là phương pháp này sẽ xử lý các đối tượng lồng nhau như thế nào. Trong ví dụ của bạn, giá trị trả về là danh sách cho tập hợp, nhưng nếu đối tượng được truyền vào là một tập hợp có ngày (một kiểu dữ liệu xấu khác) bên trong nó thì sao? Tôi có nên khoan qua các khóa trong chính phương thức mặc định không? Cảm ơn rất nhiều!
DeaconDesperado

1
Tôi nghĩ rằng mô-đun JSON xử lý các đối tượng lồng nhau cho bạn. Khi nó lấy lại được danh sách, nó sẽ lặp lại các mục trong danh sách đang cố mã hóa từng mục. Nếu một trong số chúng là một ngày, defaulthàm sẽ được gọi lại, lần này với objviệc là một đối tượng ngày, vì vậy bạn chỉ cần kiểm tra nó và trả về một đại diện ngày.
jterrace

Vì vậy, phương thức mặc định có thể chạy được nhiều lần cho bất kỳ một đối tượng nào được truyền cho nó, vì nó cũng sẽ xem xét các khóa riêng lẻ sau khi được "liệt kê"?
DeaconDesperado

Sắp xếp, nó sẽ không được gọi nhiều lần cho cùng một đối tượng, nhưng nó có thể tái phát thành trẻ em. Xem câu trả lời cập nhật.
jterrace

Làm việc chính xác như bạn mô tả. Tôi vẫn phải tìm ra một số lỗi, nhưng hầu hết có lẽ là những thứ có thể được tái cấu trúc. Cảm ơn rất nhiều cho hướng dẫn của bạn!
DeaconDesperado

7

Tôi đã điều chỉnh giải pháp Raymond Hettinger cho python 3.

Đây là những gì đã thay đổi:

  • unicode biến mất
  • cập nhật cuộc gọi đến phụ huynh defaultvớisuper()
  • sử dụng base64để tuần tự hóa bytesloại thành str(vì dường như bytestrong python 3 không thể chuyển đổi thành JSON)
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]

4
Mã được hiển thị ở cuối câu trả lời này cho một câu hỏi liên quan thực hiện điều tương tự bằng cách [chỉ] giải mã và mã hóa đối tượng byte json.dumps()trả về / từ 'latin1', bỏ qua những base64thứ không cần thiết.
martineau

6

Chỉ từ điển, Danh sách và các loại đối tượng nguyên thủy (int, chuỗi, bool) có sẵn trong JSON.


5
"Loại đối tượng nguyên thủy" không có nghĩa gì khi nói về Python. "Đối tượng tích hợp" có ý nghĩa hơn, nhưng ở đây quá rộng (đối với người mới bắt đầu: nó bao gồm các ký tự, danh sách và cả bộ). (Thuật ngữ JSON có thể khác nhau mặc dù.)

chuỗi đối tượng số mảng đúng sai null
Joseph Le Brech

6

Bạn không cần phải tạo một lớp mã hóa tùy chỉnh để cung cấp defaultphương thức - nó có thể được truyền vào dưới dạng đối số từ khóa:

import json

def serialize_sets(obj):
    if isinstance(obj, set):
        return list(obj)

    return obj

json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)

kết quả trong [1, 2, 3]tất cả các phiên bản Python được hỗ trợ.


4

Nếu bạn chỉ cần mã hóa các bộ, không phải các đối tượng Python chung và muốn giữ cho nó dễ đọc với con người, có thể sử dụng phiên bản đơn giản của câu trả lời của Raymond Hettinger:

import json
import collections

class JSONSetEncoder(json.JSONEncoder):
    """Use with json.dumps to allow Python sets to be encoded to JSON

    Example
    -------

    import json

    data = dict(aset=set([1,2,3]))

    encoded = json.dumps(data, cls=JSONSetEncoder)
    decoded = json.loads(encoded, object_hook=json_as_python_set)
    assert data == decoded     # Should assert successfully

    Any object that is matched by isinstance(obj, collections.Set) will
    be encoded, but the decoded value will always be a normal Python set.

    """

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        else:
            return json.JSONEncoder.default(self, obj)

def json_as_python_set(dct):
    """Decode json {'_set_object': [1,2,3]} to set([1,2,3])

    Example
    -------
    decoded = json.loads(encoded, object_hook=json_as_python_set)

    Also see :class:`JSONSetEncoder`

    """
    if '_set_object' in dct:
        return set(dct['_set_object'])
    return dct

1

Nếu bạn chỉ cần kết xuất nhanh và không muốn triển khai bộ mã hóa tùy chỉnh. Bạn có thể sử dụng như sau:

json_string = json.dumps(data, iterable_as_array=True)

Điều này sẽ chuyển đổi tất cả các bộ (và các lần lặp khác) thành các mảng. Chỉ cần lưu ý rằng các trường đó sẽ ở lại mảng khi bạn phân tích lại json. Nếu bạn muốn bảo tồn các loại, bạn cần viết bộ mã hóa tùy chỉnh.


7
Khi tôi thử điều này, tôi nhận được: TypeError: __init __ () có một đối số từ khóa không mong đợi 'iterable_as_array'
atm

Bạn cần cài đặt Simplejson
JerryBringer 11/03/19

nhập Simplejson dưới dạng json và sau đó json_opes = json.dumps (data, iterable_as_array = True) hoạt động tốt trong Python 3.6
fraverta

1

Một thiếu sót của giải pháp được chấp nhận là đầu ra của nó rất cụ thể. Tức là đầu ra json thô của nó không thể được quan sát bởi một người hoặc được tải bởi ngôn ngữ khác (ví dụ: javascript). thí dụ:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

Sẽ có em:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

Tôi có thể đề xuất một giải pháp hạ cấp tập hợp thành một dict chứa danh sách trên đường ra và quay lại tập hợp khi được nạp vào python bằng cùng một bộ mã hóa, do đó duy trì khả năng quan sát và thuyết bất khả tri của ngôn ngữ:

from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        elif isinstance(obj, set):
            return {"__set__": list(obj)}
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '__set__' in dct:
        return set(dct['__set__'])
    elif '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])

Điều này giúp bạn:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

Ghi chú rằng việc tuần tự hóa một từ điển có thành phần với khóa "__set__"sẽ phá vỡ cơ chế này. Vì vậy, __set__bây giờ đã trở thành một dictkhóa dành riêng . Rõ ràng là cảm thấy tự do để sử dụng một khóa khác, sâu hơn.

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.