【问题标题】:How can I have json.dumps treat my class as a dict?我怎样才能让 json.dumps 将我的班级视为字典?
【发布时间】:2021-05-01 19:13:59
【问题描述】:

我想创建一个自定义 Python 类,该类可以像字典一样进行 JSON 序列化。以Python 的duck-typing 命名,我想我可以创建一个看起来和嘎嘎声完全像字典的类。但是,对于json.dumps,下面显示的类显然不够像dict——下面的代码会产生错误TypeError: Object of type TotallyADict is not JSON serializable。我可以对 TotallyADict 进行哪些更改,以便 json.dumps 的默认编码器将输出 {"a": 1, "b": 2, "c": 3}

我知道这个直接的问题可以通过创建自定义编码器来解决,但在这个特定问题已经从中提炼出来的更大问题中,这不是一个可接受的解决方案。

另一个尝试的解决方案是让 TotallyADict 继承自 dict 而不是 MutableMapping。这不会引发任何异常,但在这种情况下 json.dumps(x) 会产生 {};显然,json.dumps 的默认编码器用于 dicts 的数据源不是以下任何被覆盖的方法。

我在这里想要的是能够使用属性语义 (x.c = x.a + x.b) 但仍然序列化为 JSON 对象。因此,一个似乎不起作用的可能建议是 TypedDict(必须是 x['c'] = x['a'] + x['b'])。通过__setattr____getattribute__ 拦截属性分配和检索并重定向到从dict 继承的条目self 似乎工作得很好,所以这是我的默认解决方案。但令我惊讶的是,有一次我真的想使用鸭式打字而不是严格(ish)打字,它似乎不起作用。

from collections.abc import MutableMapping
import json


class TotallyADict(MutableMapping):
  def __init__(self, a, b, c):
    self.a = a
    self.b = b
    self.c = c
    self._fields = {'a', 'b', 'c'}

  def __getitem__(self, key):
    if key in self._fields:
      return getattr(self, key)
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def __setitem__(self, key, value):
    if key in self._fields:
      setattr(self, key, value)
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def __delitem__(self, key):
    raise RuntimeError('Cannot delete fields from {}'.format(type(self).__name__))

  def __iter__(self):
    return iter(self._fields)

  def __len__(self):
    return len(self._fields)

  def __contains__(self, k):
    return k in self._fields

  def copy(self):
    return type(self)(**{k: getattr(self, k) for k in self._fields})

  def __repr__(self):
    return '{' + ', '.join('"{}": {}'.format(k, repr(getattr(self, k))) for k in self._fields) + '}'

  def get(self, key, default=None):
    if key in self._fields:
      value = getattr(self, key)
      if value is None:
        value = default
      return value
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def setdefault(self, key, default=None):
    if key in self._fields:
      value = getattr(self, key)
      if value is None:
        value = default
        setattr(self, key, value)
      return value
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def pop(self, key, value=None):
    raise RuntimeError('Cannot delete fields from {}'.format(type(self).__name__))

  def keys(self):
    return self._fields

  def items(self):
    return [(k, getattr(self, k)) for k in self._fields]

  def values(self):
    return [getattr(self, k) for k in self._fields]

  def __eq__(self, other):
    if type(self) is type(other):
      for k in self._fields:
        if getattr(self, k) != getattr(other, k):
          return False
      return True
    else:
      return False

  def __ne__(self, other):
    return not self.__eq__(other)


x = TotallyADict(1, 2, 3)
print(json.dumps(x))

【问题讨论】:

  • 在阅读了 JSON 的文档后,您可能缺少 default 方法 docs.python.org/3/library/json.html#json.JSONEncoder
  • 是的,调用json.dumps(x, default=lambda d: {k: d[k] for k in d._fields}) 或类似的方法确实会产生预期的结果。但是,这并不能回答为什么TotallyADict 不会在json.dumps 中将dict 转换为json.dumps,或者如何将其更改为dict

标签: python json dictionary duck-typing


