Главная / Курсы / Python / Вложенные функции, лямбды, вариативные функции

Глава 7. Вложенные функции, лямбды, вариативные функции

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

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

В питоне функции могут быть вложенными (inner, nested), то есть объявленными внутри другой функции:

def outer_f():
def inner_f():
print("This is inner function")
inner_f()

outer_f()
inner_f()

В данном примере вызов outer_f() завершится успешно, а вызов inner_f() строчкой ниже приведет к исключению:

NameError: name 'inner_f' is not defined

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

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

Рассмотрим функцию has_permissions(), которая принимает путь к директории. Внутри нее есть вложенная функция get_permissions_str(), которая принимает имя пользователя и проверяет, имеет ли пользователь доступ к директории.

В этом примере используются строки вида f"text {val} text": так называемые f-строки, о которых вы узнаете в главе 10. Это литералы форматированных строк: перед строкой идет символ f, а в саму строку в фигурные скобки можо вставлять любые значения.

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" параметра внешней функции.

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

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.

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

Напишите функцию calc_gcd(a, b), которая находит наибольший общий делитель (GCD, greatest common divisor) чисел a и b. Функция должна возвращать два значения: GCD и количество вызовов, которые были выполнены перед возвратом ответа.

Например, calc_gcd(25, 15) вернет 5 и 4, а calc_gcd(8, 3) вернет 1 и 5.

Для подсчета количества вызовов заведите вложенную рекурсивную функцию и вызывайте ее. В своем решении реализуйте алгоритм Евклида. Он заключается в следующем:

  • GCD равен a, если a и b совпадают.
  • GCD равен GCD от a - b и b, если a больше b.
  • GCD равен GCD от a и b - a, если a меньше b.

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

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)
Задача # 1

Кстати, автор питона Гвидо ван Россум в своей известной презентации «Introduction to Python» предложил вот такой лаконичный вариант поиска наибольшего общего делителя:

def gcd(a, b):
"greatest common divisor"
while a != 0:
a, b = b % a, a # parallel assignment
return b

В примере от Гвидо применяется мощный инструмент питона: parallel assignment.

a = x
b = y

Это прием, позволяющий запись выше сократить до одной строки:

a, b = x, y

Parallel assignment особенно популярен, когда требуется выполнить swap — поменять значения двух переменных:

x, y = y, x

Лямбда-функции

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

Если для объявления обычной именованной функции используется ключевое слово def, то для объявления лямбды — ключевое слово lambda:

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

Тело лямбда-функции должно состоять из единственного выражения. В нем не может быть никаких операторов (assert, pass, raise, += и прочих). Наличие оператора внутри лямбда-функции приведет к исключению типа SyntaxError.

Пример объявления и вызова лямбда-функции, которая повторяет строку s n раз:

mult_str = lambda s, n: s * n
print(mult_str("*", 5))
*****

Здесь функциональному объекту mult_str присваивается безымянная лямбда-функция. На следующей строке функциональный объект вызывается. Эта лямбда-функция эквивалентна обычной функции, просто позволяет сэкономить немного места:

def mult_str(s, n):
return s * n

Ключевые слова, такие как return и pass, в лямбде использовать нельзя. Список параметров лямбды теоретически может быть и пустым:

x = lambda: 5

print(x())
5

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

(lambda s, n: print(s * n))("I", 3)

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

III

Напишите лямбда-функцию, которая принимает два числа a и b, возвращает их сумму и разность. Присвойте ее функциональному объекту calc.

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


Синтаксис: <variable> = lambda <arg1, arg2, ...>: (ret_val1, ret_val2, ...).

calc = lambda a, b: (a + b, a - b)
Задача # 2


Напишите лямбда-функцию, которая просто выводит в консоль "Press any key to continue". Присвойте ее функциональному объекту to_next_step.

Вызовите этот объект.


Синтаксис: <variable> = lambda: <function_call()>.

to_next_step = lambda : print("Press any key to continue") to_next_step()
Задача # 3

Теперь вы узнаете лямбду, если увидите ее в чужом коде! Но стоит ли лямбдами злоупотреблять? В PEP8 сказано следующее: всегда используйте определение функций через def вместо того, чтобы присваивать лямбду функциональному объекту. PEP8 настаивает на тотальном избегании лямбд, такие дела.

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

f = lambda x: 2 * x
def f(x): return 2 * x

С чем это связано? Да, лямбды действительно позволяют сэкономить немного места. Но взамен лишают вас понятного стека вызовов, усложняют процесс отладки. Потому что в стек вызовов вместо имени конкретной функции попадает весьма расплывчатое <lambda>. Так что выбирая между компактностью и понятностью, отдавайте предпочтение понятности.

Что выведет этот код?

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)

Функция f принимает на вход список. Внутри нее создается функциональный объект predicate, которому присваивается лямбда. Лямбда возвращает True, если переданный ей аргумент больше нуля. Иначе она возвращает False. Затем в цикле к каждому элементу списка items применяется этот предикат. И если элемент оказывается больше нуля, мы возвращаем его из функции f. Если такой элемент не найден, функция возвращает ноль.

10
Задача # 4

Вариативные функции

Вариативные функции (variadic functions) — это функции с переменным числом аргументов. Они реализованы в питоне и способны принимать произвольное количество позиционных и именованных аргументов:

def f(*args, **kwargs):
...

f(1, 2, 3, k1="A", k2="B")

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

*args — это список всех переданных в функцию позиционных аргументов (в нашем случае это 1, 2, 3). А **kwargs (от слова keyworded) хранит все именованные аргументы (k1, k2). Распаковку и упаковку коллекций мы обсудим в одной из следующих глав.

**kwargs является словарем — коллекцией, хранящей ключи и значения. Более подробно словари будут рассмотрены в одной из следующих глав. А пока кратко приведем варианты обхода словаря, содержащего именованные аргументы kwargs:

for key in kwargs:
print(key)
for key, value in kwargs.items():
print(key, value)

Имплементируйте функцию f(), которая принимает вариативные позиционные и именованные аргументы.

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

Например, при вызове f(1, 2, k1="a", k2="b") в консоль должно быть выведено 4 значения, каждое c новой строки: "1", "2", "k1", "k2".


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

def f(*args, **kwargs): for a in args: print(a) for k in kwargs: print(k)
Задача # 5

Функции как объекты первого класса

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

Что выведет этот код?

def calc(x, y):
return x * y - y

def f(a, b, action):
return action(a, b)

g = f
print(g(2, 5, calc))

Функция f принимает три аргумента, последний из которых — функция. Объекту g присваивается функция f. Затем этот объект, являющийся функцией, вызывается с аргументами 2, 5 и calc. В консоль выводится значение выражения 2 * 5 - 5, то есть 5.

5
Задача # 6

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

Резюмируем

  • Функции в питоне могут быть вложенными. Таким образом достигается инкапсуляция кода.
  • Вложенные функции имеют доступ к аргументам и переменным своей внешней функции.
  • Лямбда-функции — это функции-однострочники вида lambda параметры: выражение.
  • PEP8 не рекомендует использовать лямбды, потому что они запутывают стек вызовов: вместо явного имени функции в него попадает абстрактная <lambda>.
  • Функции в питоне — это объекты первого класса.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!