Перейти к содержанию

432 уроков, 26 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 13 из 20 Начальный 15 мин 100 XP

Функция как значение в Python: передача функций

map и sorted уже принимали функции в аргументах — теперь разберём приём целиком: функция такое же значение, как число или строка.

Редакция Питоники

Мы уже передавали функции, не называя этого: в map и filter отправляли правило lambda, в sorted — параметр key. Пора разобрать приём целиком, потому что на нём держится половина серьёзного Python. Секрет простой: функция в Python — такое же значение, как число или строка. Её можно присвоить переменной, положить в список, передать аргументом — и вернуть из другой функции.

Имя функции — тоже переменная

Начнём с присвоения. Имя функции без скобок — это сама функция; его можно положить в другую переменную:

переменная на функцию
def shout(text):
    return text.upper() + '!'

speak = shout
print(speak('привет'))
print(shout('привет'))
Вывод
ПРИВЕТ!
ПРИВЕТ!

speak = shout не создала новую функцию — обе переменные смотрят на одну и ту же, и вызывать можно через любое имя. Здесь важно поймать разницу: справа стоит имя без скобок. Со скобками shout('привет') — вызов, и в переменную попала бы строка. А печатать саму функцию не стоит:

ошибка: печать функции
# Так печатать бессмысленно
def shout(text):
    return text.upper() + '!'

print(shout)
Вывод выглядит как <function shout at 0x000001A2...>: это не код и не результат, а адрес объекта в памяти, и он меняется от запуска к запуску. О функции судят по вызову: shout('привет') вернёт 'ПРИВЕТ!'.

Функция как аргумент

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

функция в аргументе
def apply(func, value):
    return func(value)

print(apply(abs, -9))
print(apply(len, 'кот'))

def double(n):
    return n * 2
print(apply(double, 21))
Вывод
9
3
42

apply ничего не знает о том, что ей пришлют: в параметр func попадёт функция, и строка func(value) вызовет её с переданным значением. Один вызов посчитал модуль, второй — длину строки, третий — удвоение, и сама apply не менялась. Ровно так устроены map, filter и параметр key у sorted: принимающая функция вызывает ваш колбэк, когда ему пора работать.

Одна функция — разные стратегии

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

две стратегии чистки текста
def clean(text, fixer):
    return fixer(text.strip())

def shout_style(t):
    return t.upper() + '!'
def quiet_style(t):
    return t.lower() + '...'

print(clean('  Привет  ', shout_style))
print(clean('  Привет  ', quiet_style))
Вывод
ПРИВЕТ!
привет...

clean знает один алгоритм — обрезать пробелы методом strip и передать дальше фиксеру. Крикливая стратегия и тихая стратегия дают из одного текста разные результаты, и код clean при этом не изменился ни на символ. Замена поведения подстановкой функции — приём, который встречается везде: правила сортировки, обработчики событий, конфигурации библиотек.

Функции в списке и в словаре

Раз функция — значение, она живёт и в наборах. Список функций перебирают циклом, словарь — берут по ключу:

список функций
def shout_style(t):
    return t.upper() + '!'
def quiet_style(t):
    return t.lower() + '...'

styles = [shout_style, quiet_style]
for style in styles:
    print(style('День'))
Вывод
ДЕНЬ!
день...
словарь операций
ops = {
    'сложить': lambda a, b: a + b,
    'умножить': lambda a, b: a * b,
}
print(ops['сложить'](3, 4))
print(ops['умножить'](3, 4))
Вывод
7
12

Словарь операций — маленькая замена длинной цепочки if/elif: по строковому имени достаётся правило, и оно тут же вызывается. Имена в наборы кладут без скобок — со скобками в списке оказались бы строки-результаты, а не функции.

Конвейер: функции друг за другом

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

обработка конвейером
def strip_spaces(t):
    return t.strip()
def lower_all(t):
    return t.lower()

def process(text, steps):
    for step in steps:
        text = step(text)
    return text

print(process('  ДоБрЫй ДеНь  ', [strip_spaces, lower_all]))
Вывод
добрый день

