Повысьте свою способность следовать принципам SOLID с помощью техники Python среднего уровня.

Когда вы только начинаете работать с Python, практически все, что вы изучаете, является фундаментальным. У вас не так много времени или мотивации, чтобы остановиться и спросить когда я буду использовать это?, когда вы изучаете базовые вещи, такие как типы данных, условные выражения, циклы и функции ... потому что ответ - как программист на Python, вы будете использовать все это постоянно! Являетесь ли вы специалистом по обработке данных, создающим модели машинного обучения, или разработчиком программного обеспечения, создающим серверную часть веб-сайта, маловероятно, что вы вообще сможете добиться каких-либо успехов, если не будете знать основ.

Но как только вы поймете основные основы Python, может быть сложно определить, какие новые инструменты и концепции будут полезны - и, возможно, даже необходимы - в вашем продвижении по сравнению с теми, которые могут оказаться пустой тратой вашего времени.

В этой истории я расскажу вам:

  1. Обзор принципов разработки программного обеспечения SOLID
  2. Примеры «навешиваемых» сценариев, которые сложно реализовать при соблюдении принципов SOLID
  3. Как декоратор решает эту проблему
  4. Использование декораторов Python, написанных другими
  5. Написание собственных декораторов на Python

Код из этого сообщения блога также можно найти в записной книжке Google Colaboratory:



ТВЕРДЫЕ принципы

Если вы планируете участвовать в «реальных» программных проектах, а не только в разовых сценариях или личных проектах, неплохо было бы ознакомиться с принципами SOLID.

Принципы SOLID были первоначально созданы Робертом К. Мартином, также известным как дядя Боб из компании Uncle Bob Consulting. Цитата с его сайта:

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

5 принципов SOLID:

  • S Принцип единой ответственности
  • O принцип замкнутости пера
  • Л Исков Принцип замещения
  • I Принцип разделения интерфейса
  • D принцип инверсии зависимости

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

Принцип единоначалия

Каждый компонент программного обеспечения должен отвечать только за один набор функций. В контексте примеров в этом блоге «компонент» будет означать функцию Python.

Принцип открыт-закрыт

Уже написанное программное обеспечение должно быть «закрыто» для модификации, но «открыто» для расширения. Это означает, что мы должны избегать переписывания существующего кода, когда это возможно, но в идеале мы должны иметь возможность добавлять в этот код расширенные функции.

«Прикрепленные» программные сценарии

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

Вот некоторые примеры сценариев «прикрепленного» программного обеспечения:

  • Кеширование
  • логирование
  • Контроль доступа
  • Проверка ввода
  • Твики для формата ввода или вывода

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

Пример кеширования

Давайте сосредоточимся на кешировании как на конкретном примере «навесного» сценария. В общем, кеширование означает, что вы сохраняете результат дорогостоящей операции в кеш-памяти (обычно реализованном в виде некоторой формы словаря / хэш-карты), чтобы позже вы могли просто посмотреть увеличить этот результат вместо повторного выполнения этой дорогостоящей операции.

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

def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

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

assert fib(25) == 75025

Проблема с этой реализацией заключается в том, что вам приходится многократно пересчитывать одни и те же значения. Например, если вы вычисляете fib(10), это означает fib(9) + fib(8). Тогда fib(9) = fib(8) + fib(7) и fib(8) = fib(7) + fib(6). Вы уже можете видеть, что fib(8) и fib(7) вычисляются дважды, и это повторение каскадно проходит через остальную часть рекурсии. Как сделать этот код быстрее?

«Известный» способ оптимизации решения Фибоначчи - это добавить кэширование, чтобы после вычисления fib(8) вам больше не приходилось вычислять его снова, вам просто нужно было извлечь его из кеша. Но как это сделать, следуя принципам SOLID?

Неоптимальные решения для кеширования

Самый очевидный / распространенный способ решить эту проблему - создать кеш вне функции, затем адаптировать функцию для проверки кеша перед выполнением вычислений и добавить все вычисленные значения в кеш. Что-то вроде этого:

