Как сделать ключ dict устаревшим?

Проблема

Скажем, у меня есть функция на Python, которая возвращает dict с некоторыми объектами.

class MyObj:
    pass

def my_func():
    o = MyObj()
    return {'some string' : o, 'additional info': 'some other text'}

В какой-то момент я замечаю, что имеет смысл переименовать ключ в 'some string', поскольку это вводит в заблуждение и плохо описывает, что на самом деле хранится с этим ключом. Однако, если бы я просто сменил ключ, люди, использующие этот фрагмент кода, были бы очень раздражены, потому что я не дал им времени через период устаревания для адаптации своего кода.

Текущая попытка

Я думал о том, чтобы реализовать предупреждение об устаревании, используя тонкую оболочку вокруг dict:

from warnings import warn

class MyDict(dict):
    def __getitem__(self, key):
        if key == 'some string':
             warn('Please use the new key: `some object` instead of `some string`')
        return super().__getitem__(key)

Таким образом, я могу создать dict со старым и новым ключами, указывающими на один и тот же объект.

class MyObj:
    pass

def my_func():
    o = MyObj()
    return MyDict({'some string' : o, 'some object' : o, 'additional info': 'some other text'})

Вопросы:

  • Каким образом код может сломаться, если я добавлю это изменение?
  • Есть ли более простой (например, меньшее количество изменений, использование существующих решений, использование общих шаблонов) способ добиться этого?

На самом деле, мне ваш подход кажется разумным. Режимы отказа включают другие способы доступа к элементу, например dict.update, dict.get ...

wim 08.01.2019 17:00

Этот вопрос лучше подошел бы к codereview, чем к SO.

dimid 08.01.2019 17:28

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

NOhs 08.01.2019 17:59
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
18
3
506
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Честно говоря, я не думаю, что в вашем решении есть что-то особенно неправильное или анти-шаблон, за исключением того факта, что my_func должен дублировать каждый устаревший ключ с его заменой (см. Ниже).

Вы даже можете немного обобщить его (на случай, если вы решите отказаться от других ключей):

class MyDict(dict):
    old_keys_to_new_keys = {'some string': 'some object'}
    def __getitem__(self, key):
        if key in self.old_keys_to_new_keys:
            msg = 'Please use the new key: `{}` instead of `{}`'.format(self.old_keys_to_new_keys[key], key)
            warn(msg)
        return super().__getitem__(key)

class MyObj:
    pass

def my_func():
    o = MyObj()
    return MyDict({'some string' : o, 'some object': o, 'additional info': 'some other text'})

потом

>> my_func()['some string'])
UserWarning: Please use the new key: `some object` instead of `some string`

Все, что вам нужно сделать сейчас, чтобы «исключить» больше ключей, - это обновить old_keys_to_new_keys.

Тем не мение,

note how my_func has to duplicate each deprecated key with its replacement. This violates the DRY principle and will clutter the code if and when you do need to deprecate more keys (and you will have to remember to update both MyDict.old_keys_to_new_keysиmy_func). If I may quote Raymond Hettinger:

There must be a better way

Это можно исправить с помощью следующих изменений в __getitem__:

def __getitem__(self, old_key):
    if old_key in self.old_keys_to_new_keys:
        new_key = self.old_keys_to_new_keys[old_key]
        msg = 'Please use the new key: `{}` instead of `{}`'.format(new_key, old_key)
        warn(msg)
        self[old_key] = self[new_key]  # be warned - this will cause infinite recursion if
                                       # old_key == new_key but that should not really happen
                                       # (unless you mess up old_keys_to_new_keys)
    return super().__getitem__(old_key)

Тогда my_func сможет использовать только новые ключи:

def my_func():
    o = MyObj()
    return MyDict({'some object': o, 'additional info': 'some other text'})

Поведение такое же, любой код, использующий устаревшие ключи, получит предупреждение (и, конечно, доступ к новым ключам будет работать):

print(my_func()['some string'])
# UserWarning: Please use the new key: `some object` instead of `some string`
# <__main__.MyObj object at 0x000002FBFF4D73C8>
print(my_func()['some object'])
# <__main__.MyObj object at 0x000002C36FCA2F28>

Было бы неплохо также переопределить метод __setitem__(), чтобы он отображал предупреждение, когда это необходимо.

martineau 08.01.2019 18:16

Взяв за основу это обсуждение, я создал небольшой пакет для заинтересованных: github.com/NOhs/dkey

NOhs 14.01.2019 19:14

Как говорили другие, ваш нынешний подход уже кажется неплохим. Единственное возможное предостережение, которое я вижу, заключается в том, что класс MyDict централизует все знания об устаревших значениях. В зависимости от вашего варианта использования вы можете предпочесть определить, что является устаревшим, а что нет, в том месте, где оно определено. Например, вы можете сделать что-то в этом роде:

from warnings import warn

class MyDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._deprecated_keys = {}
    def __getitem__(self, key):
        if key in self._deprecated_keys:
            new_key = self._deprecated_keys[key]
            if new_key:
                warn(f'Please use the new key: `{new_key}` instead of `{key}`.')
            else:
                warn(f'Deprecated key: `{key}`.')
        return super().__getitem__(key)
    # Option A
    def put_deprecated(self, key, value, new_key=None):
        self._deprecated_keys[key] = new_key
        self[key] = value
    # Option B
    def put(self, key, value, deprecated_keys=None):
        self[key] = value
        for deprecated_key in (deprecated_keys or []):
            self[deprecated_key] = value
            self._deprecated_keys[deprecated_key] = key


my_dict = MyDict()
# Option A
my_dict['new_key'] = 'value'
my_dict.put_deprecated('old_key', 'value', new_key='new_key')
# Option B
my_dict.put('new_key', 'value', deprecated_keys=['old_key'])

my_dict['old_key']
# UserWarning: Please use the new key: `new_key` instead of `old_key`.

Вариант A требует повторения, но позволяет использовать устаревшие ключи без замены, а вариант B более лаконичен. Преимущество здесь в том, что определение новых ключей и отказ от старых выполняется в точке, где назначены ключ и значение, вместо того, чтобы требовать изменения MyDict.

Другие вопросы по теме