【解决方案1】:

这里的问题是您的 _fields 变量。这不会序列化为 JSON 对象,因为 {'c', 'b', 'a'} 不是有效的 json。如果您查看x.__dict__ 属性,您可以看到该对象将被表示为什么。

{'a': 1, 'b': 2, 'c': 3, '_fields': {'c', 'b', 'a'}}

如果您将 _fields 更改为列表,您还可以在 JSON.dumps 中使用 default 参数

这些是我所做的更改,以使您正在寻找工作

self._fields = ['a', 'b', 'c']
print(json.dumps(x, default=vars))

这是我的 Canges 的完整代码。

from collections.abc import MutableMapping
import json


class TotallyADict(MutableMapping):
  def __init__(self, a, b, c):
    self.a = a
    self.b = b
    self.c = c
    self._fields = ['a', 'b', 'c']

  def __getitem__(self, key):
    if key in self._fields:
      return getattr(self, key)
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def __setitem__(self, key, value):
    if key in self._fields:
      setattr(self, key, value)
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def __delitem__(self, key):
    raise RuntimeError('Cannot delete fields from {}'.format(type(self).__name__))

  def __iter__(self):
    return iter(self._fields)

  def __len__(self):
    return len(self._fields)

  def __contains__(self, k):
    return k in self._fields

  def copy(self):
    return type(self)(**{k: getattr(self, k) for k in self._fields})

  def __repr__(self):
    return '{' + ', '.join('"{}": {}'.format(k, repr(getattr(self, k))) for k in self._fields) + '}'

  def get(self, key, default=None):
    if key in self._fields:
      value = getattr(self, key)
      if value is None:
        value = default
      return value
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def setdefault(self, key, default=None):
    if key in self._fields:
      value = getattr(self, key)
      if value is None:
        value = default
        setattr(self, key, value)
      return value
    else:
      raise KeyError('"{}" is not a field in {}'.format(key, type(self).__name__))

  def pop(self, key, value=None):
    raise RuntimeError('Cannot delete fields from {}'.format(type(self).__name__))

  def keys(self):
    return self._fields

  def items(self):
    return [(k, getattr(self, k)) for k in self._fields]

  def values(self):
    return [getattr(self, k) for k in self._fields]

  def __eq__(self, other):
    if type(self) is type(other):
      for k in self._fields:
        if getattr(self, k) != getattr(other, k):
          return False
      return True
    else:
      return False

  def __ne__(self, other):
    return not self.__eq__(other)


x = TotallyADict(1, 2, 3)

print(json.dumps(x, default=vars))

您也可以尝试使用UserDict

https://docs.python.org/3/library/collections.html#collections.UserDict

【讨论】:

  • 根据问题的第一段,期望的输出是{"a": 1, "b": 2, "c": 3},但这种方法输出{"a": 1, "b": 2, "c": 3, "_fields": ["a", "b", "c"]}
【解决方案2】:

在某些情况下,最简单的解决方案是最好的解决方案。在这种情况下,请创建一个 to_dict() 函数,该函数将自定义类中的数据作为 Python 字典在 json 转储之前返回。

这样,您可以在闲暇时操作类中的数据,并在其他库需要字典时将其转换为字典。然后,如果您需要相反的内容,只需编写另一个将 dict 解析为您的自定义类的函数。

由于这个类是用来保存数据的,我推荐使用DataClasses

然后你可以将这个函数添加到你的类中以获取它的属性作为一个字典:

from dataclasses import dataclass, asdict

def get_as_dict(self):
        return {k: v for k, v in asdict(self).items() if self._dataclass_fields_[k].repr}

【讨论】:

  • 没有理由在这里使用dataclass,因为没有它,您的get_as_dict 可以简化为return {k: self[k] for k in self._fields},但这并不能回答为什么TotallyADict 不被视为dict 的问题json.dumps 本着 Python 的鸭式打字精神。
猜你喜欢
  • 2023-03-18
  • 2016-10-08
  • 2020-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多