Расширенный подобный dict подкласс для поддержки приведения и JSON-дампа без дополнительных - PullRequest
0 голосов
/ 13 сентября 2018

Мне нужно создать экземпляр t класса, похожего на dict 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"}

и приведение не работает должным образом, потому что нет ключа «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 подклассы, он явно тестирует на уровне C, используя PyDict_Check.

Когда я попытался сделать T подклассом dict и MutableMapping, я получилтот же результат, что и при использовании только подкласса dict.

Конечно, я могу считать ошибкой, что самосвал json не был обновлен, чтобы предположить, что (конкретные подклассы) collections.abc.Mapping являются дампами,Но даже если это будет признано ошибкой и будет исправлено в какой-то будущей версии Python, я не думаю, что такое исправление будет применено к более старым версиям Python.

Q1 : Как я могу сделать реализацию T, которая является подклассом dict, для правильного приведения?
Q2 : Если Q1 неу меня нет ответа, сработает ли это, если я создам класс уровня C, который возвращает правильное значение для PyDict_Check, но не выполняет какую-либо реальную реализацию (а затем делает T подклассом этого, а также MutableMapping (я не думаю, что добавление такого неполного dict уровня 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() mв классе T и сообщите пользователям ruamel.yaml об этом

, или вернитесь к подклассу dict и попросите людей сделать

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

ничего из этогодействительно дружелюбен и может указывать на то, что невозможно создать диктовочный класс, который не является тривиальным и хорошо взаимодействует со стандартной библиотекой.

Ответы [ 3 ]

0 голосов
/ 14 сентября 2018

Поскольку настоящая проблема здесь заключается в том, что кодировщик по умолчанию 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 package, бэкпорт Python 3.3 inspect.signature, также не имеет метода apply_defaults. Чтобы сделать код максимально совместимым с предыдущими версиями, вы можете просто скопировать и вставить код Python 3.5+ inspect.BoundArguments.apply_defaults в свой модуль и назначить его в качестве атрибута inspect.BoundArguments после импорта 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
0 голосов
/ 21 сентября 2018

Ответы на вопросы Q1 и Q2: «Вы не можете» соответственно. "Нет"

Вкратце: вы не можете добавить ключ на лету в Python и получить вывод JSON также (без исправления json.dumps или предоставления default к нему).

Причина в том, что для того, чтобы JSON работал вообще, вам нужно сделать ваш класс подкласс dict (или какой-то другой объект, реализованный в уровень C), так что его вызов PyDict_Check() возвращает ненулевое значение (что означает, что поле tp_flags в заголовке объекта имеет Бит Py_TPFLAGS_DICT_SUBCLASS установлен).

Приведение (dict(**data))) сначала выполняет эту проверку на уровне C как хорошо (в dictobject.c:dict_merge). Но есть разница в том, как все идет оттуда. При сбросе JSON код на самом деле перебирает ключ / значения, используя подпрограммы, предоставляемые подклассом если они доступны.

Напротив, актерский состав не смотрит, есть ли подклассы происходит и копирует значения из реализации уровня C ( dict, ruamel.ordereddict и т. Д.).

При приведении чего-либо, что не является подклассом dict, тогда нормальный интерфейс уровня класса Python (__iter__) вызывается для получения пары ключ / значение. Вот почему создание подклассов MutableMapping делает кастинг работает, но, к сожалению, это нарушает дамп JSON.

Недостаточно создать урезанный класс уровня C, который возвращает ненулевой PyDict_Check(), так как приведение будет повторяться на уровне C над ключами и значениями этого класса.

Единственный способ реализовать это прозрачно, это реализовать класс, подобный dict как уровень C, который выполняет вставка на лету ключа default и его значения. Это должно быть сделано путем подделки длина, которая на единицу больше, чем фактическое количество записей и каким-то образом реализовать индексирование на уровне C ma_keys и ma_values, чтобы иметь это лишний предмет Если это вообще возможно, это будет трудно, так как dict_merge предполагает Исправлено знание о небольшом количестве внутренних объектов исходного объекта.

Альтернативой для исправления json.dumps является исправление dict_merge, но последнее повлияет на много кода отрицательно по скорости, так что это менее вероятно (и также не будет сделать это задним числом на старых версиях Python).

0 голосов
/ 13 сентября 2018

Вы можете подойти к проблеме совершенно по-другому. Вместо того, чтобы пытаться получить значение, когда ключ 'default' запрашивается на лету, вы можете инициализировать dict с ключом 'default', установленным на желаемое значение, а затем защитить значение 'default' ключ, переопределяя все методы, которые потенциально могут изменить содержание dict так, чтобы значение ключа 'default' никогда не изменялось:

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...