cache = {}
def fib(n):
    if n < 2:
        return n
    if n in cache:
        return cache[n]
    result = fib(n-1) + fib(n-2)
    cache[n] = result
    return result

Набор тестов все равно пройдет ✅

assert fib(25) == 75025

Но как насчет принципов SOLID?

Единоличная ответственность

Теперь вместо функции, которая вычисляет числа Фибоначчи, у нас есть функция, которая вычисляет числа Фибоначчи и управляет добавлением и извлечением вещей из кеша.

Открыто-закрыто

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

Другой популярной реализацией этого было бы предотвращение создания глобального кеша путем преобразования функции в метод класса и превращения кеша в переменную-член этого класса. Это имеет те же проблемы с единственной ответственностью ❌ и открытым-закрытым ❌, что и в предыдущем решении, и дополнительно потребует переписывания модульного теста ❌ (и всего кода, который в настоящее время ожидает, что это будет автономная функция, а не метод класса) .

Подход декоратора

Декораторный подход - отличный способ решить эту проблему. Он восходит к классической книге «Банда четырех» Шаблоны дизайна, опубликованной в 1994 году.

Хотя в этой книге описывается «композиция вместо наследования» в контексте проектирования классов ООП, мы можем перевести ее в этом контексте как о композиции функций.

В общем, подход декоратора означает, что вы хотите «обернуть» свой код этой дополнительной функциональностью, то есть создать что-то, состоящее из исходной функциональности, теперь украшенной с навязанной функциональностью.

Поскольку нам не нужно изменять существующий код, используя подход декоратора, мы можем «закрепить» новые функции, при этом следуя принципам единой ответственности и принципам SOLID открытого-закрытого типа.

Разъяснение

Существует множество ресурсов, описывающих шаблон декоратора, например, эта статья. Эта терминология характерна для узкого типа объектно-ориентированных классов декораторов, которые теоретически могут быть реализованы с использованием декораторов Python. Классы декораторов обращаются к тем же сценариям, которые мы обсуждали, и общая идея аналогична. Но декораторы в Python относятся к конструкции более высокого уровня, которая может применяться как к функциям, так и к классам. Эта потенциальная путаница, связанная с именованием декораторов Python, рассматривается, но не решается в Предложении по усовершенствованию Python от 2003 года.

Использование декораторов Python

Прежде чем мы погрузимся в написание собственного декоратора Python, давайте воспользуемся тем, который уже был написан для нас. Концептуально это похоже на изучение того, как использовать существующие функции, такие как print() или len(), прежде чем пытаться писать функции для себя.

Вернемся к предыдущему примеру с числами Фибоначчи:

def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

Теперь давайте добавим кеш LRU из встроенного модуля functools (встроенный означает, что нам не нужно устанавливать какие-либо библиотеки, кроме базового Python, нам просто нужно импортировать его):

import functools
@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

…вот и все! 🤯

Набор тестов все еще проходит ✅

Принцип единоначалия

Функция fib, как написано, по-прежнему является просто функцией числа Фибоначчи. Это не функция, сочетающая числа Фибоначчи и кеширование.

Принцип открытого-закрытого

Мы оставили исходную функцию «закрытой». Если бы кто-то пошел посмотреть на Git, вину все равно будет приписывать первоначальному автору. Но в то же время мы «открыли» его для расширения, поскольку теперь он использует кеширование.

Проверка реальности. Если вам интересно, действительно ли добавление этих двух строк кода «сделало что-нибудь», я запустил эти два фрагмента, используя %%timeit magic command и fib(25). Исходный код занял 30,9 мс (30,9 миллисекунды), а код с кешированием занял 6 мкс (6 микросекунд). В миллисекунде 1000 микросекунд, а это означает, что производительность кода с кешированием была более чем в 1000 раз выше, чем у оригинала - довольно впечатляюще для добавления двух строк кода!

