Contextmanager на Python: транзакционные изменения словаря с квотой

Напишите решение на Python и проверьте его на странице.

Другие упражнения по этой теме: Контекстные менеджеры Python: задачи на with, откат и исключения.

Черновик настроек с квотой правок

Служба управления параметрами синхронизирует пакеты правок с реестром настроек и следит за тем, чтобы разовая модификация не оказалась слишком масштабной. Для безопасного внесения изменений реестр не редактируют напрямую: генераторный менеджер контекста под декоратором @contextlib.contextmanager принимает целевой плоский словарь и целочисленную квоту на число расхождений, создаёт поверхностную копию оригинала и передаёт этот изолированный черновик в блок with.

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

Когда суммарное число таких расхождений не превышает заданную квоту, целевой словарь полностью заменяется содержимым черновика путём очистки и обновления. Если же квота строго превышена, фиксация изменений отменяется, целевой словарь не меняется, а менеджер возбуждает ValueError.

Реализуйте такой генераторный менеджер контекста для безопасного редактирования словаря настроек.

Что уже дано

В редакторе объявлена заготовка генераторной функции bounded_draft(target, quota) с декоратором @contextlib.contextmanager:

@contextlib.contextmanager
def bounded_draft(target, quota):
    # TODO
    raise NotImplementedError
    yield

При тестировании также доступны вспомогательные функции проверки observe_dictionary_drafts и observe_empty_dictionary_draft, реализовывать их самостоятельно не требуется.

Что нужно сделать

Реализуйте генераторную функцию bounded_draft(target, quota) с декоратором @contextlib.contextmanager.

Функция принимает:

  • target — исходный плоский словарь dict[str, int] (включая стандартные подклассы dict и int), содержащий от 0 до 20 строковых ASCII-ключей длиной от 1 до 30 символов со значениями от -1 000 000 до 1 000 000;
  • quota — целое неотрицательное число от 0 до 20, задающее максимально допустимое количество расхождений.

Менеджер контекста должен работать по следующим правилам:

  • При входе в блок with создаётся независимая поверхностная копия целевого словаря, которая передаётся через yield. Во время выполнения тела блока исходный объект target остаётся без изменений.
  • В теле блока над полученным черновиком могут производиться стандартные операции добавления, изменения значений, удаления ключей или очистки (до 1000 операций за сессию).
  • Если внутри блока возникает любое исключение класса BaseException, менеджер отбрасывает черновик, оставляет исходный словарь target неизменным и беспрепятственно пропускает исключение наружу.
  • При нормальном выходе из блока подсчитывается итоговое количество расхождений между исходным состоянием target и финальным состоянием черновика. Каждое из следующих различий учитывается ровно один раз:
  • добавленный новый ключ;
  • удалённый старый ключ;
  • ключ, значение которого изменилось по сравнению с исходным. Если ключ был добавлен, а затем удалён, либо если значение ключа менялось, но в итоге вернулось к исходному значению, расхождением это не считается.
  • Если итоговое число расхождений меньше либо равно quota, менеджер контекста по месту обновляет исходный словарь target (сохраняя сам объект словаря), очищая его и наполняя актуальными записями черновика.
  • Если итоговое число расхождений строго больше quota, менеджер контекста возбуждает ValueError (или его подкласс), не внося никаких изменений в исходный словарь target.

Ввод и вывод

Стандартные потоки ввода stdin и вывода stdout в решении не используются: считывать ввод и вызывать print() не нужно. После выполнения кода учащегося проверяющая система считывает из пространства имён модуля функцию верхнего уровня bounded_draft и вызывает её с проверочными аргументами.

Пример использования

settings = {'a': 1}
with bounded_draft(settings, 2) as draft:
    del draft['a']
    draft['b'] = 2
print(settings)
try:
    with bounded_draft(settings, 1) as draft:
        draft['b'] = 3
        draft['c'] = 4
except ValueError:
    print(settings)

Вывод:

{'b': 2}
{'b': 2}
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Связанные задачи