python - 扩展的类 dict 子类以支持强制转换和 JSON 转储,无需额外功能
问题描述
我需要创建一个t
类 dict 类的实例,该类T
支持使用 将“强制转换”为真正的 dict dict(**t)
,而无需恢复为 do
dict([(k, v) for k, v in t.items()])
。以及支持使用标准json
库转储为 JSON,无需扩展普通的 JSON 编码器(即没有为default
参数提供函数)。
作为t
一个正常人dict
,两者都有效:
import json
def dump(data):
print(list(data.items()))
try:
print('cast:', dict(**data))
except Exception as e:
print('ERROR:', e)
try:
print('json:', json.dumps(data))
except Exception as e:
print('ERROR:', e)
t = dict(a=1, b=2)
dump(t)
印刷:
[('a', 1), ('b', 2)]
cast: {'a': 1, 'b': 2}
json: {"a": 1, "b": 2}
但是,我想t
成为该类的一个实例,例如“即时”向其项目T
添加一个键,因此不可能预先插入(实际上我希望显示来自一个或多个 T 实例的合并键,这个是对那个真实的、更复杂的类的简化)。default
class T(dict):
def __getitem__(self, key):
if key == 'default':
return 'DEFAULT'
return dict.__getitem__(self, key)
def items(self):
for k in dict.keys(self):
yield k, self[k]
yield 'default', self['default']
def keys(self):
for k in dict.keys(self):
yield k
yield 'default'
t = T(a=1, b=2)
dump(t)
这给出了:
[('a', 1), ('b', 2), ('default', 'DEFAULT')]
cast: {'a': 1, 'b': 2}
json: {"a": 1, "b": 2, "default": "DEFAULT"}
并且演员无法正常工作,因为没有“默认”键,而且我不知道要提供哪个“魔术”功能才能使演员正常工作。
当我建立T
在实现的功能上collections.abc
并在子类中提供所需的抽象方法时,强制转换工作:
from collections.abc import MutableMapping
class TIter:
def __init__(self, t):
self.keys = list(t.d.keys()) + ['default']
self.index = 0
def __next__(self):
if self.index == len(self.keys):
raise StopIteration
res = self.keys[self.index]
self.index += 1
return res
class T(MutableMapping):
def __init__(self, **kw):
self.d = dict(**kw)
def __delitem__(self, key):
if key != 'default':
del self.d[key]
def __len__(self):
return len(self.d) + 1
def __setitem__(self, key, v):
if key != 'default':
self.d[key] = v
def __getitem__(self, key):
if key == 'default':
return 'DEFAULT'
# return None
return self.d[key]
def __iter__(self):
return TIter(self)
t = T(a=1, b=2)
dump(t)
这使:
[('a', 1), ('b', 2), ('default', 'DEFAULT')]
cast: {'a': 1, 'b': 2, 'default': 'DEFAULT'}
ERROR: Object of type 'T' is not JSON serializable
JSON 转储失败,因为该转储程序无法处理
MutableMapping
子类,它使用PyDict_Check
.
当我尝试创建T
两者的子类时dict
,
MutableMapping
我确实得到了与仅使用子类时相同的结果dict
。
我当然可以认为这是一个错误,即json
转储程序尚未更新以假设(具体子类)
collections.abc.Mapping
是可转储的。但即使它被承认为一个错误并在未来的 Python 版本中得到修复,我认为这样的修复不会应用于旧版本的 Python。
Q1:如何使作为T
, 的子类的实现
dict
正确转换?
Q2:如果 Q1 没有答案,如果我创建一个返回正确值
PyDict_Check
但不执行任何实际实现的 C 级类(然后创建T
它的子类以及MutableMapping
(我不要认为添加这样一个不完整的 C 级别的字典会起作用,但我没有尝试过),这个傻瓜json.dumps()
吗?
Q3 这是让两者像第一个例子一样工作的完全错误的方法吗?
实际代码要复杂得多,它是我的ruamel.yaml
库的一部分,它必须在 Python 2.7 和 Python 3.4+ 上工作。
只要我不能解决这个问题,我就必须告诉那些曾经有功能正常的 JSON 转储器(没有额外参数)的人使用:
def json_default(obj):
if isinstance(obj, ruamel.yaml.comments.CommentedMap):
return obj._od
if isinstance(obj, ruamel.yaml.comments.CommentedSeq):
return obj._lst
raise TypeError
print(json.dumps(d, default=json_default))
,告诉他们使用与默认(往返)加载程序不同的加载程序。例如:
yaml = YAML(typ='safe')
data = yaml.load(stream)
,在类上实现一些.to_json()
方法T
并让用户ruamel.yaml
意识到这一点
,或者回到子类化dict
并告诉人们去做
dict([(k, v) for k, v in t.items()])
这些都不是真正友好的,并且表明不可能制作一个非平凡且与标准库很好地配合的类 dict 类。
解决方案
由于这里真正的问题是真正json.dumps
的默认编码器无法将MutableMapping
(或ruamel.yaml.comments.CommentedMap
在您的实际示例中)视为字典,而不是像您提到的那样告诉人们将default
参数设置json.dumps
为您的函数,您可以使用它来设置默认值参数的值,以便人们在使用您的包时不必做任何不同的事情:json_default
functools.partial
json_default
default
json.dumps
from functools import partial
json.dumps = partial(json.dumps, default=json_default)
或者如果你需要允许人们指定他们自己的default
参数甚至他们自己的json.JSONEncoder
子类,你可以使用一个包装器,json.dumps
以便它包装参数default
指定的default
函数和参数default
指定的自定义编码器的方法,以指定的cls
为准:
import inspect
class override_json_default:
# keep track of the default methods that have already been wrapped
# so we don't wrap them again
_wrapped_defaults = set()
def __call__(self, func):
def override_default(default_func):
def default_wrapper(o):
o = default_func(o)
if isinstance(o, MutableMapping):
o = dict(o)
return o
return default_wrapper
def override_default_method(default_func):
def default_wrapper(self, o):
try:
return default_func(self, o)
except TypeError:
if isinstance(o, MutableMapping):
return dict(o)
raise
return default_wrapper
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
default = bound.arguments.get('default')
if default:
bound.arguments['default'] = override_default(default)
encoder = bound.arguments.get('cls')
if not default and not encoder:
bound.arguments['cls'] = encoder = json.JSONEncoder
if encoder:
default = getattr(encoder, 'default')
if default not in self._wrapped_defaults:
default = override_default_method(default)
self._wrapped_defaults.add(default)
setattr(encoder, 'default', default)
return func(*bound.args, **bound.kwargs)
sig = inspect.signature(func)
return wrapper
json.dumps=override_json_default()(json.dumps)
以便以下测试代码具有自定义default
函数和处理datetime
对象的自定义编码器,以及没有自定义default
或编码器的测试代码:
from datetime import datetime
def datetime_encoder(o):
if isinstance(o, datetime):
return o.isoformat()
return o
class DateTimeEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return super(DateTimeEncoder, self).default(o)
def dump(data):
print(list(data.items()))
try:
print('cast:', dict(**data))
except Exception as e:
print('ERROR:', e)
try:
print('json with custom default:', json.dumps(data, default=datetime_encoder))
print('json wtih custom encoder:', json.dumps(data, cls=DateTimeEncoder))
del data['c']
print('json without datetime:', json.dumps(data))
except Exception as e:
print('ERROR:', e)
t = T(a=1, b=2, c=datetime.now())
dump(t)
都会给出正确的输出:
[('a', 1), ('b', 2), ('c', datetime.datetime(2018, 9, 15, 23, 59, 25, 575642)), ('default', 'DEFAULT')]
cast: {'a': 1, 'b': 2, 'c': datetime.datetime(2018, 9, 15, 23, 59, 25, 575642), 'default': 'DEFAULT'}
json with custom default: {"a": 1, "b": 2, "c": "2018-09-15T23:59:25.575642", "default": "DEFAULT"}
json wtih custom encoder: {"a": 1, "b": 2, "c": "2018-09-15T23:59:25.575642", "default": "DEFAULT"}
json without datetime: {"a": 1, "b": 2, "default": "DEFAULT"}
正如评论中所指出的,上面的代码使用inspect.signature
,直到 Python 3.3 才可用,即使那样,inspect.BoundArguments.apply_defaults
直到 Python 3.5 才可用,并且funcsigs
包,Python 3.3 的反向移植inspect.signature
,也没有该apply_defaults
方法。为了使代码尽可能向后兼容,您可以简单地将 Python 3.5+ 的代码复制并粘贴inspect.BoundArguments.apply_defaults
到您的模块中,并根据需要将其分配为inspect.BoundArguments
after importing的属性funcsigs
:
from collections import OrderedDict
if not hasattr(inspect, 'signature'):
import funcsigs
for attr in funcsigs.__all__:
setattr(inspect, attr, getattr(funcsigs, attr))
if not hasattr(inspect.BoundArguments, 'apply_defaults'):
def apply_defaults(self):
arguments = self.arguments
new_arguments = []
for name, param in self._signature.parameters.items():
try:
new_arguments.append((name, arguments[name]))
except KeyError:
if param.default is not funcsigs._empty:
val = param.default
elif param.kind is funcsigs._VAR_POSITIONAL:
val = ()
elif param.kind is funcsigs._VAR_KEYWORD:
val = {}
else:
continue
new_arguments.append((name, val))
self.arguments = OrderedDict(new_arguments)
inspect.BoundArguments.apply_defaults = apply_defaults
推荐阅读
- event-handling - 需要 SSIS 包 OnError 事件处理程序的可重复方法
- javascript - 使用 React Router 重新加载页面时出现 404 错误
- c++ - 在 C++ 中打开浏览器(没有窗口)并单击不同的按钮
- html - 用图案图像填充旋转的圆形 SVG
- php - Twitter API - 显示多个指定用户的最新推文
- yocto - 将 yocto 图像闪烁到 cm3 emmc
- aurelia - Aurelia HttpClient.Fetch 从 Auth0 获取令牌,但在 Postman 中工作正常
- c# - 尝试通过 TCP 将 RPI 连接到 Unity
- javascript - JavaScript; 将文件的内容放入数组块中
- python-3.x - 从 Python 2 加载 Python 3 泡菜