Итак ... что только что произошло?

Механически мы просто импортировали модуль (functools), затем добавили символ @ и функцию из этого модуля.

Концептуально мы «обернули» существующую fib функцию в декоратор кеша:

  1. Декоратор lru_cache принял нашу функцию в качестве аргумента
  2. Добавлена ​​функция кеширования
  3. Он вернул новую функцию, состоящую из fib плюс логика кеширования.
  4. Наконец, новой функции было повторно присвоено имя fib, чтобы мы могли продолжать использовать исходный интерфейс.

Использование уже написанных декораторов

К сожалению, разработчики Python не поддерживают глобальный список декораторов, доступных в базовом Python, но это репозиторий GitHub содержит довольно хороший список этих декораторов, а также декораторов «в дикой природе» (т.е. расположенных в библиотеках вне самого Python):



(Это репо не обновлялось с 2017 года, дайте мне знать в комментариях, если вы нашли лучший или дополнительный ресурс!)

Проверка реальности. Если вам интересно, как часто и в каком контексте вы будете встречать декораторы в реальном мире… в своей работе в области разработки программного обеспечения и науки о данных я чаще всего сталкиваюсь с декораторами в контекст веб-разработки backend / full-stack, особенно Flask и Dash. Эти фреймворки используют декораторы, позволяющие писать функции, которые будут включены в полную функциональность веб-сервера. По сути, невозможно создать приложение с использованием любого из этих фреймворков без использования декораторов. Использование декоратора Flask выглядит примерно так (минимальное приложение из Краткого руководства):

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

Прекрасно использовать декораторы, написанные кем-то другим! Вы можете получить отличную функциональность прямо «из коробки»: просто импортируйте (при необходимости), добавьте синтаксис @, и ваша функция теперь украшена дополнительными функциями. Уже есть отличные декораторы для кеширования, ведения журналов и управления доступом. Можно оставить декораторы в виде неоткрытого «черного ящика», если кто-то уже написал декоратор, который выполняет то, что вам нужно!

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

Python функционирует как «первоклассный» объект

Прежде чем мы сможем сразу перейти к написанию собственных декораторов, нам нужно рассмотреть, как функции Python являются первоклассными объектами. Это концепция, которая будет вам хорошо знакома, если вы использовали JavaScript для интерфейсной веб-разработки (подумайте о обратных вызовах и прослушивателях событий), и очень чужда, если вы Раньше я использовал только такие языки, как Ruby или Java, которые не обрабатывают функции таким образом.

Функции в Python:

  • Может храниться в структурах данных
  • Может быть присвоено переменным
  • Можно передать другим функциям
  • Могут быть вложенными
  • Может захватывать местное государство

(Ознакомьтесь с этим руководством для получения дополнительных объяснений и примеров этих свойств.)

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

Назначение функций переменным и передача функций другим функциям

Вот несколько коротких / тривиальных примеров, показывающих, что означают эти свойства. Во-первых, давайте настроим некоторые функции:

def squared(num):
    return num ** 2
def cubed(num):
    return num ** 3
print(squared(5)) # prints 25
print(cubed(5))   # prints 125

Довольно просто. У нас есть две функции, каждая из которых принимает единственный аргумент num и возвращает полиномиальное преобразование num. squared квадрат его, и cubed кубик.

Теперь продемонстрируем «первоклассный» функционал:

def func_plus_two(polynomial_func, num):
    return polynomial_func(num) + 2
first_func = squared
second_func = cubed
print(func_plus_two(first_func, 5))  # prints 27
print(func_plus_two(second_func, 5)) # prints 127

Итак, мы назначаем функции переменным, в данном случае присваивая squared first_func и cubed second_func. (Для этого нет особо полезной причины, это просто для демонстрационных целей.)

