4.2. Позиционные и именованные аргументы. Функции высших порядков. Лямбда-функции

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

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

Напишем функцию, которая для стоимости товара считает окончательную стоимость с учётом скидки в процентах:

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


print(final_price(1000, 5))

Вывод программы:

950.0

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

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


print(final_price(1000, 5))
# Значение скидки не задано, используется значение по умолчанию
print(final_price(1000))

Вывод программы:

950.0
990.0

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

def add_value(x, list_arg=[]):
    list_arg += [x]
    return list_arg


print(add_value(0))
print(add_value(0, [1, 2, 3]))
print(add_value(1))

Вывод программы:

[0]
[1, 2, 3, 0]
[0, 1]

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

def add_value(x, list_arg=None):
    if list_arg is None:
        list_arg = []
    list_arg += [x]
    return list_arg


print(add_value(0))
print(add_value(0, [1, 2, 3]))
print(add_value(1))

Вывод программы:

[0]
[1, 2, 3, 0]
[1]

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

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

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

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


print(final_price(1000, discount=5))
print(final_price(discount=10, price=1000))

Вывод программы:

950.0
900.0

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

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

Рассмотрим первый случай: функция принимает любое количество позиционных аргументов. Такой функцией, например, является стандартная функция print(), так как она может принимать любое количество выводимых строк-аргументов. Чтобы указать, что функция может принимать неограниченное количество позиционных аргументов, нужно при её объявлении поставить аргумент с *. К примеру, *args. В функции этот аргумент будет кортежем, содержащим переданные значения позиционных аргументов.

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

def final_price(*prices, discount=1):
    return [price - price * discount / 100 for price in prices]


print(final_price(100, 200, 300, discount=5))

Вывод программы:

[95.0, 190.0, 285.0]

Чтобы функция могла принимать неограниченное количество именованных аргументов, нужно при её объявлении поставить аргумент с **. Например, **kwargs (сокращение от keyword arguments). В функции этот аргумент будет словарём, ключи которого будут строками, содержащими имена передаваемых аргументов, а значения по ключам будут соответствовать значениям передаваемых аргументов.

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

def final_price(*prices, discount=1, **kwargs):
    low = kwargs.get("price_low", min(prices))
    high = kwargs.get("price_high", max(prices))
    return [price - price * discount / 100 for price in prices if low <= price <= high]


print(final_price(100, 200, 300, 400, 500, discount=5))
print(final_price(100, 200, 300, 400, 500, discount=5, price_low=200))
print(final_price(100, 200, 300, 400, 500, discount=5, price_high=200))
print(final_price(100, 200, 300, 400, 500, discount=5, price_low=200, price_high=350))

Вывод программы:

[95.0, 190.0, 285.0, 380.0, 475.0]
[190.0, 285.0, 380.0, 475.0]
[95.0, 190.0]
[190.0, 285.0]

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

Рассмотрим пару полезных встроенных функций высшего порядка. Функция filter() позволяет отобрать элементы из итерируемого объекта по некоторому критерию. Результат работы функции — итератор. Критерий отбора — функция, возвращающая значения логического типа. Функция filter() последовательно проверяет значение функции-критерия для каждого элемента, и при значении True элемент попадает в выходной итератор. Напишем программу, которая выберет из списка целых чисел только положительные:

def only_pos(x):
    return x > 0


result = filter(only_pos, [-1, 5, 6, -10, 0])
print(", ".join(str(x) for x in result))

Вывод программы:

5, 6

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

Напишем программу, которая из всех символов строки выберет буквы. В примере мы используем метод isalpha() для типа данных str.

result = filter(str.isalpha, "123ABcd()")
print("".join(result))

Вывод программы:

ABcd

Ещё одной полезной функцией высшего порядка в Python является map(). Она возвращает итератор, каждый элемент которого получен применением функции-обработчика к итерируемому объекту.
Напишем программу, которая для списка целых чисел выведет список квадратов этих чисел:

def square(x):
    return x ** 2

    
result = map(square, range(5))
print(", ".join(str(x) for x in result))

Вывод программы:

0, 1, 4, 9, 16

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

result = map(str.lower, ["abCD", "EFGh", "IJkl"])
print("\n".join(result))

Вывод программы:

abcd
efgh
ijkl

Частым случаем применения функции map() является получение списка целых чисел из стандартного ввода:

numbers = list(map(int, input().split()))

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

lambda x: x > 0

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

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

result = filter(lambda x: x > 0, [-1, 5, 6, -10, 0])
print(", ".join(str(x) for x in result))

Вывод программы:

5, 6
result = map(lambda x: x ** 2, range(5))
print(", ".join(str(x) for x in result))

Вывод программы:

0, 1, 4, 9, 16

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

Напишем программу для сортировки списка строк по длине строки в порядке возрастания:

lines = ["abcd", "ab", "abc", "abcdef"]
print(sorted(lines, key=lambda line: len(line)))

Вывод программы:

['ab', 'abc', 'abcd', 'abcdef']

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

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

lines = ["abcd", "ab", "ba", "acde"]
print(sorted(lines, key=lambda line: (len(line), line)))

Вывод программы:

['ab', 'ba', 'abcd', 'acde']

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

lines = ["abcd", "ab", "ba", "acde"]
print(sorted(lines, key=lambda line: (-len(line), line)))

Вывод программы:

['abcd', 'acde', 'ab', 'ba']

Аргумент key есть и у метода sort() (меняет исходный список на отсортированный), а также у функций max() и min(). Найдём в списке строк самую длинную, а если таких несколько, то лексикографически меньшую:

lines = ["abcd", "ab", "ba", "acde"]
print(min(lines, key=lambda line: (-len(line), line)))

Вывод программы:

abcd

Если вспомнить про списочные выражения, то функции map() и filter() можно заменить соответствующими списочными выражениями. Например:

result = (x for x in [-1, 5, 6, -10, 0] if x > 0)
print(", ".join(str(x) for x in result))

Вывод программы:

5, 6

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

Ещё по теме

Частым случаем применения лямбда-функций является обработка итерируемых объектов с помощью функций стандартной библиотеки functools. Например, функция reduce() кумулятивно (с накоплением) применяет функцию двух аргументов к элементам обрабатываемого итерируемого объекта. Таким образом, функция reduce() из нескольких значений получает одно, производя так называемую операцию свёртки. В качестве функции обработки можно использовать лямбда-функцию двух аргументов.

Напишем программу, которая производит свёртку списка целых чисел в их сумму:

from functools import reduce

numbers = range(1, 6)
print(reduce(lambda x, y: x + y, numbers))

Вывод программы:

15

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф4.1. Функции. Области видимости. Передача параметров в функции

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

Следующий параграф4.3. Рекурсия. Декораторы. Генераторы

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