Каждый шаг конвейера — функция, получившая текст и вернувшая текст. Хотите ещё шаг — пишете новую функцию и дописываете её в список, process не трогается. Так строятся конвейеры обработки данных и промежуточные слои в больших системах.

Функция возвращает функцию

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

фабрика функций
def make_greeting(word):
    def greet(name):
        return f'{word}, {name}!'
    return greet

hello = make_greeting('Привет')
bye = make_greeting('Пока')
print(hello('Анна'))
print(bye('Борис'))
Вывод
Привет, Анна!
Пока, Борис!

Два вызова фабрики дали две разные функции: одна помнит слово «Привет», другая — «Пока». Внутренняя функция greet использует переменную word из внешней — и помнит её даже после того, как фабрика отработала. Как устроена эта память под капотом — тема следующего урока о замыканиях; пока достаточно факта: возвращённая функция живёт со своими данными.

ошибка: скобки при передаче
# Так не работает: функция вызывается слишком рано
def apply(func, value):
    return func(value)

def shout(text):
    return text.upper() + '!'

print(apply(shout(), 'привет'))
Python ответит TypeError: shout() missing 1 required positional argument: 'text'. Скобки запустили shout ещё до вызова apply — передавать нужно имя: apply(shout, 'привет') вернёт 'ПРИВЕТ!'.
МестоКак передаётся функцияУрок
map и filterПервый аргумент: map(rule, значения)урок про map
sorted и min/maxПараметр key: sorted(pairs, key=правило)урок про sorted с key
Свой кодОбычный аргумент: clean(text, fixer)этот урок
Обёртки и декораторыФункция получает функцию и возвращает новуюурок про декораторы

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

callable: проверить, что перед нами

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

проверка вызываемости
def is_even(n):
    return n % 2 == 0

print(callable(is_even))
print(callable(42))
print(callable('текст'))
Вывод
True
False
False

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

Колбэк с разным числом аргументов

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

правило с одним аргументом
def apply_all(items, rule):
    return [rule(item) for item in items]

def add_ten(n):
    return n + 10

print(apply_all([1, 2, 3], add_ten))
Вывод
[11, 12, 13]

Правило должно принимать ровно то, что ему дают. Если правилу нужны дополнительные данные, их удобно закрыть замыканием: lambda n: add_ten(n, 10) — но это уже техника следующего урока. Второй путь — принимать колбэк с *args и передавать ему столько значений, сколько он умеет.

Свой map: пишем, чтобы понять

Мы пользуемся map с прошлого урока, но не писали его сами. Напишем — не чтобы заменить встроенный, а чтобы увидеть механику: колбэк вызывается изнутри цикла, по одному разу на элемент.

map своими руками
def my_map(items, rule):
    result = []
    for item in items:
        result.append(rule(item))
    return result

def to_upper(text):
    return text.upper()

print(my_map(['хлеб', 'сыр'], to_upper))
Вывод
['ХЛЕБ', 'СЫР']

Встроенный map делает ровно то же самое, только внутри интерпретатора и быстрее. Собственный вариант полезен не скоростью, а пониманием: после него map перестаёт быть магией — вы знаете, что внутри цикла стоит вызов колбэка. Такой код мы пишем один раз, чтобы понять, и дальше пользуемся встроенным.

Что написалиЧто получилосьКак правильно
apply(shout, x)Передана сама функциято, что нужно
apply(shout(), x)Вызов без аргумента, TypeErrorapply(shout, x)
[shout_style()]Список из результатов вызовов[shout_style]
print(shout)Служебная запись с адресом памятиprint(shout('привет'))
ops['сложить']Сама функция, а не результатops'сложить'

Пять строк — и пять разных способов ошибиться в одном месте: со скобками и без них. Мы держим в голове простое правило: скобки означают «выполни сейчас». Всё, что нужно передать дальше, пишется без скобок.

Практика: один отчёт, два формата

Соберём вместе: данные приходят списком, а как именно их показать — решает переданная функция.

формат выбирает колбэк
def format_report(title, rows, formatter):
    print(title)
    for row in rows:
        print(formatter(row))

def as_line(row):
    return ' - '.join(row)

def as_csv(row):
    return ','.join(row)

data = [['чай', '120'], ['кофе', '350']]

