Skip to content

Функции и декораторы

Что такое args, kwargs?

args и kwargs — это способы передачи переменного количества аргументов в функцию. Слова args и kwargs — это просто соглашение, их можно заменить на любые другие названия. Важными здесь являются звездочки ( и *), которые указывают на способ передачи аргументов.

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

**kwargs работает так же, как и args, но вместо кортежа используется словарь. Это позволяет функции принимать любое количество именованных аргументов.

При определении функции аргументы должны следовать в строгом порядке:

1.Позиционные аргументы (обычные)

2.*args (позиционные или ключевые)

3.Ключевые аргументы (только по ключу)

4.**kwargs (любые именованные)

def example(a, b, *args, c, d=10, **kwargs):
    print(f"a: {a}, b: {b}")          # позиционные
    print(f"args: {args}")            # дополнительные позиционные
    print(f"c: {c}, d: {d}")          # ключевые (c - обязательный)
    print(f"kwargs: {kwargs}")        # дополнительные ключевые

# Вызов
example(1, 2, 3, 4, c=5, e=6, f=7)
# a: 1, b: 2
# args: (3, 4)
# c: 5, d: 10
# kwargs: {'e': 6, 'f': 7}

Как в функцию передаются аргументы: по ссылке или по значению?

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

def modify_list(my_list):
    my_list.append(4)        # изменяем список, на который ссылается аргумент
    my_list = [1, 2, 3]      # присваиваем новый список самому аргументу

original_list = [0]
modify_list(original_list)

print(original_list)  # Выведет: [0, 4], изменения внутри функции отражены на оригинальном объекте

В этом примере список original_list изменяется внутри функции modify_list(), добавляя в него элемент 4. Эти изменения отражаются на самом объекте original_list, так как мы работаем с одним и тем же списком. Однако, когда мы присваиваем новый список переменной my_list внутри функции, это не изменяет объект original_list, а только изменяет то, на что ссылается аргумент функции.

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


Значения по умолчанию в функциях

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

Проблема с изменяемыми объектами

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

Пример проблемы:

Функция с def func(items=[]) будет использовать один и тот же список при всех вызовах. Если вы добавите элемент в список при одном вызове, он останется там при следующем вызове.

Такое поведение обусловлено дизайном языка:

  • Производительность — не нужно создавать объекты при каждом вызове
  • Консистентность — одинаковое поведение для всех типов
  • Эффективность — особенно для тяжелых объектов

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

def func(items=None):
    if items is None:
        items = []  # Новый список при каждом вызове

С числами, строками, кортежами проблем не возникает — их нельзя изменить, поэтому поведение всегда предсказуемо.

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


Что такое лямбда-функции?

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

Лямбда-функция определяется следующим образом:

double = lambda x: x * 2

Лямбда-функции в основном используются в качестве аргументов функций высшего порядка, которые принимают другие функции в качестве аргументов (map, reduce, filter). Также они могут использоваться для создания более читаемого и компактного кода. Например, можно использовать лямбда-функцию вместо объявления обычной функции для преобразования списка:

numbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers))

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


Функции высшего порядка

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

В Python функции map, filter, и reduce являются классическими примерами функций высшего порядка, так как они принимают другие функции в качестве аргументов.


Что такое замыкание?

Замыкание (closure) — функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле внешней функции (свободные переменные). Внутренняя функция создается каждый раз во время выполнения внешней. Технически замыкание включает три компонента: - внешняя функция, которая определяет некоторую область видимости и в которой определены некоторые переменные и параметры - лексическое окружение - переменные и параметры (лексическое окружение), которые определены во внешней функции - вложенная функция, которая использует переменные и параметры внешней функции

Для определения замыканий в Python применяются локальные функции:

def multiply(num1):
    var = 10
    def inner(num2):
        return num1 * num2
    return inner

Тут замыканием является функция inner. Функция inner использует внутри себя переменную num1 - параметр функции multiply, поэтому переменная num1 будет запомнена, а вот переменная var не используется и запоминатся не будет. Использование созданной функции выглядит так: Сначала делается вызов функции multiply с передачей одного аргумента, значение которого запишется в переменную num1:

