json.dump не вызывает default или cls

#python #json #python-3.x #datetime

#python #json #python-3.x #дата и время

Вопрос:

Пытаюсь сериализовать dict, содержащий объекты datetime в качестве ключей для json. В нескольких других ответах предлагалось использовать json.dump параметры default или cls однако, ни один из этих методов, похоже, даже не вызывается. Смотрите MWES ниже. Чего я не понимаю?

Используя Default

 from datetime import datetime
import json

def default(obj):
    print("Default Called")
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()

test = {datetime(1970, 1, 1, 8, 0, 0): 10}

with open("output.json", "w") as fo:
    json.dump(test, fo, default=default)
  
 Traceback (most recent call last):
  File "test.py", line 21, in <module>
    json.dump(test, fo, default=default)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/__init__.py", line 179, in dump
    for chunk in iterable:
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 431, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 376, in _iterencode_dict
    raise TypeError(f'keys must be str, int, float, bool or None, '
TypeError: keys must be str, int, float, bool or None, not datetime
  

Используя cls

 from datetime import datetime
import json

class DateEncoder(json.JSONEncoder):
    def default(self, obj):
        print("Default called")
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()

        # Let the base class default method raise the TypeError
        return json.JSONEncoder.default(self, obj)

test = {datetime(1970, 1, 1, 8, 0, 0): 10}

with open("output.json", "w") as fo:
    json.dump(test, fo, cls=DateEncoder)
  
 Traceback (most recent call last):
  File "test.py", line 16, in <module>
    json.dump(test, fo, cls=DateEncoder)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/__init__.py", line 179, in dump
    for chunk in iterable:
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 431, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 376, in _iterencode_dict
    raise TypeError(f'keys must be str, int, float, bool or None, '
TypeError: keys must be str, int, float, bool or None, not datetime
  

Комментарии:

1. объекты datetime неизменяемы docs.python.org/3/library/datetime.html#available-types

Ответ №1:

TLDR: .default вызывается только для dict значений, а не ключей.

Чтобы уточнить ответ Абхишека Кумара; после просмотра json.JSONEncoder источника, .default вызывается только для dict значений, которые не являются сериализуемыми, никогда ключей. Ключи должны быть одним из str , int , float bool или None .

Чтобы использовать datetime объекты в качестве ключей, .iterencode необходимо перезаписать, чтобы использовать рекурсивный метод препроцессора для преобразования datetime в str :

 test = {
        "key_1": "Value_1",
        "key_2": 10,
        "key_3": ["list_"   str(i) for i in range(5)],
        "key_4": {"nestkey_"   str(i) : "nestvalue_"   str(i) for i in range(5) },
        "key_5": datetime.datetime(1970, 1, 1, 8, 0, 0),
        datetime.datetime(1970, 1, 1, 8, 0, 0): "datetime_key"
}

class DateTimeEncoder(json.JSONEncoder):

    def _preprocess_date(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime, datetime.timedelta)):
            return str(obj)
        elif isinstance(obj, dict):
            return {self._preprocess_date(k): self._preprocess_date(v) for k,v in obj.items()}
        elif isinstance(obj, list):
            return [self._preprocess_date(i) for i in obj]
        return obj

    def default(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime, datetime.timedelta)):
            return str(obj)
        return super().default(obj)

    def iterencode(self, obj):
        return super().iterencode(self._preprocess_date(obj))

with open('output.json', 'w') as fo:
    json.dump(test, fo, cls=DateTimeEncoder)
  

Очевидно, что это дорогостоящая операция для больших словарей, поэтому следует соблюдать некоторую осторожность. Кроме того, было бы неплохо json.JSONEncoder обновиться, чтобы использовать default как для ключей, так и для значений — проблема, созданная по адресу:https://bugs.python.org/issue41569

Комментарии:

1. Кажется, iterencode нужен параметр _one_shot: def iterencode(self, obj,_one_shot):

Ответ №2:

вы можете попробовать это :

 class DatesToStrings(json.JSONEncoder):
    def _encode(self, obj):
        if isinstance(obj, dict):
            def transform_date(o):
                return self._encode(o.isoformat() if isinstance(o, datetime) else o)
            return {transform_date(k): transform_date(v) for k, v in obj.items()}
        else:
            return obj

    def encode(self, obj):
        return super(DatesToStrings, self).encode(self._encode(obj))
  
 with open("output.json", "w") as fo:
    json.dump( json.dumps( test, cls=DatesToStrings), fo, cls=DatesToStrings)

  

Комментарии:

1. Спасибо, Абхишек, но, к сожалению, он все еще возвращается TypeError: Object of type set is not JSON serializable

2. Вы пробовали это :: with open("out.json", "w") as fo: json.dump( json.dumps( test, cls=DatesToStrings), fo)

3. Да, я пробовал обе версии. Не могли бы вы рассказать мне, как вы определили test пожалуйста?

4. я просто скопировал ваш тест : test = {datetime(1970, 1, 1, 8, 0, 0): 10}

5. Заметил тип на моей стороне. Да, это работает. Спасибо. Есть идеи, почему мой исходный пост не работает?