Декораторы Python, объяснение для начинающих

В мире программирования на Python декораторы могут быть элегантным и мощным инструментом в руках опытных разработчиков.

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

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

Быстрый обзор функций

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

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

functions

Функции принимают входные данные, используют их для выполнения набора кода и возвращают выходной сигнал

В Python функция записывается так:

def add_one(num):
    return num + 1

Когда мы хотим вызвать ее, мы можем написать имена функций в круглых скобках и передать необходимые входы (аргументы):

final_value = add_one(1)
print(final_value) # 2

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

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

Как передавать функции в качестве аргументов

Обычно при вызове функций с аргументами передаются значения типа Integer, Float, Strings, Lists, Dictionaries и другие типы данных.

Но мы также можем передать функцию в качестве аргумента:

def inner_function():
    print("inner_function is called")
    
def outer_function(func):
    print("outer_function is called")
    func()
   
outer_function(inner_function)
# outer_function is called
# inner_function is called
    

В данном примере мы создаем две функции: inner_function и outer_function.

outer_function имеет параметр func, который он вызывает после вызова самого себя.

first_class_citizens

внешняя_функция выполняется первой. Затем она вызывает функцию, которая была передана в качестве параметра

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

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

Так, outer_function может принимать в качестве параметра функцию и вызывать ее при выполнении.

Как вернуть функции

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

def outer_function():
    print("outer_function is called")
    
    def inner_function():
        print("inner_function is called")
      
    return inner_function

Обратите внимание, что в данном примере, возвращая inner_function, мы не вызывали его.

Мы вернули только ссылку на него, чтобы можно было хранить и вызывать его в дальнейшем:

returned_function = outer_function()
# outer_funciton is called

returned_function()
# inner_function is called

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

Как создавать декораторы в Python

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

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

def add_one_decorator(func):
    def add_one():
        value = func()
        return value + 1
        
    return add_one

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

def example_function():
    return 1
    
final_value = add_one_decorator(example_function)
print(final_value()) # 2

В данном примере мы вызываем функцию add_one_decorator и передаем в нее ссылку на example_function.

Когда мы вызываем функцию add_one_decorator, она создает новую функцию add_one, определенную внутри нее, и возвращает ссылку на эту новую функцию. Мы храним эту функцию в переменной final_value.

Таким образом, при выполнении функции final_value вызывается функция add_one.

Функция add_one, определенная внутри add_one_decorator, будет вызывать example_function, сохранять его значение и прибавлять к нему единицу.

В конечном итоге это приводит к тому, что возвращается 2 и выводится на консоль.

python_decorators-1

Процесс того, как будет выполняться код

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

Чтобы уточнить, декораторы не являются специфическими для Python. Это концепция, которая может применяться и в других языках программирования. Но в Python их можно легко использовать с помощью синтаксиса @.

Как использовать синтаксис @ в Python

at_syntax

Символ @

Как мы видели выше, когда мы хотим использовать декораторы, мы должны вызвать функцию-декоратор и передать в нее функцию, которую мы хотим модифицировать.

В Python мы можем использовать синтаксис @, что позволит нам быть гораздо более эффективными.

@add_one_decorator
def example_function():
    return 1

Записав над нашей функцией @add_one_decorator, она будет эквивалентна следующей:

example_function = add_one_decorator(example_function)

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

Как передавать аргументы с помощью декораторов

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

Например, если бы у нас была функция, требующая два параметра и возвращающая их сумму:

def add(a,b):
    return a + b
    
print(add(1,2)) # 3

А если бы мы использовали декоратор, который добавлял бы 1 к выводу:

def add_one_decorator(func):
    def add_one():
        value = func()
        return value + 1
        
    return add_one
    
@add_one_decorator
def add(a,b):
    return a + b
    
add(1,2)
# TypeError: add_one_decorator.<locals>.add_one() takes 0 positional arguments but 2 were given

При этом мы сталкиваемся с ошибкой: функция-обертка (add_one) не принимает аргументов, а мы указали два аргумента.

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

def add_one_decorator(func):
    def add_one(*args, **kwargs):
        value = func(*args, **kwargs)
        return value + 1
        
     return add_one
     
 @add_one_decorator
 def add(a,b):
     return a+b
    
 print(add(1,2)) # 4

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

args будет представлять собой список всех заданных позиционных ключевых слов, в данном случае [1,2].

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

Запись func(*args, **kwargs) указывает на то, что мы хотим вызвать func с теми же позиционными и ключевыми аргументами, которые были получены

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

Почему декораторы в Python полезны? Примеры реального кода

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

Logging

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

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

Вот простой пример того, как мы можем создать простой логгер (используя встроенный пакет Python logging) для сохранения информации о работе нашего приложения в файл с именем main.log:

import logging

def function_logger(func):
    logging.basicConfig(level = logging.INFO, filename="main.log")
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} ran with positional arguments: {args} and keyword arguments: {kwargs}. Return value: {result}")
        return result
    
    return wrapper

@function_logger
def add_one(value):
    return value + 1

print(add_one(1))

При выполнении функции add_one в файл main.log будет добавлен новый журнал:

INFO:root:add_one ran with positional arguments: (1,) and keyword arguments: {}. Return value: 2

Кэширование

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

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

В Python это можно реализовать с помощью декоратора @lru_cache из модуля functools, который поставляется вместе с Python.

LRU означает Least Recently Used, то есть всякий раз, когда функция была вызвана, будут сохранены использованные аргументы и возвращаемое значение. Но как только количество таких записей достигнет максимального размера, который по умолчанию равен 128, наименее недавно использованная запись будет удалена.

from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

В данном примере функция fibonacci принимает аргумент n и, если он меньше 1, возвращает n, в противном случае возвращается сумма функции, вызванной с помощью n-1 и n-2.

Таким образом, если функция вызывается со значением n=10, то она возвращает 55:

print(fibnoacci(10))
# 55

В этом случае, когда мы вызываем функцию fibonacci(10), она вызывает функцию fibonacci(9) и fibonacci(8), и так далее, пока не достигнет 1 или 0.

Если бы мы затем использовали эту функцию более одного раза:

fibonacci(50)
fibonacci(100)

Мы можем использовать кэш сохраненных записей. Таким образом, когда мы вызываем fibonacci(50), она может прекратить вызов функции fibonacci по достижении 10, а когда мы вызываем fibonacci(100), она может прекратить вызов функции по достижении 50, что делает программу намного более эффективной.

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

Возможность простого использования синтаксиса @ позволяет легко использовать дополнительные модули и пакеты.

Итоги

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

Если вам понравилась моя статья, загляните на мой канал YouTube, где вы найдете больше материалов по Python.

Счастливого кодирования!

Вернуться на верх