In [2]: mult_by_9 = multiply(9)

Переменная mult_by_9 ссылается на внутреннюю функцию inner и при этом внутренняя функция помнит значение num1 = 9 и поэтому все числа будут умножаться на 9:

In [3]: mult_by_9
Out[3]: <function __main__.multiply.<locals>.inner(num2)>

In [4]: mult_by_9.__closure__
Out[4]: (<cell at 0xb0bd5f2c: int object at 0x836bf60>,)

In [5]: mult_by_9.__closure__[0].cell_contents
Out[5]: 9

In [8]: mult_by_9(10)
Out[8]: 90

In [9]: mult_by_9(2)
Out[9]: 18

Декораторы

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

Декоратор добавляет поведение функции без изменения ее исходного кода.

  • Технически декораторы реализуются через механизм замыкания
  • Это некий синтаксический сахар

Дублирующий код противоречит принципу единой ответственности, а также принципу DRY. И для того чтобы не повторять, чтобы вынести эту функциональность отдельно имеет смысл делать декоратор. Применяется в:

  • rout (flask)
  • loginrequaet (django, flask)
  • логировании
  • подсчете времени

Примечание: декоратор может быть создан как класс с магическим методом call. Поэтому более точно говорить, что декоратор — это вызываемый объект (callable). К вызываемым объектам относят функции и классы.

def my_decorator(func):
    def wrapper():
        print("Любой код, исполняющийся перед вызовом функции")
        result = func()
        print("Любой код, исполняющийся после вызова функции")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("Привет!")

say_hello()

Встроенные декораторы Python:

  • @staticmethod — для обозначения статического метода класса, который не зависит от состояния экземпляра.
  • @classmethod — для обозначения метода, который принимает класс (cls) в качестве первого аргумента, а не экземпляр.
  • @property — позволяет превратить метод в атрибут, что полезно для создания геттеров и сеттеров.

Декоратор с параметрами

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

@my_decorator(5)  # <-- пример декоратора с параметром
def my_function():
    pass

Примеры

  1. Декоратор, который повторяет выполнение функции n раз

    def repeat(n):
        """Декоратор, который повторяет выполнение функции n раз"""
        def decorator(func):
            def wrapper(*args, **kwargs):
                for i in range(n):
                    print(f"Вызов {i+1}/{n}")
                    result = func(*args, **kwargs)
                return result
            return wrapper
        return decorator
    
    # Использование
    @repeat(3)
    def say_hello(name):
        print(f"Привет, {name}!")
        return f"Сообщение для {name} отправлено"
    
    say_hello("Анна")
    # Вывод:
    # Вызов 1/3
    # Привет, Анна!
    # Вызов 2/3
    # Привет, Анна!
    # Вызов 3/3
    # Привет, Анна!
    
  2. Декоратор для логирования с разными уровнями

    def log(level="INFO"):
        """Декоратор для логирования с разными уровнями"""
        def decorator(func):
            def wrapper(*args, **kwargs):
                print(f"[{level}] Вызов функции {func.__name__}")
                print(f"[{level}] Аргументы: {args}, {kwargs}")
                result = func(*args, **kwargs)
                print(f"[{level}] Функция {func.__name__} завершилась")
                return result
            return wrapper
        return decorator
    
    # Использование
    @log("DEBUG")
    def calculate(a, b):
        return a + b
    
    calculate(5, 3)
    # Вывод:
    # [DEBUG] Вызов функции calculate
    # [DEBUG] Аргументы: (5, 3), {}
    # [DEBUG] Функция calculate завершилась
    

В Python могу применить декоратор к классу?

Да, в Python можно применять декораторы к классам. Они позволяют: * Изменить сам класс * Добавить ему атрибуты * Обернуть методы * Зарегистрировать класс в фабрике * Сделать его синглтоном и т.д. Пример: декоратор, добавляющий атрибут

def add_version(cls):
    cls.version = "1.0"
    return cls

@add_version
class MyClass:
    pass

print(MyClass.version)  # → "1.0"