# Глава 7. Вложенные функции, лямбды, вариативные функции
Копнем тему функций глубже! Обсудим, для чего нужны вложенные функции, стоит ли злоупотреблять лямбдами и как выглядят вариадики — функции с переменным числом аргументов.
## Вложенные функции
В питоне функция может быть вложенной (inner, nested), то есть объявленной внутри другой функции. Это позволяет создавать функции, которые используются только внутри определенного контекста и не доступны извне. Вложенная функция имеет доступ к переменным и параметрам внешней функции, но сама по себе недоступна за пределами внешней функции.
Давайте рассмотрим простой пример:
```python {.example_for_playground}
def outer_f():
def inner_f():
print("This is inner function")
print("This is outer function")
inner_f()
outer_f()
inner_f()
```
В данном примере вызов `outer_f()` завершится успешно, а вызов `inner_f()` строчкой ниже приведет к исключению:
```
This is outer function
This is inner function
NameError: name 'inner_f' is not defined
```
Пример показывает, что вложенные функции скрыты от доступа откуда-либо, кроме функции-обертки. Таким образом достигается инкапсуляция вспомогательного кода: он исключается из внешней области видимости.
Заведем функцию `create_adder`, которая принимает число и возвращает функцию, которая складывает свой аргумент с этим числом:
```python {.example_for_playground}
def create_adder(x):
def adder(y):
return x + y
return adder
add_5 = create_adder(5)
result = add_5(3)
print(result) # Выведет: 8
```
Здесь вложенная функция `adder` имеет доступ к переменной `x` из внешней функции `create_adder` даже после того, как внешняя функция завершила свое выполнение.
Напишем еще один пример, демонстрирующий, как вложенная функция «запоминает» или «захватывает» переменные из внешней функции. Такое поведение называется **замыканием.** Об этом мы поговорим подробнее чуть позже.
```python {.example_for_playground}
def create_validator(threshold):
def validate(value):
return value >= threshold
return validate
age_checker = create_validator(18)
score_checker = create_validator(60)
print(age_checker(25)) # Выведет: True
print(age_checker(15)) # Выведет: False
print(score_checker(70)) # Выведет: True
print(score_checker(40)) # Выведет: False
```
Итак, вложенные функции нельзя вызывать извне. Но сами они имеют доступ к аргументам и локальным переменным функции-обертки.
Теперь рассмотрим более сложный пример с функцией `has_permissions()`, которая принимает путь к директории. Внутри нее есть вложенная функция `get_permissions_str()`, которая принимает имя пользователя и проверяет, имеет ли пользователь доступ к директории.
В этом примере используются строки вида `f"text {val} text"`: так называемые f-строки, о которых вы узнаете [в главе 10.](/courses/python/chapters/python_chapter_0100#block-formatting) Это литералы форматированных строк: перед строкой идет символ `f`, а в саму строку в фигурные скобки можно вставлять любые значения.
```python {.example_for_playground}
def has_permissions(directory):
def get_permissions_str(user):
if user == "root":
return f"Permission to {directory} granted for {user}"
# Maybe add additional checks here
return f"Permission to {directory} declined for {user}"
return get_permissions_str
has_permissions_tmp = has_permissions("/tmp")
print(has_permissions_tmp("sandbox_user"))
has_permissions_logs = has_permissions("/var/logs")
print(has_permissions_logs("root"))
```
```
Permission to /tmp declined for sandbox_user
Permission to /var/logs granted for root
```
В этом примере вложенная функция `get_permissions_str()` работает с параметрами `directory` и `user`. Мы видим, что она имеет доступ к параметрам и переменным внешней функции.
Функция `has_permissions()` возвращает свою внутреннюю функцию. Ее можно присвоить переменной, чтобы затем пользоваться переменной как вызываемым объектом.
Это и происходит в последних 4-х строках кода примера: мы вызвали функцию `has_permissions()` с аргументом `"/tmp"`. Она вернула свою внутреннюю функцию, и мы присвоили ее переменной `has_permissions_tmp`. Теперь эту переменную можно использовать как функцию: в ней хранится вложенная функция `get_permissions_str()`. Поэтому если вызвать `has_permissions_tmp` с аргументом `"sandbox_user"`, то отработает код вложенной функции, в котором запомнено значение `"/tmp"` параметра внешней функции.
### Что такое замыкание (closure)?
Теперь, когда мы рассмотрели вложенные функции, давайте разберемся, что такое **замыкание** (closure). Замыкание — это вложенная функция, которая «запоминает» переменные из своей внешней области видимости, даже после того как внешняя функция завершила свое выполнение.
Проще говоря, вложенная функция имеет доступ к переменным внешней функции, и этот доступ сохраняется даже тогда, когда внешняя функция уже закончила работу.
Рассмотрим простой пример:
```python {.example_for_playground}
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!" # greet "помнит" значение greeting из внешней функции
return greet
say_hello = make_greeter("Привет")
say_hi = make_greeter("Хай")
print(say_hello("Анна")) # Выведет: Привет, Анна!
print(say_hi("Борис")) # Выведет: Хай, Борис!
```
В этом примере:
- `greet` — это замыкание, потому что «помнит» значение `greeting` из внешней функции `make_greeter`:
- Когда мы вызываем `make_greeter("Привет")`, создается замыкание, которое будет использовать `"Привет"` как приветствие.
- Когда мы вызываем `make_greeter("Хай")`, создается другое замыкание, которое будет использовать `"Хай"` как приветствие.
А в этом примере каждое замыкание «помнит» свое собственное значение `count`:
```python {.example_for_playground}
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = create_counter()
counter2 = create_counter()
print(counter1()) # Выведет: 1
print(counter1()) # Выведет: 2
print(counter2()) # Выведет: 1 (независимый счетчик)
print(counter1()) # Выведет: 3
```
Знание о вложенных функциях нам пригодится, когда в [главе про декораторы](/courses/python/chapters/python_chapter_0270/) мы будем обсуждать **замыкания** (closures) — вложенные функции, которые ссылаются на переменные, объявленные в теле внешней функции. Рассмотрим это на небольшом примере:
```python {.example_for_playground}
def f_outer(initial_val):
items = [initial_val]
def f_inner(val):
items.append(val)
print("Items:", items)
return f_inner
f = f_outer(1)
f(2)
f(3)
```
```
Items: [1, 2]
Items: [1, 2, 3]
```
Здесь вложенная функция `f_inner()` замыкает список `items`, объявленный в области видимости внешней функции `f_outer()`. Поэтому она может добавлять в этот список новые элементы с помощью метода `append()`. На строке `f = f_outer(1)` создается вызываемый объект `f`, которому присваивается возвращаемое из функции `f_outer()` замыкание `f_inner()`. Оно впоследствии и вызывается для значений 2 и 3.
Так как вложенная функция «видит» аргументы и переменные внешней функции, то будьте внимательны к именованию переменных во вложенной функции. В коде вложенной функции они могут затенить переменные из внешней области видимости. Более подробно про это мы расскажем в главе [«Области видимости».](/courses/python/chapters/python_chapter_0220/)
Прежде чем переходить к более сложной задаче, давайте потренируемся на нескольких простых упражнениях.
Напишите функцию `make_power(n)`, которая принимает число `n` и возвращает вложенную функцию, возводящую любое переданное число в степень `n`.
```python {.task_source #python_chapter_0070_task_0080}
```
Используйте вложенную функцию, которая принимает один параметр и возводит его в степень `n`. {.task_hint}
```python {.task_answer}
def make_power(n):
def power(x):
return x ** n
return power
```
Напишите функцию `make_string_prefixer(prefix)`, которая принимает строку `prefix` и возвращает вложенную функцию, добавляющую этот префикс к любой переданной строке.
```python {.task_source #python_chapter_0070_task_0070}
```
Используйте вложенную функцию, которая принимает одну строку и возвращает объединение префикса с этой строкой. {.task_hint}
```python {.task_answer}
def make_string_prefixer(prefix):
def prefixer(text):
return prefix + text
return prefixer
```
Теперь перейдем к более сложной задаче:
Напишите функцию `calc_gcd(a, b)`, которая находит наибольший общий делитель (GCD, greatest common divisor) чисел `a` и `b`. Функция должна возвращать два значения: GCD и количество вызовов, которые были выполнены перед возвратом ответа. {.task_text}
Например, `calc_gcd(25, 15)` вернет 5 и 4, а `calc_gcd(8, 3)` вернет 1 и 5. {.task_text}
Для подсчета количества вызовов заведите вложенную рекурсивную функцию и вызывайте ее. В своем решении реализуйте алгоритм Евклида. Он заключается в следующем: {.task_text}
- GCD равен `a`, если `a` и `b` совпадают.
- GCD равен GCD от `a - b` и `b`, если `a` больше `b`.
- GCD равен GCD от `a` и `b - a`, если `a` меньше `b`.
```python {.task_source #python_chapter_0070_task_0010}
```
Внутри функции `calc_gcd()` заведите вложенную функцию. Она должна принимать на вход значения `a` и `b`, а также третий параметр: счетчик вызовов. Вложенная функция должна инкрементировать его и возвращать в качестве одного из значений в `return`. {.task_hint}
```python {.task_answer}
def calc_gcd(a, b):
calls = 0
def calc_gcd_inner(a, b, calls):
calls += 1
if a == b:
return a, calls
if a > b:
return calc_gcd_inner(a - b, b, calls)
return calc_gcd_inner(a, b - a, calls)
return calc_gcd_inner(a, b, calls)
```
Кстати, автор питона Гвидо ван Россум в своей известной [презентации «Introduction to Python»](https://people.csail.mit.edu/rudolph/Teaching/Lectures/guido-intro-1.pdf) предложил вот такой лаконичный вариант поиска наибольшего общего делителя:
```python {.example_for_playground}
def gcd(a, b):
"greatest common divisor"
while a != 0:
a, b = b % a, a # parallel assignment
return b
```
В примере от Гвидо применяется мощный инструмент питона: parallel assignment.
```python
a = x
b = y
```
Это прием, позволяющий запись выше сократить до одной строки:
```python
a, b = x, y
```
Parallel assignment особенно популярен, когда требуется выполнить swap — поменять значения двух переменных:
```python
x, y = y, x
```
## Лямбда-функции
### Что такое лямбда-функции?
Лямбда-функции (их также называют анонимными, безымянными) — это компактные функции-однострочники. Они могут иметь много параметров, но содержат только одно вычисляемое и возвращаемое выражение. Для возврата значения из лямбды `return` не используется.
Если для объявления обычной именованной функции используется ключевое слово `def`, то для объявления лямбды — ключевое слово `lambda`:
```python
lambda параметры: выражение
```
Тело лямбда-функции должно состоять из единственного выражения. В нем не может быть никаких операторов (`assert`, `pass`, `raise`, `+=` и прочих). Наличие оператора внутри лямбда-функции приведет к исключению типа `SyntaxError`.
### Примеры лямбда-функций
Давайте начнем с самых простых примеров:
```python {.example_for_playground}
# Лямбда-функция, которая удваивает число
double = lambda x: x * 2
print(double(5)) # Выведет: 10
```
```python {.example_for_playground}
# Лямбда-функция, которая складывает два числа
add = lambda x, y: x + y
print(add(3, 4)) # Выведет: 7
```
```python {.example_for_playground}
# Лямбда-функция без параметров
greet = lambda: "Привет, мир!"
print(greet()) # Выведет: Привет, мир!
```
Пример объявления и вызова лямбда-функции, которая повторяет `n` раз строку `s`:
```python {.example_for_playground}
mult_str = lambda s, n: s * n
print(mult_str("*", 5))
```
```
*****
```
Здесь функциональному объекту `mult_str` присваивается безымянная лямбда-функция. На следующей строке функциональный объект вызывается. Эта лямбда-функция эквивалентна обычной функции, просто позволяет сэкономить немного места:
```python
def mult_str(s, n):
return s * n
```
Ключевые слова, такие как `return` и `pass`, в лямбде использовать **нельзя.** Список параметров лямбды теоретически может быть и пустым:
```python {.example_for_playground}
x = lambda: 5
print(x())
```
```
5
```
Вместо присваивания лямбды функциональному объекту ее можно сразу вызвать.
```python {.example_for_playground}
(lambda s, n: print(s * n))("I", 3)
```
Код выглядит странно, но выводит в консоль ожидаемый результат:
```
III
```
Напишите лямбда-функцию, которая принимает два числа `a` и `b`, возвращает их сумму и разность. Присвойте ее функциональному объекту `calc`. {.task_text}
Чтобы лямбда корректно вернула пару значений, явным образом положите их в кортеж: оберните возвращаемые значения в круглые скобки, необходимые для конструирования кортежа. {.task_text}
```python {.task_source #python_chapter_0070_task_0020}
```
Синтаксис: `<variable> = lambda <arg1, arg2, ...>: (ret_val1, ret_val2, ...)`. {.task_hint}
```python {.task_answer}
calc = lambda a, b: (a + b, a - b)
```
Напишите лямбда-функцию, которая просто выводит в консоль `"Press any key to continue"`. Присвойте ее функциональному объекту `to_next_step`. {.task_text}
Вызовите этот объект. {.task_text}
```python {.task_source #python_chapter_0070_task_0030}
```
Синтаксис: `<variable> = lambda: <function_call()>`.{.task_hint}
```python {.task_answer}
to_next_step = lambda : print("Press any key to continue")
to_next_step()
```
Теперь вы узнаете лямбду, если увидите ее в чужом коде! Но стоит ли лямбдами злоупотреблять? В PEP8 [сказано следующее:](https://peps.python.org/pep-0008/#programming-recommendations) всегда используйте определение функций через `def` вместо того, чтобы присваивать лямбду функциональному объекту. PEP8 настаивает на тотальном избегании лямбд, такие дела.
Например, вот эту лямбду PEP8 считает нужным превратить в полноценную функцию:
```python
f = lambda x: 2 * x
```
```python
def f(x): return 2 * x
```
С чем это связано? Да, лямбды действительно позволяют сэкономить немного места. Но взамен лишают вас понятного стека вызовов, усложняют процесс отладки. Потому что в стек вызовов вместо имени конкретной функции попадает весьма расплывчатое `<lambda>`. Так что выбирая между компактностью и понятностью, отдавайте предпочтение понятности.
Что выведет этот код? {.task_text}
```python {.example_for_playground}
def f(items):
predicate = lambda x: x > 0
for item in items:
if predicate(item):
return item
return 0
x = f([-10, 0, 10, 20])
print(x)
```
```consoleoutput {.task_source #python_chapter_0070_task_0040}
```
Функция `f` принимает на вход список. Внутри нее создается функциональный объект `predicate`, которому присваивается лямбда. Лямбда возвращает `True`, если переданный ей аргумент больше нуля. Иначе она возвращает `False`. Затем в цикле к каждому элементу списка `items` применяется этот предикат. И если элемент оказывается больше нуля, мы возвращаем его из функции `f`. Если такой элемент не найден, функция возвращает ноль. {.task_hint}
```python {.task_answer}
10
```
## Вариативные функции {#block-variadic}
### Что такое вариативные функции?
[Вариативные функции](/courses/python/chapters/python_chapter_0260) (variadic functions) — это функции с переменным числом аргументов. Они реализованы в питоне и способны принимать произвольное количество позиционных и именованных аргументов.
Давайте разберем, что означают `*args` и `**kwargs`:
- `*args` — собирает все позиционные аргументы в кортеж
- `**kwargs` — собирает все именованные аргументы в словарь
### Простые примеры использования
Рассмотрим простые примеры, чтобы лучше понять, как работают `*args` и `**kwargs`:
```python {.example_for_playground}
def show_args(*args):
print(f"Количество аргументов: {len(args)}")
print(f"Аргументы: {args}")
print(f"Тип args: {type(args)}")
show_args(1, 2, 3, "hello")
```
```
Количество аргументов: 4
Аргументы: (1, 2, 3, 'hello')
Тип args: <class 'tuple'>
```
```python {.example_for_playground}
def show_kwargs(**kwargs):
print(f"Количество именованных аргументов: {len(kwargs)}")
print(f"Аргументы: {kwargs}")
print(f"Тип kwargs: {type(kwargs)}")
show_kwargs(name="Alice", age=30, city="Moscow")
```
```
Количество именованных аргументов: 3
Аргументы: {'name': 'Alice', 'age': 30, 'city': 'Moscow'}
Тип kwargs: <class 'dict'>
```
```python {.example_for_playground}
def show_all(*args, **kwargs):
print(f"Позиционные аргументы: {args}")
print(f"Именованные аргументы: {kwargs}")
show_all(1, 2, 3, name="Bob", age=25)
```
```
Позиционные аргументы: (1, 2, 3)
Именованные аргументы: {'name': 'Bob', 'age': 25}
```
Теперь посмотрим на полный пример:
```python
def f(*args, **kwargs):
...
f(1, 2, 3, k1="A", k2="B")
```
`args` и `kwargs` — популярные имена для вариативных позиционных и именованных аргументов. Символы звездочек `*` и `**` перед аргументами означают, что внутри переменной содержится некоторая коллекция, которую можно распаковать.
`*args` — это кортеж всех переданных в функцию позиционных аргументов (в нашем случае это 1, 2, 3). А `**kwargs` (от слова keyworded) хранит все именованные аргументы (`k1`, `k2`) в виде словаря. Распаковку и упаковку коллекций мы обсудим в [одной из следующих глав.](/courses/python/chapters/python_chapter_0250/)
`**kwargs` является словарем — коллекцией, хранящей ключи и значения. Более подробно словари будут рассмотрены в одной из следующих глав. А пока кратко приведем варианты обхода словаря, содержащего именованные аргументы `kwargs`:
```python
for key in kwargs:
print(key)
```
```python
for key, value in kwargs.items():
print(key, value)
```
Имплементируйте функцию `f()`, которая принимает вариативные позиционные и именованные аргументы. {.task_text}
В теле функции проитерируйтесь по позиционным и именованным аргументам, чтобы вывести их по одному в консоль. Для именованных аргументов нужно выводить их ключи. {.task_text}
Например, при вызове `f(1, 2, k1="a", k2="b")` в консоль должно быть выведено 4 значения, каждое c новой строки: "1", "2", "k1", "k2". {.task_text}
```python {.task_source #python_chapter_0070_task_0050}
```
Внутри функции `f()` сначала проитерируйтесь циклом `for` по позиционным аргументам для вывода их по одному в консоль, затем — по именованным. {.task_hint}
```python {.task_answer}
def f(*args, **kwargs):
for a in args:
print(a)
for k in kwargs:
print(k)
```
## Функции как объекты первого класса
### Что такое объекты первого класса?
Когда говорят, что в Python функции являются **объектами первого класса**, это означает, что функции ведут себя как обычные данные: их можно присваивать переменным, передавать в качестве аргументов другим функциям, возвращать из функций как результат и хранить в списках, словарях и других коллекциях.
В Python всё является объектом, поскольку все типы данных наследуются от базового класса `object`. Это включает и функции, и числа, и строки, и классы. Благодаря этому функции могут использоваться как любые другие объекты. Подробнее о наследовании и объектной модели Python вы узнаете в следующих [главах](https://senjun.ru/courses/python/chapters/python_chapter_0160/).
Это важное понятие в программировании, особенно в функциональном стиле. Давайте рассмотрим несколько примеров:
```python {.example_for_playground}
def greet(name):
return f"Привет, {name}!"
# Присваивание функции переменной
hello_func = greet
print(hello_func("Мир")) # Выведет: Привет, Мир!
```
```python {.example_for_playground}
def square(x):
return x * x
def cube(x):
return x * x * x
# Хранение функций в списке
functions = [square, cube]
for func in functions:
print(func(3)) # Выведет: 9 и 27
```
```python {.example_for_playground}
def apply_operation(value, operation):
"""Принимает значение и функцию, применяет функцию к значению"""
return operation(value)
def double(x):
return x * 2
result = apply_operation(5, double)
print(result) # Выведет: 10
```
Что выведет этот код? {.task_text}
```python {.example_for_playground}
def calc(x, y):
return x * y - y
def f(a, b, action):
return action(a, b)
g = f
print(g(2, 5, calc))
```
```consoleoutput {.task_source #python_chapter_0070_task_0060}
```
Функция `f` принимает три аргумента, последний из которых — функция. Объекту `g` присваивается функция `f`. Затем этот объект, являющийся функцией, вызывается с аргументами 2, 5 и `calc`. В консоль выводится значение выражения `2 * 5 - 5`, то есть 5.{.task_hint}
```python {.task_answer}
5
```
Напишите функцию `create_calculator()`, которая возвращает вложенную функцию `calculate(operation, a, b)`. {.task_text}
Внутри `create_calculator()` объявите вложенные функции для выполнения операций: `add(x, y)`, `subtract(x, y)`, `multiply(x, y)`, `divide(x, y)`. Если операция неизвестна, то вместо вложенной функции верните строку `"Unknown operation"`. {.task_text}
Функция `calculate()` должна принимать три аргумента: {.task_text}
- `operation` — строка с названием операции (`"add"`, `"subtract"`, `"multiply"`, `"divide"`);
- `a` и `b` — числа, над которыми нужно выполнить операцию.
```python {.task_source #python_chapter_0070_task_0090}
```
Объявите вложенные функции для каждой операции. В `calculate()` используйте `match/case` для вызова нужной функции. {.task_hint}
```python {.task_answer}
def create_calculator():
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
def calculate(operation, a, b):
match operation:
case "add":
return add(a, b)
case "subtract":
return subtract(a, b)
case "multiply":
return multiply(a, b)
case "divide":
return divide(a, b)
case _:
return "Unknown operation"
return calculate
calc = create_calculator()
print(calc("add", 5, 3)) # 8
print(calc("multiply", 4, 6)) # 24
print(calc("divide", 10, 2)) # 5.0
print(calc("subtract", 10, 4)) # 6
print(calc("mod", 5, 2)) # Unknown operation
```
Понимание того факта, что функции являются объектами первого класса, пригодится в главах про [декораторы](/courses/python/chapters/python_chapter_0270/) и [функции высших порядков.](/courses/python/chapters/python_chapter_0280/)
## Резюмируем
- Функции в питоне могут быть вложенными. Таким образом достигается инкапсуляция кода.
- Вложенные функции имеют доступ к аргументам и переменным своей внешней функции.
- Лямбда-функции — это функции-однострочники вида `lambda параметры: выражение`.
- PEP8 не рекомендует использовать лямбды, потому что они запутывают стек вызовов: вместо явного имени функции в него попадает абстрактная `<lambda>`.
- Функции в питоне — это объекты первого класса.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!