format_report('Прайс', data, as_line)
format_report('Прайс', data, as_csv)
Вывод
Прайс
чай - 120
кофе - 350
Прайс
чай,120
кофе,350

Один и тот же отчёт — два формата: текст для человека и строка для выгрузки. Код печати не изменился, изменилось только то, что мы передали. Ровно этот приём лежит в основе шаблонов отчётов, сортировки по правилу и любых настроек поведения.

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

Сначала предскажи ответ в голове — это главный навык программиста.

def apply(func, value):
    return func(value)

print(apply(str.upper, 'чай'))
def double(n):
    return n * 2

f = double
print(f(5))
ops = {'минус': lambda a, b: a - b}
print(ops['минус'](10, 4))
Проверь себя
0 / 9

1. Что такое имя функции без скобок, например shout?

2. Что вернёт apply(len, 'ок') у функции def apply(func, value): return func(value)?

3. Какая функция называется функцией высшего порядка?

4. Что попадёт в переменную при записи speak = shout()?

5. Что лежит в списке styles = [shout_style, quiet_style]?

6. Почему hello из make_greeting('Привет') печатает именно Привет, Анна?

7. Что вернёт callable(42)?

8. Что вернёт apply(len, [1, 2, 3]) у функции def apply(func, value): return func(value)?

9. Почему список styles = [shout_style()] содержит не функции?

Карточки терминов
Запомнено: 0 / 8
Практика

Напиши функцию apply_twice(func, value), которая применяет func к value два раза подряд, и напечатай результат для функции double(n): n * 2 и числа 3.

practice.py
Вопросы и ответы по уроку

Как передать функцию в функцию в Python?

Именем без скобок: apply(shout, 'привет') передаёт саму функцию, а принимающая сторона вызовет её через параметр: func(value). Запись со скобками apply(shout(), 'привет') вызовет функцию немедленно и передаст результат — классическая ловушка начинающих.

Урок 7: map — функция для каждого элемента

Что такое функции высшего порядка?

Функции, которые принимают или возвращают другие функции: map и filter применяют переданное правило к элементам, sorted с параметром key сортирует по правилу, а фабрика вроде make_greeting возвращает новую функцию. На этом приёме построены декораторы Python.

Урок 16: декораторы — обёртка вокруг функции

Что такое колбэк?

Функция-стратегия, которую вы передаёте, а вызывает её чужой код, когда пора: clean(text, fixer) вызывает fixer внутри себя. Колбэки позволяют менять поведение без переписывания принимающей функции — тот же принцип, что у key в sorted и правила в map.

Можно ли хранить функции в списке или словаре?

Да: функции — значения. Список styles = [shout_style, quiet_style] перебирают циклом и вызывают каждый элемент; словарь ops = {'сложить': lambda a, b: a + b} даёт выбор правила по строковому ключу. Главное — класть имена без скобок, чтобы в наборе оказались функции, а не результаты вызовов.

Как проверить, что в переменной лежит именно функция?

Встроенной функцией callable: callable(is_even) даёт True, callable(42) — False. Проверять стоит там, где код получает функцию извне — из конфига или аргументов командной строки — и должен понять, вызывать её или ругаться.

Что будет, если правило-колбэк принимает два аргумента, а вызов даёт один?

Произойдёт TypeError: не хватает аргумента. Решается это тремя способами: сделать правило одноаргументным, закрыть лишние данные замыканием или принять колбэк с *args и передавать ему столько значений, сколько он умеет.

Чем функция-значение отличается от результата её вызова?

Тем, что одно — объект, а другое — данные. Имя функции без скобок даёт сам объект, его можно хранить и передавать; вызов со скобками даёт результат, который можно только использовать как данные. Один символ — и совершенно разные вещи.

Зачем хранить функции в словаре, если можно написать if/elif?

Словарь даёт выбор правила по строковому имени: добавить новый вариант — одна строка в словаре, а не новая ветка в цепочке условий. Длинные цепочки if/elif с каждым новым случаем растут, а словарь остаётся плоским и читается как таблица.

Чем колбэк отличается от обычного аргумента?

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

Понравился урок? Сошлитесь на него

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

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.