В данном коде демонстрируется корректная работа декоратора 𝚃𝚛𝚊𝚌𝚎𝚛 с множественными экземплярами.
Марк Лутц «Изучаем Python», глава 39:
«Однако, как теоретически обсуждалось ранее, такая альтернатива, где используются только классы, подвержена множеству классов, но не вполне нормально работает с множеством экземпляров заданного класса: каждый вызов, создающий экземпляр, запускает метод __𝚌𝚊𝚕𝚕__, который перезаписывает предыдущий экземпляр».
Механизм работы:
🔘 При создании 𝚋𝚘𝚋 = 𝙿𝚎𝚛𝚜𝚘𝚗('𝙱𝚘𝚋') вызывается метод __𝚌𝚊𝚕𝚕__ декоратора 𝚃𝚛𝚊𝚌𝚎𝚛 🔘 В 𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 сохраняется экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗 с именем '𝙱𝚘𝚋' 🔘 𝚋𝚘𝚋 ссылается на объект 𝚃𝚛𝚊𝚌𝚎𝚛, который содержит экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗('𝙱𝚘𝚋') 🔘 При создании 𝚜𝚞𝚎 = 𝙿𝚎𝚛𝚜𝚘𝚗('𝚂𝚞𝚎') создается новый объект 𝚃𝚛𝚊𝚌𝚎𝚛 🔘 В новом объекте 𝚃𝚛𝚊𝚌𝚎𝚛 в 𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 сохраняется экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗 с именем '𝚂𝚞𝚎' 🔘 𝚜𝚞𝚎 ссылается на новый объект 𝚃𝚛𝚊𝚌𝚎𝚛, который содержит экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗('𝚂𝚞𝚎')
При обращении к 𝚋𝚘𝚋.𝚗𝚊𝚖𝚎 и 𝚜𝚞𝚎.𝚗𝚊𝚖𝚎: 🔘 𝚋𝚘𝚋.𝚗𝚊𝚖𝚎 обращается к атрибуту 𝚗𝚊𝚖𝚎 через __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ первого объекта 𝚃𝚛𝚊𝚌𝚎𝚛 🔘 __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ делегирует запрос к 𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 первого объекта (содержит 𝙿𝚎𝚛𝚜𝚘𝚗('𝙱𝚘𝚋')) 🔘 Поэтому 𝚋𝚘𝚋.𝚗𝚊𝚖𝚎 возвращает '𝙱𝚘𝚋' 🔘𝚜𝚞𝚎.𝚗𝚊𝚖𝚎 обращается к атрибуту 𝚗𝚊𝚖𝚎 через __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ второго объекта 𝚃𝚛𝚊𝚌𝚎𝚛 🔘 __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ делегирует запрос к 𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 второго объекта (содержит 𝙿𝚎𝚛𝚜𝚘𝚗('𝚂𝚞𝚎')) 🔘 Поэтому 𝚜𝚞𝚎.𝚗𝚊𝚖𝚎 возвращает '𝚂𝚞𝚎'
В данном случае каждый вызов 𝙿𝚎𝚛𝚜𝚘𝚗() создает новый объект 𝚃𝚛𝚊𝚌𝚎𝚛, поэтому проблема с перезаписью экземпляров не проявляется.
«В этом коде класс 𝚆𝚛𝚊𝚙𝚙𝚎𝚛 перехватывает доступ к любым именованным атрибутам внутреннего объекта, выводит трассировочное сообщение и применяет встроенную функцию __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ для передачи запроса 𝚠𝚛𝚊𝚙𝚙𝚎𝚍−объекту.»
Механизм работы:
− При создании экземпляра 𝚆𝚛𝚊𝚙𝚙𝚎𝚛([𝟷, 𝟸, 𝟹]) сохраняется список в атрибуте 𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 − При вызове 𝚡.𝚊𝚙𝚙𝚎𝚗𝚍(𝟺) 𝙿𝚢𝚝𝚑𝚘𝚗 не находит метод 𝚊𝚙𝚙𝚎𝚗𝚍 у класса 𝚆𝚛𝚊𝚙𝚙𝚎𝚛 − Срабатывает метод __𝚐𝚎𝚝𝚊𝚝𝚝𝚛__ с параметром 𝚊𝚝𝚝𝚛𝚗𝚊𝚖𝚎 = '𝚊𝚙𝚙𝚎𝚗𝚍' − Выводится сообщение «𝚃𝚛𝚊𝚌𝚎: 𝚊𝚙𝚙𝚎𝚗𝚍» − Функция 𝚐𝚎𝚝𝚊𝚝𝚝𝚛(𝚜𝚎𝚕𝚏.𝚠𝚛𝚊𝚙𝚙𝚎𝚍, 𝚊𝚝𝚝𝚛𝚗𝚊𝚖𝚎) находит метод 𝚊𝚙𝚙𝚎𝚗𝚍 у обернутого списка и вызывает его − Список [𝟷, 𝟸, 𝟹] становится [𝟷, 𝟸, 𝟹, 𝟺] − При выводе 𝚡.𝚠𝚛𝚊𝚙𝚙𝚎𝚍 выводится содержимое обернутого списка: [𝟷, 𝟸, 𝟹, 𝟺]
«В частности, он отслеживает доступ к атрибутам, осуществляемый вызовом метода класса 𝚠𝚛𝚊𝚙𝚙𝚎𝚍−объекта; доступ к другим методам внутреннего объекта не перехватывается и не подлежит обработке по определению».
«Из−за того, что декораторы классов способны перехватывать вызовы, создающие экземпляры, они могут использоваться либо для управления созданием экземпляров классов, либо для реализации паттерна проектирования «одиночка». В последнем случае декоратор обеспечивает, чтобы экземпляр класса существовал в единственном числе, и при последующих вызовах возвращался именно он.»
Декоратор 𝚂𝚒𝚗𝚐𝚕𝚎𝚝𝚘𝚗 работает следующим образом:
− В словаре 𝚒𝚗𝚜𝚝𝚊𝚗𝚌𝚎𝚜 хранятся созданные экземпляры для каждого класса; − При первом вызове 𝙿𝚎𝚛𝚜𝚘𝚗('𝙱𝚘𝚋') создается экземпляр и сохраняется в словаре; − При втором вызове 𝙿𝚎𝚛𝚜𝚘𝚗('𝚂𝚞𝚎') декоратор проверяет, что экземпляр уже существует, и возвращает тот же объект; − Поэтому и 𝚋𝚘𝚋, и 𝚜𝚞𝚎 ссылаются на один и тот же объект с именем '𝙱𝚘𝚋'.
Таким образом, при выводе 𝚋𝚘𝚋.𝚗𝚊𝚖𝚎 и 𝚜𝚞𝚎.𝚗𝚊𝚖𝚎 мы получаем '𝙱𝚘𝚋' дважды, поскольку это один и тот же объект.
«Несмотря на успешную генерацию случайного числа, функция всегда возвращает 𝟺 из−за 𝚛𝚎𝚝𝚞𝚛𝚗 в 𝚏𝚒𝚗𝚊𝚕𝚕𝚢−блоке, что делает 𝚝𝚛𝚢−блок фактически бесполезным.
𝙿𝙴𝙿 𝟽𝟼𝟻 ввёл 𝚂𝚢𝚗𝚝𝚊𝚡𝚆𝚊𝚛𝚗𝚒𝚗𝚐 на этот случай, объясняя, почему конструкции вроде 𝚛𝚎𝚝𝚞𝚛𝚗 / 𝚋𝚛𝚎𝚊𝚔 / 𝚌𝚘𝚗𝚝𝚒𝚗𝚞𝚎 в 𝚏𝚒𝚗𝚊𝚕𝚕𝚢 могут быть ошибкой — они затмевают поведение основного блока 𝚝𝚛𝚢, сбивая с толку разработчиков.
Избавиться от 𝚝𝚛𝚢 / 𝚎𝚡𝚌𝚎𝚙𝚝, если исключения маловероятны:
𝚍𝚎𝚏 𝚍𝟼() −> 𝚒𝚗𝚝: 𝚛𝚎𝚝𝚞𝚛𝚗 𝚛𝚊𝚗𝚍𝚘𝚖.𝚛𝚊𝚗𝚍𝚒𝚗𝚝(𝟷, 𝟼)
Рекомендации автора и сообщества
🔘Избегайте 𝚛𝚎𝚝𝚞𝚛𝚗 и подобных конструкций в 𝚏𝚒𝚗𝚊𝚕𝚕𝚢; 🔘Переносите логику в 𝚎𝚡𝚌𝚎𝚙𝚝, если нужно "подстраховаться" при ошибке; 🔘Удаляйте 𝚏𝚒𝚗𝚊𝚕𝚕𝚢, если он не нужен, либо используйте 𝚠𝚒𝚝𝚑 и контекстные менеджеры; 🔘Разрешённое поведение линтеров — удаление таких конструкций и рефакторинг».
«Параметр 𝚙𝚊𝚛𝚜𝚎_𝚍𝚊𝚝𝚎𝚜 функции 𝚛𝚎𝚊𝚍_𝚌𝚜𝚟 позволяет задать список строковых значений, отмечающих столбцы, текстовые значения из которых 𝚙𝚊𝚗𝚍𝚊𝚜 должна преобразовать в метки даты / времени. В следующем примере в него передается список, содержащий значения "𝙳𝚊𝚝𝚎":»
Вот разница между чтением файла без параметра 𝚙𝚊𝚛𝚜𝚎_𝚍𝚊𝚝𝚎𝚜 и с ним:
𝟷. БЕЗ 𝚙𝚊𝚛𝚜𝚎_𝚍𝚊𝚝𝚎𝚜: столбец 𝙳𝚊𝚝𝚎 остается строковым (𝚘𝚋𝚓𝚎𝚌𝚝), значения имеют тип 𝚜𝚝𝚛 𝟸. С 𝚙𝚊𝚛𝚜𝚎_𝚍𝚊𝚝𝚎𝚜=["𝙳𝚊𝚝𝚎"]: столбец 𝙳𝚊𝚝𝚎 преобразуется в 𝚍𝚊𝚝𝚎𝚝𝚒𝚖𝚎𝟼𝟺, значения имеют тип 𝚃𝚒𝚖𝚎𝚜𝚝𝚊𝚖𝚙
Параметр 𝚙𝚊𝚛𝚜𝚎_𝚍𝚊𝚝𝚎𝚜 принимает: − Список имен столбцов для преобразования в даты − 𝚃𝚛𝚞𝚎 для автоматического определения столбцов с датами − Словарь для более сложных преобразований
«Корень проблемы здесь кроется в аргументе 𝚜𝚎𝚕𝚏 метода __𝚌𝚊𝚕𝚕__ класса 𝚝𝚛𝚊𝚌𝚎𝚛 — он является экземпляром 𝚝𝚛𝚊𝚌𝚎𝚛 или же экземпляром 𝙿𝚎𝚛𝚜𝚘𝚗? На самом деле нам необходимы в коде оба экземпляра: экземпляр 𝚝𝚛𝚊𝚌𝚎𝚛 для состояния декоратора и экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗 для перенаправления на исходный метод. В действительности 𝚜𝚎𝚕𝚏 обязан быть объектом 𝚝𝚛𝚊𝚌𝚎𝚛, чтобы предоставить доступ к информации состояния 𝚝𝚛𝚊𝚌𝚎𝚛 (его атрибутам 𝚌𝚊𝚕𝚕𝚜 и 𝚏𝚞𝚗𝚌); это справедливо для декорирования как простой функции, так и метода».
«К сожалению, когда имя декорированного метода повторно привязывается к объекту экземпляра класса с помощью __𝚌𝚊𝚕𝚕__, интерпретатор 𝙿𝚢𝚝𝚑𝚘𝚗 передает в 𝚜𝚎𝚕𝚏 только экземпляр 𝚝𝚛𝚊𝚌𝚎𝚛; он вообще не передает экземпляр 𝙿𝚎𝚛𝚜𝚘𝚗 в списке аргументов. Кроме того, поскольку экземпляру 𝚝𝚛𝚊𝚌𝚎𝚛 ничего не известно об экземпляре 𝙿𝚎𝚛𝚜𝚘𝚗, который мы пытаемся обработать посредством вызовов методов, создать связанный метод с экземпляром не удастся, следовательно, нет способа для корректного координирования вызова».
«В итоге предыдущий код передает декорированному методу слишком мало аргументов, что приводит к ошибке».
Перемещение счетчика в общую глобальную область видимости, чтобы сделать возможным его изменение, также означает, что он будет разделяться всеми внутренними функциями. В отличие от атрибутов экземпляров классов глобальные счетчики относятся ко всей программе, а не к каждой функции — счетчик инкрементируется для вызова любой отслеживаемой функции.
Это демонстрирует разницу между использованием глобальных переменных и атрибутов экземпляров классов в декораторах. Глобальная переменная создает общий счетчик для всех декорированных функций, а не отдельный счетчик для каждой функции.
Глобальная переменная 𝚌𝚊𝚕𝚕𝚜 является общей для всех декорированных функций, поэтому счетчик инкрементируется при каждом вызове любой из декорированных функций.
Последовательность выполнения:
🔘 𝚜𝚙𝚊𝚖(𝟷, 𝟸, 𝟹) − 𝚌𝚊𝚕𝚕𝚜 становится 𝟷, выводится «𝚌𝚊𝚕𝚕 𝟷 𝚝𝚘 𝚜𝚙𝚊𝚖», затем 𝟼 🔘 𝚜𝚙𝚊𝚖(𝚊=𝟺, 𝚋=𝟻, 𝚌=𝟼) − 𝚌𝚊𝚕𝚕𝚜 становится 𝟸, выводится «𝚌𝚊𝚕𝚕 𝟸 𝚝𝚘 𝚜𝚙𝚊𝚖», затем 𝟷𝟻 🔘 𝚎𝚐𝚐𝚜(𝟸, 𝟷𝟼) − 𝚌𝚊𝚕𝚕𝚜 становится 𝟹, выводится «𝚌𝚊𝚕𝚕 𝟹 𝚝𝚘 𝚎𝚐𝚐𝚜», затем 𝟼𝟻𝟻𝟹𝟼 🔘 𝚎𝚐𝚐𝚜(𝟺, 𝚢=𝟺) − 𝚌𝚊𝚕𝚕𝚜 становится 𝟺, выводится «𝚌𝚊𝚕𝚕 𝟺 𝚝𝚘 𝚎𝚐𝚐𝚜», затем 𝟸𝟻𝟼