Затем, что может быть более интересно, мы передаем эти функции другой функции, сначала передавая first_func, затем second_func в func_plus_two. Эта функция вызывает функцию, которая была ей передана, а затем возвращает результат исходной функции + 2. Таким образом, вместо 25 и 125 мы получаем 27 и 127, когда num = 5. Эта структура (передача функции в другую функцию ) также называется функцией высшего порядка, т.е. мы могли бы сказать: «func_plus_two - функция высшего порядка».

Вложенные функции

Еще более сложно связать этот пример с чем-то реально полезным, но потерпите меня, это становится важным, когда мы действительно приступаем к написанию декораторов!

Рассмотрим этот пример вложенных функций:

def nth_degree(num, n):
    def squared_inner(num):
        return num ** 2
    def cubed_inner(num):
        return num ** 3
    if n == 2:
        return squared_inner(num)
    elif n == 3:
        return cubed_inner(num)

Теперь вместо squared и cubed в качестве функций в глобальной области видимости у нас есть функции squared_inner и cubed_inner, которые существуют только в рамках функции nth_degree. Другими словами, если мы попытаемся запустить squared_inner(5) в глобальной области видимости, он не вернет 25, а выдаст NameError: name 'squared_inner' is not defined. Чтобы вызвать (т.е. вызвать) эту функцию, мы могли бы использовать такой код:

nth_degree(4, 2) # 4 squared, prints 16
nth_degree(2, 3) # 2 cubed, prints 8

В первом примере мы передали n из 2, поэтому nth_degree вызвали функцию squared_inner на 4 и вернули 16. Во втором примере мы передали n из 3, поэтому nth_degree вызвал функцию cubed_inner на 2 и вернул 8. На самом деле мы могли бы легко переписать этот код, чтобы полностью избежать внутренних функций, но я надеюсь, что вы понимаете главный вывод: функция может иметь другую функцию, вложенную внутри себя, а затем возвращать что-то на основе этой вложенной функции.

Написание собственного декоратора с добавлением запятых

Хорошо, теперь, когда мы завершили обзор первоклассных функций Python, давайте напишем «декоратор» с настраиваемыми функциями!

Допустим, ваш босс приходит к вам с такой задачей:

«Нашим пользователям трудно читать эти большие числа. На первый взгляд, 1000000 - это 1 миллион или 10 миллионов? Давайте избавимся от догадок и поставим несколько запятых, как это обычно делается в формате Comma Style в Excel. Таким образом, они видят что-то вроде 1 000 000 вместо 1000000 ».

(Это навеяно реальной задачей, которую мне пришлось выполнять как разработчику программного обеспечения, когда я работал консультантом по программному обеспечению в Crowe. Бухгалтеры любят свои запятые!)

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

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

Старые модульные тесты:

assert one_million() == "1000000"
assert one_billion() == "1000000000"
assert times_100(7) == "700"
assert minus_10000(150000) == "140000"
assert multiply(300, 800) == "240000"

Новые модульные тесты:

assert one_million() == "1,000,000"
assert one_billion() == "1,000,000,000"
assert times_100(7) == "700"
assert minus_10000(150000) == "140,000"
assert multiply(300, 800) == "240,000"

Декоратор, который вы решили написать, имеет общую структуру:

def add_commas(func):
    def add_commas_wrapper():
        # call original function
        # add in the commas
        # return the result
    return add_commas_wrapper

Другими словами, это функция-оболочка, которая принимает исходную функцию в качестве аргумента, а затем возвращает внутреннюю функцию, которая вызывает исходную функцию и также добавляет в запятые.

Простейшая версия декоратора (без аргументов)

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

def add_commas(func):
    def add_commas_wrapper():
        original_string = func()
        # needs to be int for string formatting
        original_int = int(original_string)
        # we are ignoring locale, using default thousands sep
        return f'{original_int:,}'
    return add_commas_wrapper

Теперь, когда у нас есть эта функция, мы можем изменить существующие функции one_million и one_billion:

def one_million():
    return "1000000"
def one_billion():
    return "1000000000"
one_million = add_commas(one_million)
one_billion = add_commas(one_billion)

