扩展了类似dict的子类,无需额外支持转换和JSON转储

时间:2018-09-13 12:53:36

标签: python json dictionary casting

我需要创建支持dict的类t的实例T 都使用dict(**t)“投射”到一个真实的字典,而无需恢复 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的实例,该类添加了例如一种 键default “即时” 插入其项目,因此无法进行预先插入(实际上,我希望合并键 从一个或多个T实例显示出来,这是对实数的简化, 更复杂的类)。

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在C级别上进行显式测试。

当我尝试将TdictMutableMapping,我确实得到了与仅使用时相同的结果 dict子类。

我当然可以认为它是json自卸车没有的错误 已更新为假定(的具体子类) collections.abc.Mapping是可转储的。但是即使被承认 作为一个错误,并在将来的某些Python版本中得到修复,我不认为 这样的修复将应用于旧版本的Python。

第1季度:我如何制作T实施,它是的子类 dict,才能正常投放?
第二季度:如果第一季度没有答案,会回答吗? 如果我使C级类返回正确的值,则可以工作 PyDict_Check,但不执行任何实际实现(并且 然后将TMutableMapping都设为该子类(我不 认为添加这样一个不完整的C级字典会起作用,但是我没有 尝试过),而这个傻子json.dumps()会成为傻瓜吗?
第3季度,这是否会使第一种方法像第一个示例一样正常工作?


实际代码,即 更复杂的是我的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的类,这是不平凡的,并且与标准很好地协作 库。

2 个答案:

答案 0 :(得分:2)

由于这里的真正问题实际上是json.dumps的默认编码器无法将MutableMapping(或您实际示例中的ruamel.yaml.comments.CommentedMap)视为命令,而不是告诉人们如前所述,将default的{​​{1}}参数设置为json.dumps函数,您可以使用json_defaultfunctools.partial设为{{1} } json_default的参数,这样人们在使用您的软件包时就不必做任何其他事情:

default

或者,如果您需要允许人们指定自己的json.dumps参数甚至自己的from functools import partial json.dumps = partial(json.dumps, default=json_default) 子类,则可以在default周围使用包装器,以便将{{1由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,它在Python 3.3之前不可用,即使那样,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) 在Python 3.5和{{3}之前不可用}包,是Python 3.3的[('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.5+的funcsigs的代码复制并粘贴到您的模块中,并在导入{{1 }}:

inspect.BoundArguments.apply_defaults

答案 1 :(得分:1)

对Q1和Q2的回答是:“您不能”。 “不”

简而言之:您无法在Python中即时添加密钥并获得JSON输出 以及(不修补json.dumps或向其提供default)。

这样做的原因是,要使JSON完全起作用,您需要 您的类是dict的子类(或其他在上实现的对象 C级),以便其调用PyDict_Check()返回非零 (这意味着objectheader中的tp_flags字段具有 Py_TPFLAGS_DICT_SUBCLASS位置1)。

强制转换(dict(**data)))首先在C级别执行此检查,如下所示: 好(在dictobject.c:dict_merge中)。但是在方式上有所不同 事情从那里开始。转储JSON时,代码实际上 使用子类提供的例程迭代键/值 如果可用的话。

相反,如果有任何子类,则转换看起来不 继续并复制C级实现中的值( dictruamel.ordereddict等)。

在投射不是dict子类的内容时, 调用普通的Python类级别接口(__iter__来获取 键/值对。这就是为什么子类MutableMapping使强制转换 可以,但是不幸的是,它中断了JSON转储。

创建精简的C级类并返回非零值是不够的 PyDict_Check(),因为强制转换会在C级上对该类的键和值进行迭代。

透明地实现此目标的唯一方法是通过实现C级字典(如class)来完成 动态插入密钥default及其值。它必须通过伪造一个 长度比实际条目数大一倍,并且 以某种方式在ma_keysma_values的C级实现索引 多余的物品。如果可能的话,这将非常困难,因为dict_merge假设 修复了有关源对象内部很多知识的知识。

修复json.dumps的另一种方法是修复dict_merge,但后者会影响 很多代码的速度都受到负面影响,因此不太可能发生(而且不会 可以在旧版本的Python上追溯执行。