Исправьте класс-декоратор Python для методов экземпляра

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

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

Универсальный журнал вызовов

При разработке библиотеки плагинов программист столкнулся с неожиданной ошибкой: если обернуть метод класса обычным декоратором, реализованным как класс, при вызове теряется ссылка на экземпляр. Функция ожидает получить объект в параметре self, но не находит его среди позиционных аргументов и завершает работу сбоем.

Для отладки этой проблемы создаётся универсальный класс-декоратор с протоколом дескриптора __get__. Он проверяет контекст обращения: если функцию вызывают напрямую, она выполняется штатно, а при доступе через экземпляр чужого класса декоратор возвращает связанный метод через types.MethodType, передавая этот экземпляр первым аргументом. При этом декоратор ведёт раздельный счёт вызовов — отдельно для автономных запусков и отдельно для обращений через конкретный объект.

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

Что уже дано

В редакторе представлена заготовка класса CallLedger с методами __init__, __call__, __get__ и calls_for.

При выполнении кода доступна вспомогательная функция observe_call_ledger(events), предназначенная для проверки работы дескриптора и счётчиков; реализовывать её самостоятельно не требуется.

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

Реализуйте класс-декоратор CallLedger(func) со следующими атрибутами и методами:

  • Конструктор __init__(self, func) принимает целевой вызываемый объект func и инициализирует атрибут free_calls целым числом 0.
  • Прямой вызов экземпляра декоратора __call__(*args, **kwargs) выполняет целевую функцию с переданными аргументами и увеличивает счётчик free_calls ровно на 1. Аргументы *args и **kwargs перенаправляются без изменений. Если целевая функция завершается ошибкой, счётчик free_calls всё равно увеличивается, а возникшее исключение беспрепятственно всплывает дальше. При успешном выполнении возвращается результат целевой функции.
  • Метод дескриптора __get__(self, instance, owner=None):
  • если обращение происходит через класс владельца (instance is None), метод возвращает сам экземпляр декоратора self; вызов через класс владельца работает по прямому пути с увеличением free_calls;
  • если обращение происходит через конкретный объект (instance не None), метод возвращает настоящий связанный метод types.MethodType(self_bound, instance). При вызове такого связанного метода целевой функции первым аргументом передаётся instance, за которым следуют переданные позиционные и именованные аргументы, а счётчик вызовов увеличивается строго для этого конкретного экземпляра.
  • Метод calls_for(self, instance) возвращает целое число — количество фактических вызовов целевой функции через указанный конкретный экземпляр instance. Для экземпляра, через который ещё не было вызовов, возвращается 0.
  • Простое обращение к атрибуту или сохранение связанного метода в переменную не считается вызовом целевой функции и не меняет счётчики.
  • Экземпляры владельца могут быть нехэшируемыми, не иметь собственного словаря __dict__ (использовать __slots__) или переопределять оператор равенства __eq__. Сопоставление экземпляров должно опираться строго на их идентичность (is), а не на равенство или хэши.
  • Счётчики изолированы: каждый экземпляр CallLedger ведёт независимый учёт для своей декорированной функции, а вызовы через один объект владельца не влияют на счётчики других объектов.

Ввод и вывод

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

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

@CallLedger
def add(a, b):
    return a + b
print(add(2, 3))
print(add.free_calls)
class Plugin:
    @CallLedger
    def repeat(self, text):
        return text * 2
plugin = Plugin()
print(plugin.repeat('x'))
print(Plugin.repeat.calls_for(plugin))

Вывод:

5
1
xx
1
РешениеPython

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

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

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