代码之家  ›  专栏  ›  技术社区  ›  Anthon

扩展的类dict子类,支持强制转换和JSON转储,无需附加

  •  9
  • Anthon  · 技术社区  · 6 年前

    我需要创建一个实例 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的例子来看,这是对真实的简化, 更复杂,类)。

    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"}
    

    并且由于没有键“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型 迪克特 ,得到的结果与仅使用 这个 迪克特 子类。

    我当然可以认为 翻车机没有 已更新以假定(的具体子类) collections.abc.Mapping 是可倾倒的。但即使它被承认 作为一个bug,在未来的Python版本中得到修复,我不认为 这样的修复将应用于较旧版本的Python。

    第一季度 :我如何制作 T型 实现,它是 迪克特 ,以便正确铸造?
    问题2 如果我生成一个C级类,该类返回 皮迪克特支票 但不做任何实际的实现(和 那就做 T型 它的一个子类 可变映射 (我没有 认为添加这样一个不完整的C级dict可以工作,但是我没有 试过了),这个傻瓜 json.dumps() ?
    这是不是一个完全错误的方法,让两者都像第一个例子一样工作?


    实际代码,即 更复杂的是 ruamel.yaml 图书馆必须

    只要我不能解决这个问题,我就得告诉人们 要使用的正在运行的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型 让用户 属于 意识到这点

    ,或返回子类化 迪克特 告诉人们去做

     dict([(k, v) for k, v in t.items()])
    

    没有一个是真正友好的,表明这是不可能的 创建一个类似dict的类,该类不琐碎,并且与标准很好地协作 图书馆。

    3 回复  |  直到 6 年前
        1
  •  3
  •   blhsing    6 年前

    因为这里真正的问题是 json.dumps 默认编码器无法考虑 MutableMapping (或 ruamel.yaml.comments.CommentedMap 在你的现实世界的例子)作为一个口述,而不是告诉人们设置 default json.dumps文件 给你的 json_default 就像你提到的那样,你可以使用 functools.partial 使 json默认 违约 json.dumps文件 这样人们在使用您的软件包时就不必做任何不同的事情:

    from functools import partial
    json.dumps = partial(json.dumps, default=json_default)
    

    或者如果你需要允许人们指定他们自己的 违约 参数,甚至他们自己的 json.JSONEncoder 子类,可以使用包装器 json.dumps文件 所以它把 由指定的函数 违约 参数和 违约 方法指定的自定义编码器的 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)
    

    所以下面的测试代码 违约 函数和处理 datetime 对象,以及没有自定义的对象 违约 或编码器:

    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 ,直到Python3.3才可用,即使如此, inspect.BoundArguments.apply_defaults funcsigs 包,Python3.3的一个后台端口 检查签名 ,没有 apply_defaults 方法也一样。为了使代码尽可能向后兼容,只需复制并粘贴Python3.5+的代码 inspect.BoundArguments.apply_defaults 并将其作为 inspect.BoundArguments 导入后 必要时:

    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
    
        2
  •  1
  •   Anthon    6 年前

    问题1和问题2的答案是:“你不能”resp不”

    同样(不修补json.dumps或提供 default 对它)。

    原因是,要让JSON工作,您需要 你的a级子类 dict (或在 C级),以便其调用 PyDict_Check() 返回非零 Py_TPFLAGS_DICT_SUBCLASS位集)。

    演员( dict(**data)) )首先在C层检查 嗯(在 dictobject.c:dict_merge 事情从那里开始。当转储JSON时,代码实际上 使用子类提供的例程在键/值上迭代 如果有的话。

    相反,cast不看是否有任何子类 继续并复制C级实现中的值( 迪克特 , ruamel.ordereddict 等等)。

    当铸造不是 迪克特 ,然后 普通Python类级接口( __iter__ )是为了得到 键/值对。这就是为什么子类化MutableMapping使casting 可以,但不幸的是它打破了JSON转储。

    创建一个在 PyDict_Check() ,因为转换将在C级别上对该类的键和值进行迭代。

    要透明地实现这一点,唯一的方法是通过实现一个C级的类dict,这样 动态插入键 以及它的价值。必须通过假装 大于实际条目数的长度,并且 以某种方式在C级别实现索引 ma_keys ma_values 得到那个 额外项目。如果可能的话,那就很难了,因为 dict_merge 假设 修复了有关源对象相当多内部内容的知识。

    json.dumps 是为了修好 dict_合并 ,但后者会影响 也可以追溯到旧版本的Python)。

        3
  •  -1
  •   blhsing    6 年前

    你可以用完全不同的方法来解决这个问题。而不是试图在 'default' 是动态请求的,您可以使用键初始化dict '默认' 设置为所需的值,然后 保护 的价值 通过重写所有可能更改dict内容的方法,使密钥的值 '默认' 从未改变:

    class T(dict):
        def __init__(self, **kwargs):
            kwargs['default'] = 'DEFAULT'
            super(T, self).__init__(**kwargs)
    
        def __setitem__(self, key, value):
            if key != 'default':
                super(T, self).__setitem__(key, value)
    
        def __delitem__(self, key):
            if key != 'default':
                super(T, self).__delitem__(key)
    
        def clear(self):
            super(T, self).clear()
            self.__init__()
    
        def pop(self, key, **kwargs):
            if key == 'default':
                return self[key]
            return super(T, self).pop(key, **kwargs)
    
        def popitem(self):
            key, value = super(T, self).popitem()
            if key == 'default':
                key2, value2 = super(T, self).popitem()
                super(T, self).__setitem__(key, value)
                return key2, value2
            return key, value
    
        def update(self, other, **kwargs):
            if kwargs:
                if 'default' in kwargs:
                    del kwargs['default']
            elif 'default' in other:
                del other['default']
            super(T, self).update(other, **kwargs)