Функции в Python (авторская версия)

Этот урок объединяющий. Здесь будет много повторений. Но также авторское объяснение темы.

Что такое Функция

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

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

Собственно Учитель (программист) и так создает программу для каких-то целей. Он подключает (берет книгу) с библиотеками. Решает самостоятельно задачи.

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

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

Особенностью встроенной функции является название, которое отражает его способности, например dict().

dict– это название встроенной функции.

()– здесь мы можем вписать действия или код который применяется для реализации встроенной функции.

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

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

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

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

Итак, собственная функция (Ученик) выглядит так; def Название Ученика (): Само задание (тело функции)

def ключевое слово, которое обозначает начало определения функции (подготовку задания Учеником).

Название Ученика (Имя функции) — название, которое будет использовано для определения функции и её вызова. Название прописывается с маленькой буквы. Если в нём несколько слов, их нужно разделять нижним подчёркиванием (_). Имя должно давать пользователю понимание, что делать. Например, «решить», «назвать имя», «задать значение». Имя Ученика может быть любым (с некоторыми ограничениями выше). Конечно Ученик может иметь имя с намеком на его задание.

(): Дополнительные данные ( Параметры функции (аргументы). Внутри скобок будем получать, передавать дополнительные данные для Ученика.

: Двоеточие, конец определения Задания (Функции). Пишется без пробела, сразу за закрытием скобок.

Само задание (тело функции). Здесь выполняется задание Учеником. Тело функции начинается с новой строки и с отступом (отступ в Питоне является вложением в функцию (тоесть принадлежит только этой функции.

Пример.

def sayHello():
print('Привет, Мир!') # задание
# Конец задания

На примере, мы дали задание Ученику sayHello, подготовить выполнение встроенной функции print() с текстом Привет, Мир!

Теперь мы можем попросить Ученика sayHello, показать что же он выполнил.

sayHello() # вызов задания (функции)

Ученик нам покажет выполненное задание в виде текста Привет, Мир! Как видим мы напрямую обращаемся к Ученику для показа задания.

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

Дадим задание (объявим функцию) Ученику: hello

def hello():
    print('Привет, Мир!')

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

# код выполняется последовательно, поэтому сейчас интерпретатор
# не знает о существовании функции hello
hello()

def hello():
    print('Привет, Мир!')

> NameError: name 'hello' is not defined # ошибка в коде

Поэтому стоит лишь поменять объявление и вызов местами, и всё заработает:

def hello():
    print('Привет, Мир!')


hello()
> Привет, Мир!

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

Параметры и аргументы функции

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

А теперь Учитель хочет получить результат с данными которые он передает Ученику уже после создания ему задания.

Он дает ему две коробочки (переменные) a и b. Ученик складывает эти коробочки внутри скобок при создании задания (функции), мы же помним что внутри скобок ранее ничего не было.

А потом Учитель дает Ученику цифры в каждую коробочку. И запрашивает результат (вызывает функцию).

# a, b - параметры функции
def test(a, b):
    # do something (сделай что-нибудь)

# 120, 404 — аргументы
test(120, 404)

Аргументы часто путают с параметрами:

  • Параметр — это переменная, которой будет присваиваться входящее в функцию значение (та самая коробочка).
  • Аргумент — само это значение, которое передается в функцию при её вызове (значение, которое передаем при запросе задания у Ученика.

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

Ключевая особенность заданий (функций) — возможность возвращать значение.

Ну иначе как? Только текст Ученик будет писать, пусть научится считать и умножать.

Оператор return

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

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

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

# Функция будет принимать два множителя, а возвращать их округленное 
# до целого числа произведение
def int_multiple(a, b):# a, b - параметры функции
    product = a * b # умноженный результат передадим в переменную product
    # возвращаем значение
    return int(product)# при возврате приведем результат в целое число при помощи встр. фунуции int()


print(int_multiple(341, 2.7)) # передадим аргументы в int_multiple и получим результат в print()

> 920 # такой будет результат

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

Задание (Функция) в Python способны возвращать любой тип объекта.

Подведем общую структуру с return

defимя_функции ([параметры]):

    инструкции

    returnвозвращаемое_значение

Определим простейшую функцию, которая возвращает значение:

def get_message():
    return "Hello METANIT.COM"

Здесь после оператора return идет строка "Hello METANIT.COM" – это значение и будет возвращать функция get_message().

Затем это результат функции можно присвоить переменной или использовать как обычное значение:

def get_message():
    return "Hello METANIT.COM"
 
 
message = get_message()  # получаем результат функции get_message в переменную message
print(message)          # Hello METANIT.COM
 
# можно напрямую передать результат функции get_message
print(get_message())    # Hello METANIT.COM

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

def double(number):
    return 2 * number

Здесь функция double будет возвращать результат выражения 2 * number:

def double(number):
    return 2 * number
 
result1 = double(4)     # result1 = 8
result2 = double(5)     # result2 = 10
print(f"result1 = {result1}")   # result1 = 8
print(f"result2 = {result2}")   # result2 = 10

Или другой пример – получение суммы чисел:

def sum(a, b):
    return a + b
 
 
result = sum(4, 6)                  # result = 0
print(f"sum(4, 6) = {result}")      # sum(4, 6) = 10
print(f"sum(3, 5) = {sum(3, 5)}")   # sum(3, 5) = 8

Использование return() для возврата нескольких значений

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

Входные данные:

def statFun(a, b):
difference = a - b
percent_diff = (difference / a) * 100
return difference, percent_diff
difference, percent_diff = statFun()
print(difference)
print(percent_diff)

Здесь функция statFun() выдает два значения.

Вывод:

8
20

Использование return() с аргументами

В Python с return можно использовать аргументы. Аргументы — это параметры, передаваемые пользователем на вход функции, и мы спокойно можем вернуть их в return.

def divNum(a, b):
if b != 0:
return a/b
else:
return 0
print(divNum(4, 2))
print(divNum(2, 0))

Здесь функция divNum() принимает два аргумента и, если второй аргумент ненулевой, делит их, в противном случае возвращает 0.

Вывод:

2
0

Выход из функции

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

def get_message():
    return "Hello METANIT.COM"
    print("End of the function")
 
print(get_message())

С точки зрения синтаксиса данная функция корректна, однако ее инструкция print("End of the function") не имеет смысла – она никогда не выполнится, так как до ее выполнения оператор return возвратит значение и произведет выход из функции.

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

def print_person(name, age):
    if age > 120 or age < 1:
        print("Invalid age")
        return
    print(f"Name: {name}  Age: {age}")
 
 
print_person("Tom", 22)
print_person("Bob", -102)

Позиция Аргументов

Вернемся к нашим коробочкам при изменении задания уже созданной функции.

Позиционные

Python обрабатывает позиционные аргументы слева направо:

У нас есть две коробочки price и discount

Мы передаем цифры в коробочки по порядку final_price(1000, 5). Как передали слево на право так и ложим в коробочки слево на право. В коробочку price передаем 1000 а в коробочку discount соответственно 5.

def final_price(price, discount):
    return price - price * discount / 100


print(final_price(1000, 5))

Именованные

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

def print_trio(a, b, c):
    print(a, b, c)


print_trio(c=4, b=5, a=6)

> 6 5 4

Хоть коробочки и стоят по порядку a, b, c, но они имеют каждая своё имя. Поэтому можно ложить цифры в коробочки не по порядку их расположения у Ученика c=4, b=5, a=6. Тоесть мы явно указываем в какую коробчку ложим c=4.

Необязательные параметры (параметры по умолчанию)

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

def not_necessary_arg(x='My', y='love'):
    print(x, y)

# если не передавать в функцию никаких значений, она отработает со значениями по умолчанию
not_necessary_arg()
> My love

# переданные значения заменяют собой значения по умолчанию
not_necessary_arg(2, 1)
> 2 1

Вот именно так, если Учитель забыл передать данные в коробочку свои значения, Ученик использует по умолчанию кауие там уже лежат x=’My’, y=’love’.

Причечание. По умолчанию может лежать только и в одной коробочке.

Напоминание

  • Параметр — это переменная, которой будет присваиваться входящее в функцию значение (та самая коробочка).
  • Аргумент — само это значение, которое передается в функцию при её вызове (значение, которое передаем при запросе задания у Ученика.

Аргументы переменной длины (args, kwargs)

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

Звёздочка “*” перед именем параметра сообщает интерпретатору о том, что количество позиционных аргументов будет переменным:

ef infinity(*args):
    print(args)

infinity(42, 12, 'test', [6, 5])
> (42, 12, 'test', [6, 5])

Переменная args составляет кортеж из переданных в функцию аргументов.

Функции в питоне могут также принимать и переменное количество именованных аргументов. В этом случае перед названием параметра ставится “**“:

def named_infinity(**kwargs):
    print(kwargs)

named_infinity(first='nothing', second='else', third='matters')
> {'first': 'nothing', 'second': 'else', 'third': 'matters'}

Передача аргументов в виде списка

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

def my_function(stationery):
    for i, j in enumerate(stationery):
        print(f'Товар #{i + 1} - {j}')
        
stuff = ['карандаш', 'ручка', 'блокнот', 'альбом', 'тетрадь', 'ластик']   
my_function(stuff)

Результат

Товар #1 - карандаш
Товар #2 - ручка
Товар #3 - блокнот
Товар #4 - альбом
Товар #5 - тетрадь
Товар #6 - ластик

Передача по значению и по ссылке

В Python аргументы могут быть переданы, как по ссылке, так и по значению. Всё зависит от типа объекта.

Если объект неизменяемый, то он передаётся в функцию по значению. Неизменяемые объекты это:

  • Числовые типы (int, float, complex).
  • Строки (str).
  • Кортежи (tuple).
num = 42

def some_function(n):
    # в "n" передается значение переменной num (42)
    n = n + 10 # а здесь 52
    print(n)


some_function(num)
print(num)  # "num" по прежнему содержит 42

>
52
42

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

В Python изменяемые объекты это:

  • Списки (list).
  • Множества (set).
  • Словари (dict).
num = [42, 43, 44]

def some_function(n):
    # в "n" передается ссылка на переменную "num". 
    # "n" и "num" ссылаются на один и тот же объект
    n[0] = 0
    print(n)


some_function(num)
print(num)  # "num" изменился

>
[0, 43, 44]
[0, 43, 44]

Создание вложенных функций

Вложенные (или внутренние, англ. inner, nested) функции – это функции, которые мы определяем внутри других функций.

def outer_function():
    print("Я внешняя функция")
        def inner_function():
            print("Я внутренняя функция")
        inner_function()
outer_function()

В этом примере мы создали внутреннюю функцию inner_function внутри внешней функции outer_function. Затем мы вызвали внутреннюю функцию из внешней функции. В результате вызова outer_function() будет выведено:

Я внешняя функция
Я внутренняя функция

В Python такая функция имеет прямой доступ к переменным и именам, определенным во включающей её функции.

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

Начнем с примера кода, содержащего вложенную функцию:

def outer_func():
def inner_func():
print("Hello, World!")
inner_func()
>>> outer_func()
Hello, World!

В этом коде мы определяем inner_func() внутри outer_func() для вывода на экран строки Hello, World!.

Для этого мы вызываем inner_func() в последней строке outer_func().

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

А потом запрашивает результат работы старшего Ученика outer_func() и он дает ответ младшего Ученика.

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

Область видимости функций

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

Локальная область (local scope)

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

В примере ниже переменная c объявлена внутри функции sum. Её можно использовать только внутри функции, если попробовать сделать это в другом месте, то Python выдаст ошибку:

def sum(a, b):
    c = a + b
    return c

Не локальная область (enclosing function scope)

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

Напишем код счётчика, который подсчитывает количество вызовов функции. Используем для этого вложенную архитектуру:

def make_counter():
    # Объявляем переменную count в объемлющей функции
    count = 0

    def counter():
        # Указываем, что count находится в объемлющей функции
        nonlocal count 
        count += 1
        return count

    return counter

# Создаём счётчик
call_counter = make_counter()

# Пример использования счётчика
print(call_counter())  # Вывод: 1
print(call_counter())  # Вывод: 2
print(call_counter())  # Вывод: 3

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

Глобальная область (global scope)

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

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

Например, нам нужно написать программу для кондитерской, которая ведёт учёт изготовленных тортов. Каждый раз, когда мы продаём новые торты, мы будем изменять переменную cake:

# Глобальная переменная, которая обозначает количество сделанных тортов
cake_count = 10

def modify_cake():
global cake_count
# Изменяем значение глобальной переменной
cake_count = 15

modify_cake()
print(modify_cake) # Вывод: 15

И снова о коробочках

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

Области видимости определяют, в какой части программы мы можем работать с той или иной переменной (коробочкой), а от каких переменная «скрыта».

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

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

В Python существует целых 3 области видимости:

  • Локальная
  • Глобальная
  • Нелокальная

Последняя, нелокальная область видимости, была добавлена в Python 3.

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

Локальная область видимости

Рассмотрим функцию, которая выведет список some_list поэлементно:

def print_list(some_list):
for element in some_list:
print(element)

Здесь element и some_list – локальные переменные, которые видны только внутри функции, и которые не могут использоваться за ее пределами с теми значениями, которые были им присвоены внутри функции при ее работе. Ученик использует свои вычисления и цифры ложит в свои коробочки. То есть, если мы в основном теле программы вызовем print(element), то получим ошибку:

NameError: name 'element' is not defined

Учитель не видит какие переменные использует Ученик. Теперь мы поступим следующим образом:

def print_list(some_list):
for element in some_list:
print(element)

element = 'q'
print_list([1, 2, 3])
print(element)

И получим:

1
2
3
q

Здесь переменная element внутри функции (у Ученика) и переменная с таким же именем вне ее – это две разные переменные, их значения не перекрещиваются и не взаимозаменяются. Они называются одинаково, но ссылаются на разные объекты в памяти. Более того, переменная с именем element внутри функции живет столько же, сколько выполняется функция и не больше. Но будьте аккуратны с тем, чтобы давать локальным и глобальным переменным одинаковые имена, сейчас покажу почему:

def print_list(some_list):
for element in sudden_list:
print(element)

sudden_list = [0, 0, 0]
print_list([1, 2, 3])

Результат:

0
0
0

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

Здесь важно поговорить о константах. Интерпретатору Python нет разницы как вы называете переменную, поэтому код выше будет лучше переписать в следующем виде:

SUDDEN_LIST = [0, 0, 0]

def print_list(some_list):
for element in SUDDEN_LIST:
print(element)

print_list([1, 2, 3])

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

Локальная переменная используется только Учеником для своих внутрених задач.

Глобальная область видимости

В Python есть ключевое слово global, которое позволяет изменять изнутри функции значение глобальной переменной. Оно записывается перед именем переменной, которая дальше внутри функции будет считаться глобальной. Как видно из примера, теперь значение переменной candy увеличивается, и обратите внимание на то, что мы не передаем ее в качестве аргумента функции get_candy().

candy = 5 # изначальное значение переменной 5

def get_candy():
    global candy 
    candy += 1
    print('У меня {} конфет.'.format(candy))
    
get_candy() # мы один раз вызвали функцию, в candy записали 6 (после прибавления единицы)
get_candy() # мы еще раз вызвали функцию, в candy записали 7 (еще раз добавили единицу)
print(candy) # здесь мы распечатали переменную, которая была вначале кода но изменилась после выполнения двух функций

В результате получим:


У меня 6 конфет.
У меня 7 конфет.
7

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

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

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

Нелокальная область видимости

Появилось это понятие в Python 3 вместе с ключевым словом nonlocal. Логика его написания примерно такая же, как и у global. Однако у nonlocal есть особенность. Nonlocal используется чаще всего во вложенных функциях, когда мы хотим дать интерпретатору понять, что для вложенной функции определенная переменная не является локальной, но она и не является глобальной в общем смысле.

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

def get_candy():
candy = 5
def increment_candy():
nonlocal candy
candy += 1
return candy
return increment_candy

result = get_candy()()
print('Всего {} конфет.'.format(result))

Результат:

Всего 6 конфет.

Насколько это полезно вам предстоит решить самостоятельно. Больше примеров вы можете найти здесь.

В качестве вывода можно сформулировать несколько правил:

  1. Изнутри функции видны переменные, которые были определены и внутри нее и снаружи. Переменные, определенные внутри – локальные, снаружи – глобальные.
  2. Снаружи функций не видны никакие переменные, определенные внутри них.
  3. Изнутри функции можно изменять значение переменных, которые определены в глобальной области видимости с помощью спецификатора global.
  4. Изнутри вложенной функции с помощью спецификатора nonlocal можно изменять значения переменных, которые были определены во внешней функции, но не находятся в глобальной области видимости.

Интересная ссылка

Lambda-функции

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

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

lambda аргументы: выражение

В качестве примера рассмотрим лямбда-функцию, которая проверяет чётность числа и выводит True или False. В качестве параметра передаётся переменная num, а после двоеточия указано выражение num % 2 == 0:

is_even = lambda num: num % 2 == 0

print(is_even(4)) # Вывод: True
print(is_even(7)) # Вывод: False

Определим простейшее лямбда-выражение:

message = lambda: print("hello")
 
message()   # hello

Здесь лямбда-выражение присваивается переменной message. Это лямбда-выражение не имеет параметров, ничего не возвращает и просто выводит строку “hello” на консоль. И через переменную message мы можем вызвать это лямбда-выражение как обычную функцию. Фактически оно аналогично следующей функции:

def message(): 
    print("hello")

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

square = lambda n: n * n
 
print(square(4))    # 16
print(square(5))    # 25

В данном случае лямбда-выражение принимает один параметр – n. Справа от двоеточия идет возвращаемое значение – n* n. Это лямбда-выражение аналогично следующей функции:

def square2(n): return n * n

Аналогичным образом можно создавать лямбда-выражения, которые принимают несколько параметров:

um = lambda a, b: a + b
 
print(sum(4, 5))    # 9
print(sum(5, 6))    # 11

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

def do_operation(a, b, operation):
    result = operation(a, b)
    print(f"result = {result}")
 
do_operation(5, 4, lambda a, b: a + b)  # result = 9
do_operation(5, 4, lambda a, b: a * b)  # result = 20

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

То же самое касается и возвращение лямбда-выражений из функций:

def select_operation(choice):
if choice == 1: return lambda a, b: a + b elif choice == 2: return lambda a, b: a - b else: return lambda a, b: a * b operation = select_operation(1) # operation = sum print(operation(10, 6)) # 16 operation = select_operation(2) # operation = subtract print(operation(10, 6)) # 4 operation = select_operation(3) # operation = multiply print(operation(10, 6)) # 60

Замыкания в Python

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

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

Но возникает вопрос: “Можно ли сделать так, чтобы после завершения работы функции, часть локальных переменных не уничтожалась, а сохраняла свои значение до следующего запуска?” Да, так в python сделать можно.

Локальная переменная не будет уничтожена, если на нее где-то останется “живая” ссылка, после завершения работы функции. Эту ссылку может сохранять вложенная функция. Давайте вспомним, что такое вложенная функция следующим примером:

def main_func():

def inner_func():
print('hello my friend')

inner_func()

Такая функция при вызове печатает hello my friend и всё. Но если мы изменим ее так, чтобы она возвращала вложенную функцию, после чего присвоим функцию переменной b и взглянем на нее:

def main_func():

def inner_func():
print('hello my friend')

return inner_func

b = main_func()
print(b)

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

Старший Ученик main_func получил результат от младшего Ученика inner_func. После вызова функции (получения результата), мы сохраняем результат на ссылку b = main_func() (место где лежит результат).

При распечатке print(b) получим не коробочку а место где она лежит <function main_func.<locals>.inner_func at 0x00000233F1E01158>

Давайте познакомимся с этим термином.

Замыкание (closure) — функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле внешней функции (свободные переменные).

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

def main_func():
name = 'Ivan'
def inner_func():
print('hello my friend', name)

return inner_func
b = main_func()
b()

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

Здесь мы значение от старшего Ученика передали в переменную b. И переменную b вызываем как функцию (другой Ученик). Причем другой Ученик получает доступ к значением старшего Ученика.

Замыкание (closure) или фабричная функция это функция определяемая и возвращаемая другой функцией (старший Ученик получает результат младшего Ученика), при этом замыкание получает доступ к значениям и объектам в области видимости “родительской” (или объемлющей) функции независимо от того из какой области видимости происходит вызов замыкания (младший Ученик получает доступ к информации старшего Ученика).

def outers(): 
n = 2

def closure():
return n ** 2
return closure


closure_foo = outers() # Вызываем внешнюю функцию, возвращаемая функция (замыкание) присваивается переменной
print(closure_foo) # <function outers.<locals>.closure at 0x7f254d6fe170>
num = closure_foo() # Вызываем замыкание, результат присваивается переменной
print(num) # 4

# Второй вариант вызова замыкания
print(outers()()) # 4

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

Второй вариант вызова замыкания не сложно понять, если проследить эволюцию значений:

Скобки после имени функции говорят интерпретатору о том, что ее необходимо вызвать. После вызова outers(), на ее место возвращается замыкание closure, к которому добавляется оставшаяся пара скобок. Замыкание вызывается, возвращая на свое место результат.

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

def outers(lst):

def closure():
return lst[0] * 2
return closure


x = ['a']
closure_foo = outers(x) # Вызываем внешнюю функцию, передав ей список в качестве аргумента
print(closure_foo()) # aa

x[0] = 'b' # меняем единственный элемент списка
print(closure_foo()) # bb

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

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

def multiplier(factor):

def closure(x):
return factor * x
return closure


double = multiplier(2)
triple = multiplier(3)

print(double(5)) # 10 результат аналогичен вызову multiplier(2)(5)
print(triple(4)) # 12 результат аналогичен вызову multiplier(3)(4)

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

Так же для создания замыкания может использоваться анонимная функция.

def modify(foo):
return lambda x: foo(x)


"""
# результат аналогичен обычному синтаксису
def modify(foo):

def closure(x):
return foo(x)
return closure
"""


to_str = modify(str)
to_str(152) # '152'
to_bool = modify(bool)
to_bool('John Cena') # True
to_bool('') # False
adder = modify(lambda x: x + 1)
adder(152) # 153

В данном примере функции modify передаются различные функции (в том числе анонимные). Полученное замыкание возвращает результат применения функции к своему аргументу.

Почти настоящий код

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

def count_calls():
counter = 0

def closure(print_result=False):
nonlocal counter
if print_result:
return counter
counter += 1
return counter
return closure


counter = count_calls() # Вызвав функцию, получаем счетчик (замыкание)

for _ in range(5):
counter() # Вызываем счетчик

print(counter(True)) # Проверяем результат подсчета: 5

for _ in range(2):
counter()

print(counter(1)) # 7

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

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

Введение в декораторы

Как вам, наверное, известно в Python практически всё является объектом, а функции рассматриваются как объекты первого класса. Это означает, что функции могут передаваться и использоваться в качестве аргументов, как и любой другой объект (например, строка, int, float, список и т.д.). Также функции можно присваивать переменным, то есть рассматривать их как любые другие объекты. Рассмотрим следующий пример:

def func_a():
return "I was angry with my friend."
def func_b():
return "I told my wrath, my wrath did end"
def func_c(*funcs):
for func in funcs:
print(func())
main_func = func_c
main_func(func_a, func_b)

Результат

>>> I was angry with my friend.
>>> I told my wrath, my wrath did end

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

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

Затем мы присваиваем функцию func_c переменной main_func.

В итоге мы запускаем функцию main_func() и она ведет себя точно так же, как и func_c.

Пишем декоратор сами

И так вооруживший, полученными выше, знаниями о принципе действия нелокальных non-local переменных, создадим наш первый самый простейший декоратор.

def deco(func):
def wrapper():
print("Это сообщение будет напечатано до вызова функции.")
func()
print("Это сообщение будет напечатано после вызова функции.")
return wrapper

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

def ans():
print(42)

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

ans = deco(ans)
ans()

Ниже приведен результат выполнения этого кода.

>>> Это сообщение будет напечатано до вызова функции.
42
Это сообщение будет напечатано после вызова функции.

Весь код

def deco(func):
def wrapper():
print("Это сообщение будет напечатано до вызова функции.")
func()
print("Это сообщение будет напечатано после вызова функции.")
return wrapper

def ans():
print(42)

ans = deco(ans)
ans()

В двух строках кода выше мы можем наблюдать наш простейший декоратор в действии.

Функция deco принимает другую функцию в качестве своего параметра (deco(ans)) , манипулирует этой функцией внутри функции-обёртки (ans = deco(ans)) и затем возвращает функцию-обёртку.

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

Проще говоря, декораторы оборачивают декорируемую функцию def ans(): и изменяют ее поведение, не изменяя ее кода.

Функция декоратора выполняется во время импорта/определения декорированной функции, а не при ее вызове.

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

def deco(func):
"""Этот модифицированный декоратор также возвращает результат функции func."""
def wrapper():
print("Это сообщение будет напечатано до вызова функции.")
val = func()
print("Это сообщение будет напечатано после вызова функции.")
return val
return wrapper

def ans():
return 42

В примере выше функция-обертка возвращает результат целевой функции и выполнения кода обертки. Этот прием позволяет получить модифицированной результат выполнения целевой функции.

ans = deco(ans)
print(ans())
>>> Это сообщение будет напечатано до вызова функции.
Это сообщение будет напечатано после вызова функции.
42

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

Используем символ @ (синтаксический сахар )

То, как вы использовали декоратор в последнем разделе, может показаться немного неуклюжим. Во-первых, нам необходимо использовать имя декорируемой (целевой) функции ans три раза для того, чтобы вызвать и использовать наш декоратор. Кроме того, становится труднее понять, где в коде наш декоратор на самом деле вызывается, и что его функциональное назначение декорирование целевых функций – код становится трудно читаемым и непонятным. Поэтому в Python предусмотрена возможность использования декораторов в вашем коде с применением специального синтаксиса с символом @. И вы можете использовать декораторы при определении своих функций, как это показано в примере ниже:

@deco
def func():
# ...
# код нашей декорируемой функции


# теперь можно вызвать нашу декорируемую функцию как обычную
func()

Часто этот синтаксис называют pie syntax (по аналогии со слоями пирога). Его использование повышает читаемость кода, однако его использование по сути является ни чем иным, как синтаксическим сахаром, использующимся вместо, рассмотренной нами выше, инструкции func = deco (func).

Используем аргументы при декорировании функций

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

def yell(func):
def wrapper(*args, **kwargs):
val = func(*args, **kwargs)
val = val.upper() + "!"
return val
return wrapper

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

@yell
def hello(name):
return f"Hello {name}"
hello("redowan")
>>> 'HELLO REDOWAN!'

Функция hello принимает строку в качестве параметра name и возвращает сообщение в виде трансформированной строки.

И так наш декоратор yell изменяет возвращаемую целевой функций строку, преобразует ее в верхний регистр и добавляет символ ! без непосредственного изменения кода в функции hello.

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

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

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

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

Рассмотрим простой пример

def decorator(foo):
    def wrapped():
        print('текст до функции foo')
        foo()
        print('текст после функции foo')
    return wrapped

def greet():
    print('Hello')

    
# Применяем декоратор к функции greet
greet = decorator(greet)
    

greet()     # текст до функции foo
            # Hello
            # текст после функции foo

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

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

Предыдущий пример с учетом данного синтаксиса будет выглядеть так:

def decorator(foo):
    def wrapper():
        print('text before foo call')
        foo()
        print('text after foo call')
    return wrapper

@decorator
def greet():
    print('Hello')

greet()    # text before foo call
            # Hello
            # text after foo call

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

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

def each_pheasant(foo):
    def wrapper():
        print('каждый')
        foo()
        print('фазан')
    return wrapper

def hunter_is_sitting(foo):
    def wrapper():
        print('охотник')
        foo()
        print('сидит')
    return wrapper

def wishes_where(foo):
    def wrapper():
        print('желает')
        foo()
        print('где')
    return wrapper

def know():
    print('знать')

know = each_pheasant(hunter_is_sitting(wishes_where(know)))
know()

# каждый
# охотник
# желает
# знать
# где
# сидит
# фазан

Результат выполнения данного кода будет аналогичен синтаксису:

@each_pheasant
@hunter_is_sitting
@wishes_where
def know():
    print('знать')

know()

# каждый
# охотник
# желает
# знать
# где
# сидит
# фазан

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

Порядок применения декораторов
Порядок применения декораторов

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

Использование декораторов в ином порядке приведет к неверному результату:

@wishes_where
@hunter_is_sitting
@each_pheasant
def know():
    print('знать')

know()

# желает
# охотник
# каждый
# знать
# фазан
# сидит
# где

Декораторы с аргументами

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

Допустим, у нас есть сложная функция (longrunningfoo) выполнение которой занимает продолжительное время. Но мы заметили, что в нашем сценарии чаще всего при ее вызове используются одни и те же значения (которые впрочем, могут со временем измениться). Для оптимизации работы приложения, мы можем написать декоратор, который будет кэшировать результаты выполнения этой функции. При этом очевидно, что кол-во кэшированных значений должно быть ограничено, иначе производительность будет снижаться уже из-за объема кэша.
В первую очередь создадим функцию с параметром, который будет указывать кол-во значений в кэше.
def memoize(amount):
Далее мы создаем функцию-декоратор, которая принимает оборачиваемую функцию в качестве аргумента
def inner(foo):
а уже внутри нее мы создаем функцию-обертку
def wrapper(arg):
и описываем логику работы декоратора, условие проверяющее, есть ли у нас уже результат для текущего аргумента и условие проверяющее, достигнут ли лимит значений в кэше. После этого собираем все вместе. Получившаяся функция может иметь следующий вид:

def memoize(amount):
args_list = []
memoize_dict = {}

def inner(foo):
def wrapper(arg):
if arg not in memoize_dict:
if len(args_list) == amount:
value = args_list.pop(0)
memoize_dict.pop(value)
args_list.append(arg)
new_value = foo(arg)
memoize_dict[arg] = new_value
return memoize_dict[arg]
return wrapper
return inner

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

decorator = memoize(3)
long_running_foo = decorator(long_running_foo)

В первой строке мы вызываем функцию memoize с аргументом 3 (кол-во значений в кэше), которая возвращает нам замыкание inner, которое и будет нашим декоратором. Его мы присваиваем переменной decorator. Функция inner ожидает в качестве аргумента оборачиваемую функцию. Соответственно, мы вызываем получившуюся функцию и передаем ей в качестве аргумента функцию long_running_foo, после чего обернутую функцию присваиваем переменной с тем же именем. Данный синтаксис можно сократить до одной строки следующим образом:

long_running_foo = memoize(3)(long_running_foo)

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

def memoize(amount):
args_list = []
memoize_dict = {}

def inner(foo):
def wrapper(arg):
if arg not in memoize_dict:
if len(args_list) == amount:
value = args_list.pop(0)
memoize_dict.pop(value)
args_list.append(arg)
new_value = foo(arg)
memoize_dict[arg] = new_value
return memoize_dict[arg]
return wrapper
return inner

@memoize(3)
def long_running_foo(arg):
print('processing..', end=' ')
return arg

print(long_running_foo(1)) # processing.. 1
print(long_running_foo(2)) # processing.. 2
print(long_running_foo(3)) # processing.. 3
print(long_running_foo(2)) # 2
print(long_running_foo(3)) # 3
print(long_running_foo(4)) # processing.. 4
print(long_running_foo(3)) # 3
print(long_running_foo(1)) # processing.. 1

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

Сохранение атрибутов при декорировании функции

У любой объявленной функции есть множество атрибутов, начиная с имени, имени модуля и docstring’a, и заканчивая пользовательскими атрибутами. Получить доступ с этим атрибутам можно по соответствующим именам.

def print_greet(name):
    """Some description"""
    print('Hello, ' + name + '!')

print(print_greet.__name__)    # print_greet
print(print_greet.__doc__)      # Some description

Однако, при вызове задекорированной функции, вызывается именно обертка, и при попытке получить какие-либо атрибуты, будут получены атрибуты именно обертки:

def decorator(foo):
    def wrapper(*args):
        foo(*args)
    return wrapper

@decorator
def print_greet(name):
    """Some description"""
    print('Hello, ' + name + '!')

print(print_greet.__name__)    # wrapper
print(print_greet.__doc__)      # None

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

def decorator(foo):
def wrapper(*args):
foo(*args)
wrapper.__name__ = foo.__name__
wrapper.__doc__ = foo.__doc__
return wrapper

@decorator
def print_greet(name):
"""Some description"""
print('Hello, ' + name + '!')

print(print_greet.__name__) # print_greet
print(print_greet.__doc__) # Some description

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

from functools import wraps

def decorator(foo):

    @wraps(foo)
    def wrapper(*args):
        foo(*args)
    return wrapper

@decorator
def print_greet(name):
    """Some description"""
    print('Hello, ' + name + '!')

print(print_greet.__name__)    # print_greet
print(print_greet.__doc__)      # Some description

Однако следует отметить, что декоратор wraps копирует не все атрибуты, а только основные. Подробнее в документацииНесколько популярных декораторов из библиотеки functools

Раздекорирование

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

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

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

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

Воспользуемся одним из предыдущих примеров, переписав его с использованием декоратора wraps

from functools import wraps

def each_pheasant(foo):
@wraps(foo)
def wrapper():
print('каждый')
foo()
print('фазан')
return wrapper

def hunter_is_sitting(foo):
@wraps(foo)
def wrapper():
print('охотник')
foo()
print('сидит')
return wrapper

def wishes_where(foo):
@wraps(foo)
def wrapper():
print('желает')
foo()
print('где')
return wrapper

def know():
print('знать')

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

print(know)         # <function know at 0x7fa16ccab130>

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

know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know) # <function know at 0x7fa16ccab2e0>

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

У большинства объектов в python, за исключением встроенных типов данных, есть атрибут __dict__ содержащий в себе словарь с атрибутами этого объекта. Мы можем получить доступ к этому словарю, обратившись к соответствующему атрибуту.

print(know.__dict__)    # {'__wrapped__': <function know at 0x7fde13b23250>}

В данном примере, атрибут __wrapped__ был создан именно декоратором wraps. При этом другие декораторы могут создавать другие атрибуты. Например, упоминавшийся выше, lru_cache добавляет 7 атрибутов.

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

function_without_first_wrapper = know.__wrapped__

После этого мы можем даже вызвать получившуюся функцию.

function_without_first_wrapper()
# охотник
# желает
# знать
# где
# сидит

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

# получаем еще одну переменную, ссылающуюся на не обернутую функцию
original_know = know
print(know) # <function know at 0x7fa16ccab130>

# оборачиваем функцию
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know) # <function know at 0x7fa16ccab2e0>

# "разворачиваем" функцию, получая ссылку на оборачиваемую функцию
function_without_first_wrapper = know.__wrapped__
function_without_second_wrapper = function_without_first_wrapper.__wrapped__
function_without_third_wrapper = function_without_second_wrapper.__wrapped__

# проверяем, что "развернутая" функция ссылается на тот же объект, что и "оригинальная"
print(function_without_third_wrapper is original_know) # True

Как мы видим в итоге, мы получили переменную function_without_third_wrapper ссылающуюся на тот же объект в памяти, что и original_know Весь код


from functools import wraps

def each_pheasant(foo):
    @wraps(foo)
    def wrapper():
        print('каждый')
        foo()
        print('фазан')
    return wrapper

def hunter_is_sitting(foo):
    @wraps(foo)
    def wrapper():
        print('охотник')
        foo()
        print('сидит')
    return wrapper

def wishes_where(foo):
    @wraps(foo)
    def wrapper():
        print('желает')
        foo()
        print('где')
    return wrapper

def know():
    print('знать')

# получаем еще одну переменную, ссылающуюся на не обернутую функцию
original_know = know
print(know)             # <function know at 0x7fa16ccab130>

# оборачиваем функцию
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know)             # <function know at 0x7fa16ccab2e0>

# "разворачиваем" функцию, получая ссылку на оборачиваемую функцию
function_without_first_wrapper = know.__wrapped__
function_without_second_wrapper = function_without_first_wrapper.__wrapped__
function_without_third_wrapper = function_without_second_wrapper.__wrapped__

# проверяем, что "развернутая" функция ссылается на тот же объект, что и "оригинальная"
print(function_without_third_wrapper is original_know)     # True

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

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

decored_foo = decorator(foo)

Пустая функция

Иногда разработчики оставляют реализацию на потом, и чтобы объявленная функция не генерировала ошибки из-за отсутствия тела, в качестве заглушки используется ключевое слово pass:

def empty():
    pass

Процедуры и функции: различия

Слова «процедура» и «функция» в Python часто обозначают одно и то же — блок кода для выполнения определённой задачи. Однако есть различия.

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

Например, greet из примеров выше ― это процедура, которая принимает имя в качестве аргумента и выводит приветствие на экран. Она не возвращает никакого значения: её цель только выполнить действие.

  • Функция ― тоже фрагмент кода, который выполняет определённую задачу или действие. Но она возвращает результат с помощью ключевого слова return.

В Python нет строгого различия между процедурами и функциями. В коде их определяют одним и тем же ключевым словом def.

Время выполнения функции

Чтобы оценить время выполнения функции, можно поместить её вызов внутрь следующего кода:

from datetime import datetime
import time

start_time = datetime.now()
# здесь вызываем функцию
time.sleep(5)

print(datetime.now() - start_time)

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

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

def outerFunc():
    def firstInner():
        print('This is first inner function')

    def secondInner():
        print('This is second inner function')

    firstInner()
    secondInner()

outerFunc()
> This is first inner function
> This is second inner function

Рекурсия является частным случаем вложенной функции. Это функция, которая вызывает саму себя.

# посчитаем сумму чисел от 1 до num
def sum_from_one(num):
    if num == 1:
        return 1
    return num + sum_from_one(num - 1)


print(sum_from_one(5))
> 15