Сейчас мы проходим два модульных теста:

assert one_million() == "1,000,000" ✅
assert one_billion() == "1,000,000,000" ✅
assert times_100(7) == "700" ✅ # "accidentally" passing, <1000
assert minus_10000(150000) == "140,000" ❌ # returns "140000"
assert multiply(300, 800) == "240,000" ❌ # returns "240000"

Добавление синтаксического сахара

Вам может быть интересно ... а что насчет того символа @, который мы использовали ранее? Как это «декоратор» вроде functools.lru_cache, если нет символа @? Что ж, символ @ здесь представляет собой синтаксический сахар.

Синтаксический сахар - это концепция, которая конкретно выходит за рамки Python. Это означает какой-то синтаксис, который сокращает общий фрагмент кода, обычно, чтобы его было легче читать и / или труднее испортить.

Давайте изменим предыдущий фрагмент кода, чтобы использовать синтаксический сахар декоратора:

@add_commas
def one_million():
    return "1000000"
@add_commas
def one_billion():
    return "1000000000"

Это такое же количество строк кода, и он фактически делает то же самое, что и предыдущий фрагмент. Но он немного чище (с меньшим количеством скобок и повторений имени функции), и кто-то, посмотрев на определения функций (строки, начинающиеся с def), сразу заметит, что происходит какое-то украшение за пределами того, что они могут видеть в функции, на случай, если позже они попытаются отладить функцию. В предыдущем синтаксисе оформление отделено от определения таким образом, что может сбить с толку, особенно с большим количеством или более длинными функциями, чем в этих тривиальных примерах.

Дополнительные аргументы

Что, если мы просто попробуем добавить этот декоратор к функции times_100?

@add_commas
def times_100(num):
    return str(num * 100)
print(times_100(7))

😱 что случилось? Что ж, если мы вернемся к определению add_commas_wrapper, это будет:

def add_commas(func):
    def add_commas_wrapper():
        original_string = func()
        # needs to be int for string formatting
        original_int = int(original_string)
        # we are ignoring locale, using default thousands sep
        return f'{original_int:,}'
    return add_commas_wrapper

Как сказано в сообщении об ошибке, эта функция принимает 0 аргументов. Как мы можем адаптировать его для работы с num аргументом times_100, не нарушая при этом функциональность one_million и one_billion, которые не принимают никаких аргументов?

Мы могли бы добавить какую-то особую логику, например операторы if во вложенную функцию nth_degree, но это означает, что нам может потребоваться неоднократно изменять add_commas для обработки различного количества параметров. Нам не нужно этого делать, поскольку на самом деле все, что нужно сделать этой функции, - это начать со строки и поместить в нее запятые… не имеет значения, сколько аргументов было передано исходной функции.

К счастью, в Python есть способ принимать произвольное количество аргументов (т. Е. аргументов переменной длины)! Традиционный синтаксис для этого - *args, **kwargs, что означает 0 или более позиционных аргументов (args), затем 0 или более аргументов ключевого слова (kwargs). Таким образом, это будет работать для one_million (0 аргументов любого типа), times_100 (1 позиционный аргумент), multiply (2 позиционных аргумента) и любого другого количества позиционных аргументов или аргументов ключевого слова.

Добавим аргументы переменной длины:

def add_commas(func):
    def add_commas_wrapper(*args, **kwargs):
        original_string = func(*args, **kwargs)
        # needs to be int for string formatting
        original_int = int(original_string)
        # we are ignoring locale, using default thousands sep
        return f'{original_int:,}'
    return add_commas_wrapper

И добавьте синтаксис декоратора ко всем функциям:

@add_commas
def one_million():
    return "1000000"
@add_commas
def one_billion():
    return "1000000000"
@add_commas
def times_100(num):
    return str(num * 100)
@add_commas
def minus_10000(num):
    return str(num - 10000)
@add_commas
def multiply(num1, num2):
    return str(num1 * num2)

Сейчас проходим все испытания!

assert one_million() == "1,000,000" ✅
assert one_billion() == "1,000,000,000" ✅
assert times_100(7) == "700" ✅
assert minus_10000(150000) == "140,000" ✅
assert multiply(300, 800) == "240,000" ✅

🎉 🎉 🎉 🎉 🎉

Еще кое-что

Если вы хотите использовать декораторы в продакшене, вам нужно включить еще одну вещь, хотя она не имеет отношения к прохождению этих конкретных тестов. Вы могли заметить ранее в сообщении об ошибке, что оно было отправлено с add_commas_wrapper, а не с times_100. В этом примере это не особенно актуально, но мы могли бы представить другой сценарий, когда кто-то пытается использовать float, а не int, или делает что-то еще, что нарушает логику внутри add_commas, когда мы хотели бы иметь возможность introspect и увидите исходную функцию, вызывающую ошибку, а не оболочку.

Вы можете найти более подробную информацию о том, что происходит, и о том, как решить эту проблему вручную, в этом руководстве, но, к счастью, во встроенном модуле functools снова есть функция, которая может справиться с этим за нас! Он называется wraps, а документацию можно найти здесь.

Последняя версия нашего кода с добавленным wraps:

import functools
def add_commas(func):
    @functools.wraps
    def add_commas_wrapper(*args, **kwargs):
        original_string = func(*args, **kwargs)
        # needs to be int for string formatting
        original_int = int(original_string)
        # we are ignoring locale, using default thousands sep
        return f'{original_int:,}'
    return add_commas_wrapper
@add_commas
def one_million():
    return "1000000"
@add_commas
def one_billion():
    return "1000000000"
@add_commas
def times_100(num):
    return str(num * 100)
@add_commas
def minus_10000(num):
    return str(num - 10000)
@add_commas
def multiply(num1, num2):
    return str(num1 * num2)

Резюме

Просмотр нашего обновленного кода

Набор тестов проходит успешно, что означает, что мы успешно реализовали указанную «добавленную» функциональность ✅

Принцип единоначалия

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

Принцип открытого-закрытого

Мы не вносили никаких изменений в содержимое или сигнатуру вызовов исходных функций. Но, добавив 15 строк дополнительного кода, мы смогли «открыть» код для расширения, чтобы удовлетворить новые требования заинтересованных сторон.

Обзор того, что мы узнали

Некоторые виды сценариев, которые я называю «навесными» сценариями, может быть сложно реализовать без нарушения принципов SOLID, в частности принципа единой ответственности и принципа открытости. закрытый принцип. Подход с использованием декораторов и, в частности, декораторов Python, помогает решить эту проблему.

В некоторых случаях кто-то уже написал декоратор, который обрабатывает нужный вам сценарий «наклеек», особенно для распространенных сценариев, таких как кэширование, ведение журнала и контроль доступа. В других случаях вам нужно написать собственный декоратор для настраиваемых функций.

Чтобы написать свой собственный декоратор, вам необходимо помнить, что функции Python являются «первоклассными» объектами, в частности, они могут быть назначены переменным, могут быть переданы другим функциям и могут быть вложенными. . Помня об этом, а также с помощью синтаксического сахара и functools.wraps для обеспечения надлежащего самоанализа, вы можете написать свои собственные декораторы для решения этих «наклеенных» задач, следуя принципам SOLID.

использованная литература

В дополнение к ссылкам, указанным выше, следует отдать должное этому прекрасному руководству Дэна Бадера, который помог мне сформулировать обрамление:



И я снова сделаю ссылку на это подробное руководство от Real Python для всех, кто хочет вникнуть во все подробные детали:



Ознакомьтесь с этим комплексом упражнений, если хотите попрактиковаться, прежде чем использовать эту технику в продакшене:



Спасибо за чтение и дайте мне знать в комментариях, если вы знаете о каких-либо других заметных применениях декораторов или дополнительных руководств!