Как использовать лямбда-функции Python
Оглавление
- Исчисление Лямбды
- Анонимные функции
- Python Лямбда и регулярные функции
- Злоупотребления выражениями лямбд
- Правильное использование лямбда-выражений
- Альтернативы ламбдам
Python и другие языки, такие как Java, C# и даже C++, добавили в свой синтаксис лямбда-функции, а такие языки, как LISP или семейство языков ML, Haskell, OCaml и F#, используют лямбды в качестве основной концепции.
Python lambdas - это маленькие анонимные функции, подчиняющиеся более строгому, но более лаконичному синтаксису, чем обычные функции Python.
К концу этой статьи вы будете знать:
- Как появились лямбды в Python
- Как лямбды сравниваются с объектами обычных функций
- Как писать лямбда-функции
- Какие функции стандартной библиотеки Python используют лямбды
- Когда использовать или избегать лямбда-функций Python
Примечания: Вы увидите несколько примеров кода с использованием lambda
, которые, кажется, откровенно игнорируют лучшие практики стиля Python. Это сделано только для того, чтобы проиллюстрировать концепции лямбда-исчисления или подчеркнуть возможности Python lambda
.
По мере продвижения по статье эти сомнительные примеры будут противопоставляться лучшим подходам или альтернативам.
Этот учебник предназначен в основном для опытных и средних программистов на Python, но он доступен любому любознательному уму, интересующемуся программированием и лямбда-исчислением.
Все примеры, включенные в это руководство, были протестированы на Python 3.7.
Лямбда Калькуляция
Лямбда-выражения в Python и других языках программирования берут свое начало в лямбда-исчислении, модели вычислений, изобретенной Алонзо Черчем. Вы узнаете, когда появилось лямбда-исчисление и почему это фундаментальное понятие оказалось в экосистеме Python.
История
Алонзо Черч формализовал ламбда-исчисление, язык, основанный на чистой абстракции, в 1930-х годах. Лямбда-функции также называются лямбда-абстракциями, что является прямой отсылкой к модели абстракции оригинального творения Алонзо Черча.
Лямбда-исчисление может кодировать любые вычисления. Оно является полным по Тьюрингу, но, в отличие от понятия машины Тьюринга, оно чистое и не сохраняет никаких состояний.
Функциональные языки берут свое начало в математической логике и лямбда-исчислении, в то время как императивные языки программирования используют модель вычислений, основанную на состояниях и изобретенную Аланом Тьюрингом. Две модели вычислений, лямбда-исчисление и машины Тьюринга, могут быть переведены друг в друга. Эта эквивалентность известна как гипотеза Чёрча-Тьюринга.
Функциональные языки напрямую наследуют философию лямбда-исчисления, принимая декларативный подход к программированию, который подчеркивает абстракцию, преобразование данных, композицию и чистоту (отсутствие состояния и побочных эффектов). Примерами функциональных языков являются Haskell, Lisp или Erlang.
В отличие от этого, машина Тьюринга привела к императивному программированию на таких языках, как Fortran, C или Python.
Императивный стиль заключается в программировании с помощью операторов, пошагово управляя ходом программы с помощью подробных инструкций. Такой подход способствует мутации и требует управления состоянием.
Разделение в обоих семействах имеет некоторые нюансы, так как некоторые функциональные языки включают в себя императивные черты, например OCaml, в то время как функциональные черты проникают в императивное семейство языков, в частности, с введением лямбда-функций в Java или Python.
Python не является функциональным языком по своей сути, но некоторые функциональные концепции в нем появились довольно рано. В январе 1994 года в язык были добавлены map()
, filter()
, reduce()
и оператор lambda
.
Первый пример
Вот несколько примеров, чтобы вы с аппетитом познакомились с кодом на Python в функциональном стиле.
Функция identity, функция, возвращающая свой аргумент, выражается стандартным определением функции Python с помощью ключевого слова def
следующим образом:
>>> def identity(x):
... return x
identity()
принимает аргумент x
и возвращает его после вызова.
В отличие от этого, если вы используете лямбда-конструкцию Python, вы получите следующее:
>>> lambda x: x
В приведенном выше примере выражение состоит из:
- Ключевое слово:
lambda
- Связанная переменная:
x
- Тело:
x
Примечание: В контексте данной статьи связанная переменная - это аргумент лямбда-функции.
В отличие от нее, свободная переменная не связана и на нее можно ссылаться в теле выражения. Свободная переменная может быть константой или переменной, определенной в объемлющей области функции.
Вы можете написать чуть более сложный пример, функцию, которая добавляет 1
к аргументу, следующим образом:
>>> lambda x: x + 1
Вы можете применить приведенную выше функцию к аргументу, заключив функцию и ее аргумент в круглые скобки:
>>> (lambda x: x + 1)(2)
3
Reduction - это стратегия лямбда-исчисления для вычисления значения выражения. В данном примере она заключается в замене связанной переменной x
на аргумент 2
:
(lambda x: x + 1)(2) = lambda 2: 2 + 1
= 2 + 1
= 3
Поскольку лямбда-функция - это выражение, ей можно присвоить имя. Поэтому предыдущий код можно записать следующим образом:
>>> add_one = lambda x: x + 1
>>> add_one(2)
3
Приведенная выше лямбда-функция эквивалентна написанию следующей:
def add_one(x):
return x + 1
Все эти функции принимают один аргумент. Вы могли заметить, что в определении лямбд аргументы не заключены в круглые скобки. Многоаргументные функции (функции, принимающие более одного аргумента) выражаются в лямбдах Python путем перечисления аргументов и разделения их запятой (,
), но без окружения круглыми скобками:
>>> full_name = lambda first, last: f'Full name: {first.title()} {last.title()}'
>>> full_name('guido', 'van rossum')
'Full name: Guido Van Rossum'
Лямбда-функция, назначенная на full_name
, принимает два аргумента и возвращает строку , интерполирующую два параметра first
и last
. Как и ожидалось, в определении лямбды аргументы перечислены без круглых скобок, а вызов функции осуществляется точно так же, как и в обычной функции Python, с круглыми скобками вокруг аргументов.
Анонимные функции
В зависимости от типа языка программирования и культуры следующие термины могут использоваться как взаимозаменяемые:
- Анонимные функции
- Лямбда-функции
- Лямбда-выражения
- Лямбда-абстракции
- Лямбда-форма
- Литералы функций
После этого раздела вы чаще всего будете встречать термин лямбда-функция.
В буквальном смысле анонимная функция - это функция без имени. В Python анонимная функция создается с помощью ключевого слова lambda
. В более свободном смысле ей может быть присвоено или не присвоено имя. Рассмотрим анонимную функцию с двумя аргументами, заданную с помощью lambda
, но не связанную с переменной. Лямбде не присваивается имя:
>>> lambda x, y: x + y
Приведенная выше функция определяет лямбда-выражение, которое принимает два аргумента и возвращает их сумму.
Кроме того, что это дает вам понять, что Python прекрасно справляется с этой формой, это не приводит ни к какому практическому использованию. Вы можете вызвать функцию в интерпретаторе Python:
>>> _(1, 2)
3
В приведенном примере используется возможность интерактивного интерпретатора, предоставляемая только через underscore (_
). Более подробную информацию см. в примечании ниже.
Вы не смогли бы написать подобный код в модуле Python. Считайте, что _
в интерпретаторе - это побочный эффект, которым вы воспользовались. В модуле Python вы бы присвоили лямбде имя или передали бы лямбду в функцию. Вы будете использовать эти два подхода позже в этой статье.
Примечание: В интерактивном интерпретаторе одиночное подчеркивание (_
) привязывается к последнему вычисленному выражению.
В примере выше символ _
указывает на лямбда-функцию. Подробнее об использовании этого специального символа в Python читайте в статье Значение символов подчеркивания в Python.
Другой паттерн, используемый в других языках, таких как JavaScript, заключается в немедленном выполнении лямбда-функции Python. Это называется Immediately Invoked Function Expression (IIFE, произносится "iffy"). Вот пример:
>>> (lambda x, y: x + y)(2, 3)
5
Приведенная выше лямбда-функция определяется и сразу же вызывается с двумя аргументами (2
и 3
). Она возвращает значение 5
, которое является суммой аргументов.
В нескольких примерах в этом учебнике используется этот формат, чтобы подчеркнуть анонимный аспект лямбда-функции и избежать акцента на lambda
в Python как более коротком способе определения функции.
Python не поощряет использование немедленно вызываемых лямбда-выражений. Это просто происходит из-за того, что лямбда-выражение является вызываемым, в отличие от тела обычной функции.
Лямбда-функции часто используются с функциями высшего порядка, которые принимают одну или несколько функций в качестве аргументов или возвращают одну или несколько функций.
Лямбда-функция может быть функцией более высокого порядка, принимая в качестве аргумента функцию (обычную или лямбда), как в следующем надуманном примере:
>>> high_ord_func = lambda x, func: x + func(x)
>>> high_ord_func(2, lambda x: x * x)
6
>>> high_ord_func(2, lambda x: x + 3)
7
Python предоставляет функции высшего порядка в виде встроенных функций или в стандартной библиотеке. Примеры включают map()
, filter()
, functools.reduce()
, а также такие ключевые функции, как sort()
, sorted()
, min()
и max()
. Вы будете использовать лямбда-функции вместе с функциями высшего порядка Python в разделе "Уместное использование лямбда-выражений".
Python Лямбда и регулярные функции
Эта цитата из Python Design and History FAQ, кажется, задает тон относительно общих ожиданий по поводу использования лямбда-функций в Python:
В отличие от лямбда-форм в других языках, где они добавляют функциональности, лямбды в Python - это лишь сокращенное обозначение, если вам лень определять функцию. (Source)
Тем не менее, пусть это утверждение не отпугнет вас от использования lambda
в Python. На первый взгляд, вы можете принять, что лямбда-функция - это функция с некоторым синтаксическим сахаром, сокращающим код для определения или вызова функции. В следующих разделах мы рассмотрим общие черты и тонкие различия между обычными функциями Python и лямбда-функциями.
Функции
На данный момент вы можете задаться вопросом, что принципиально отличает лямбда-функцию, связанную с переменной, от обычной функции с одной строкой return
: на первый взгляд, почти ничего. Давайте проверим, как Python воспринимает функцию, построенную с помощью единственного оператора возврата , в сравнении с функцией, построенной как выражение (lambda
).
Модуль dis
предоставляет функции для анализа байткода Python, генерируемого компилятором Python:
>>> import dis
>>> add = lambda x, y: x + y
>>> type(add)
<class 'function'>
>>> dis.dis(add)
1 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
>>> add
<function <lambda> at 0x7f30c6ce9ea0>
Вы видите, что dis()
представляет собой читаемую версию байткода Python, позволяющую просматривать низкоуровневые инструкции, которые интерпретатор Python будет использовать при выполнении программы.
Теперь посмотрим на это с помощью обычного объекта функции:
>>> import dis
>>> def add(x, y): return x + y
>>> type(add)
<class 'function'>
>>> dis.dis(add)
1 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
>>> add
<function add at 0x7f30c6ce9f28>
Интерпретируемый Python байткод одинаков для обеих функций. Но вы можете заметить, что именование отличается: имя функции add
для функции, определенной с помощью def
, в то время как лямбда-функция Python воспринимается как lambda
.
Traceback
В предыдущем разделе вы видели, что в контексте лямбда-функции Python не предоставляет имя функции, а только <lambda>
. Это может быть ограничением, которое следует учитывать, когда возникает исключение, и traceback показывает только <lambda>
:
>>> div_zero = lambda x: x / 0
>>> div_zero(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <lambda>
ZeroDivisionError: division by zero
В traceback исключения, возникшего во время выполнения лямбда-функции, идентифицируется только функция, вызвавшая исключение, как <lambda>
.
Вот то же самое исключение, вызванное обычной функцией:
>>> def div_zero(x): return x / 0
>>> div_zero(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in div_zero
ZeroDivisionError: division by zero
Обычная функция вызывает аналогичную ошибку, но приводит к более точному возврату трассировки, поскольку указывает имя функции, div_zero
.
Синтаксис
Как вы видели в предыдущих разделах, лямбда-форма имеет синтаксические отличия от обычной функции. В частности, лямбда-функция обладает следующими характеристиками:
- Он может содержать только выражения и не может включать утверждения в свое тело.
- Он записывается как одна строка исполнения.
- Не поддерживает аннотации типов.
- Может быть вызван немедленно (IIFE).
Нет заявлений
Лямбда-функция не может содержать никаких утверждений. В лямбда-функции утверждения типа return
, pass
, assert
или raise
вызовут исключение SyntaxError
. Вот пример добавления assert
в тело лямбды:
>>> (lambda x: assert x == 2)(2)
File "<input>", line 1
(lambda x: assert x == 2)(2)
^
SyntaxError: invalid syntax
В этом надуманном примере предполагалось assert
, что параметр x
имеет значение 2
. Но при разборе кода интерпретатор выявил SyntaxError
, что в теле assert
находится утверждение lambda
.
Одиночное выражение
В отличие от обычной функции, лямбда-функция в Python представляет собой одно выражение. Хотя в теле lambda
вы можете разнести выражение на несколько строк с помощью круглых скобок или многострочной строки, оно остается единственным выражением:
>>> (lambda x:
... (x % 2 and 'odd' or 'even'))(3)
'odd'
Приведенный выше пример возвращает строку 'odd'
, если аргумент лямбды нечетный, и 'even'
, если аргумент четный. Она занимает две строки, потому что заключена в круглые скобки, но остается одним выражением.
Тип Аннотации
Если вы начали использовать подсказки типов, которые теперь доступны в Python, то у вас есть еще одна веская причина предпочесть обычные функции лямбда-функциям Python. Ознакомьтесь с Python Type Checking (Guide), чтобы узнать больше о подсказках типов Python и проверке типов. В лямбда-функции нет эквивалента для следующего:
def full_name(first: str, last: str) -> str:
return f'{first.title()} {last.title()}'
Любая ошибка типа с full_name()
может быть поймана такими инструментами, как mypy
или pyre
, тогда как SyntaxError
с эквивалентной лямбда-функцией возникает во время выполнения:
>>> lambda first: str, last: str: first.title() + " " + last.title() -> str
File "<stdin>", line 1
lambda first: str, last: str: first.title() + " " + last.title() -> str
SyntaxError: invalid syntax
Как и при попытке включить оператор в лямбду, добавление аннотации типа сразу же приводит к SyntaxError
во время выполнения.
ИФА
Вы уже видели несколько примеров немедленного выполнения функций:
>>> (lambda x: x * x)(3)
9
За пределами интерпретатора Python эта возможность, скорее всего, не используется на практике. Это прямое следствие того, что лямбда-функция может быть вызвана в том виде, в котором она определена. Например, это позволяет передать определение лямбда-выражения Python в функцию более высокого порядка, такую как map()
, filter()
или functools.reduce()
, или в ключевую функцию.
Аргументы
Как и обычный объект функции, определяемый с помощью def
, лямбда-выражения Python поддерживают все различные способы передачи аргументов. К ним относятся:
- Позиционные аргументы
- Именованные аргументы (иногда называемые аргументами с ключевыми словами)
- Переменный список аргументов (часто называемый varargs)
- Переменный список аргументов ключевых слов
- Аргументы только для ключевых слов
В следующих примерах показаны варианты передачи аргументов в лямбда-выражения:
>>> (lambda x, y, z: x + y + z)(1, 2, 3)
6
>>> (lambda x, y, z=3: x + y + z)(1, 2)
6
>>> (lambda x, y, z=3: x + y + z)(1, y=2)
6
>>> (lambda *args: sum(args))(1,2,3)
6
>>> (lambda **kwargs: sum(kwargs.values()))(one=1, two=2, three=3)
6
>>> (lambda x, *, y=0, z=0: x + y + z)(1, y=2, z=3)
6
Декораторы
В Python декоратор - это реализация паттерна, который позволяет добавить поведение в функцию или класс. Обычно он выражается с помощью синтаксиса @decorator
с префиксом функции. Вот надуманный пример:
def some_decorator(f):
def wraps(*args):
print(f"Calling function '{f.__name__}'")
return f(args)
return wraps
@some_decorator
def decorated_function(x):
print(f"With argument '{x}'")
В приведенном выше примере some_decorator()
- это функция, которая добавляет поведение к decorated_function()
, так что вызов decorated_function("Python")
приводит к следующему выводу:
Calling function 'decorated_function'
With argument 'Python'
decorated_function()
выводит только With argument 'Python'
, но декоратор добавляет дополнительное поведение, которое также выводит Calling function 'decorated_function'
.
Декоратор может быть применен к лямбде. Хотя украсить лямбду с помощью синтаксиса @decorator
невозможно, декоратор - это просто функция, поэтому он может вызывать лямбда-функцию:
1 # Defining a decorator
2 def trace(f):
3 def wrap(*args, **kwargs):
4 print(f"[TRACE] func: {f.__name__}, args: {args}, kwargs: {kwargs}")
5 return f(*args, **kwargs)
6
7 return wrap
8
9 # Applying decorator to a function
10 @trace
11 def add_two(x):
12 return x + 2
13
14 # Calling the decorated function
15 add_two(3)
16
17 # Applying decorator to a lambda
18 print((trace(lambda x: x ** 2))(3))
add_two()
, декорированный с помощью @trace
в строке 11, вызывается с аргументом 3
в строке 15. Напротив, в строке 18 лямбда-функция сразу же задействуется и встраивается в вызов декоратора trace()
. При выполнении приведенного выше кода вы получите следующее:
[TRACE] func: add_two, args: (3,), kwargs: {}
[TRACE] func: <lambda>, args: (3,), kwargs: {}
9
Обратите внимание, что, как вы уже видели, имя лямбда-функции отображается как <lambda>
, в то время как add_two
четко обозначено для обычной функции.
Декорирование лямбда-функции таким образом может быть полезно для отладки, возможно, для отладки поведения лямбда-функции, используемой в контексте функции более высокого порядка или ключевой функции. Рассмотрим пример с map()
:
list(map(trace(lambda x: x*2), range(3)))
Первым аргументом map()
является лямбда, которая умножает свой аргумент на 2
. Эта лямбда украшена символом trace()
. При выполнении приведенного выше примера получается следующее:
[TRACE] Calling <lambda> with args (0,) and kwargs {}
[TRACE] Calling <lambda> with args (1,) and kwargs {}
[TRACE] Calling <lambda> with args (2,) and kwargs {}
[0, 2, 4]
Результатом [0, 2, 4]
является список , полученный в результате умножения каждого элемента range(3)
. Пока считайте, что range(3)
эквивалентен списку [0, 1, 2]
.
Более подробно вы познакомитесь с map()
в Map.
Лямбда также может быть декоратором, но это не рекомендуется. Если вы столкнетесь с необходимостью сделать это, обратитесь к PEP 8, Рекомендации по программированию.
Подробнее о декораторах Python читайте в статье "Праймер по декораторам Python" .
Замыкание
А замыкание - это функция, в которой каждая свободная переменная, за исключением параметров, используемая в этой функции, привязана к конкретному значению, определенному в объемлющей области видимости этой функции. По сути, замыкания определяют среду, в которой они выполняются, и поэтому могут быть вызваны из любого места.
Понятия лямбды и замыкания не обязательно связаны, хотя лямбда-функции могут быть замыканиями так же, как и обычные функции. Некоторые языки имеют специальные конструкции для замыкания или лямбды (например, Groovy с анонимным блоком кода в качестве объекта Closure), или лямбда-выражения (например, Java Lambda expression с ограниченной возможностью замыкания).
Вот закрытие, построенное с помощью обычной функции Python:
1 def outer_func(x):
2 y = 4
3 def inner_func(z):
4 print(f"x = {x}, y = {y}, z = {z}")
5 return x + y + z
6 return inner_func
7
8 for i in range(3):
9 closure = outer_func(i)
10 print(f"closure({i+5}) = {closure(i+5)}")
outer_func()
возвращает inner_func()
, вложенную функцию , которая вычисляет сумму трех аргументов:
x
передается в качестве аргумента вouter_func()
.y
- переменная, локальная дляouter_func()
.z
- аргумент, передаваемый вinner_func()
.
Чтобы проверить поведение outer_func()
и inner_func()
, outer_func()
вызывается три раза в цикле for
, который печатает следующее:
x = 0, y = 4, z = 5
closure(5) = 9
x = 1, y = 4, z = 6
closure(6) = 11
x = 2, y = 4, z = 7
closure(7) = 13
В строке 9 кода inner_func()
, возвращаемый вызовом outer_func()
, привязывается к имени closure
. В строке 5 inner_func()
захватывает x
и y
, потому что имеет доступ к своему окружению встраивания, так что при вызове закрытия он может оперировать двумя свободными переменными x
и y
.
Аналогично, lambda
также может быть замыканием. Вот тот же пример с лямбда-функцией Python:
1 def outer_func(x):
2 y = 4
3 return lambda z: x + y + z
4
5 for i in range(3):
6 closure = outer_func(i)
7 print(f"closure({i+5}) = {closure(i+5)}")
Выполнив приведенный выше код, вы получите следующий результат:
closure(5) = 9
closure(6) = 11
closure(7) = 13
В строке 6 функция outer_func()
возвращает лямбду и присваивает ее переменной closure
. В строке 3 тело лямбда-функции ссылается на x
и y
. Переменная y
доступна во время определения, тогда как x
определяется во время выполнения, когда вызывается outer_func()
.
В этой ситуации и обычная функция, и лямбда ведут себя одинаково. В следующем разделе вы увидите ситуацию, когда поведение лямбды может быть обманчивым из-за времени ее оценки (время определения против времени выполнения).
Время оценки
В некоторых ситуациях, связанных с циклами, поведение лямбда-функции Python в качестве замыкания может быть контринтуитивным. Это требует понимания того, когда свободные переменные связаны в контексте лямбды. Следующие примеры демонстрируют разницу между использованием обычной функции и лямбда-функции Python.
Сначала протестируйте сценарий с помощью обычной функции:
1 >>> def wrap(n):
2 ... def f():
3 ... print(n)
4 ... return f
5 ...
6 >>> numbers = 'one', 'two', 'three'
7 >>> funcs = []
8 >>> for n in numbers:
9 ... funcs.append(wrap(n))
10 ...
11 >>> for f in funcs:
12 ... f()
13 ...
14 one
15 two
16 three
В обычной функции n
оценивается во время определения, в строке 9, когда функция добавляется в список: funcs.append(wrap(n))
.
Теперь, реализуя ту же логику с помощью лямбда-функции, наблюдаем неожиданное поведение:
1 >>> numbers = 'one', 'two', 'three'
2 >>> funcs = []
3 >>> for n in numbers:
4 ... funcs.append(lambda: print(n))
5 ...
6 >>> for f in funcs:
7 ... f()
8 ...
9 three
10 three
11 three
Неожиданный результат возникает потому, что свободная переменная n
, как это реализовано, связана во время выполнения лямбда-выражения. Лямбда-функция Python в строке 4 представляет собой закрытие, которое захватывает n
, свободную переменную, связанную во время выполнения. Во время выполнения, при вызове функции f
в строке 7, значение n
равно three
.
Чтобы решить эту проблему, вы можете назначить свободную переменную во время определения следующим образом:
1 >>> numbers = 'one', 'two', 'three'
2 >>> funcs = []
3 >>> for n in numbers:
4 ... funcs.append(lambda n=n: print(n))
5 ...
6 >>> for f in funcs:
7 ... f()
8 ...
9 one
10 two
11 three
В отношении аргументов лямбда-функция Python ведет себя как обычная функция. Поэтому параметр лямбды можно инициализировать значением по умолчанию: параметр n
принимает внешнее значение n
в качестве значения по умолчанию. Лямбда-функцию Python можно было бы записать как lambda x=n: print(x)
и получить тот же результат.
В строке 7 лямбда-функция Python вызывается без аргументов и использует значение по умолчанию n
, заданное во время определения.
Тестирование ламбд
Ламбды в Python можно тестировать так же, как и обычные функции. Можно использовать как unittest
, так и doctest
.
unittest
Модуль unittest
обрабатывает лямбда-функции Python аналогично обычным функциям:
import unittest
addtwo = lambda x: x + 2
class LambdaTest(unittest.TestCase):
def test_add_two(self):
self.assertEqual(addtwo(2), 4)
def test_add_two_point_two(self):
self.assertEqual(addtwo(2.2), 4.2)
def test_add_three(self):
# Should fail
self.assertEqual(addtwo(3), 6)
if __name__ == '__main__':
unittest.main(verbosity=2)
LambdaTest
определяет тестовый пример с тремя тестовыми методами, каждый из которых реализует тестовый сценарий для addtwo()
, реализованный в виде лямбда-функции. Выполнение Python-файла lambda_unittest.py
, содержащего LambdaTest
, приводит к следующему:
$ python lambda_unittest.py
test_add_three (__main__.LambdaTest) ... FAIL
test_add_two (__main__.LambdaTest) ... ok
test_add_two_point_two (__main__.LambdaTest) ... ok
======================================================================
FAIL: test_add_three (__main__.LambdaTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "lambda_unittest.py", line 18, in test_add_three
self.assertEqual(addtwo(3), 6)
AssertionError: 5 != 6
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
Как и ожидалось, у нас есть два успешных тестовых случая и один неудачный для test_add_three
: результат 5
, но ожидаемый результат был 6
. Эта неудача вызвана намеренной ошибкой в тестовом примере. Замена ожидаемого результата с 6
на 5
удовлетворит все тесты для LambdaTest
.
doctest
Модуль doctest
извлекает интерактивный код Python из docstring
для выполнения тестов. Хотя синтаксис лямбда-функций Python не поддерживает типичные docstring
, можно присвоить строку элементу __doc__
именованной лямбды:
addtwo = lambda x: x + 2
addtwo.__doc__ = """Add 2 to a number.
>>> addtwo(2)
4
>>> addtwo(2.2)
4.2
>>> addtwo(3) # Should fail
6
"""
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True)
В комментарии doc к лямбде doctest
addtwo()
описываются те же тестовые случаи, что и в предыдущем разделе.
При выполнении тестов через doctest.testmod()
вы получаете следующее:
$ python lambda_doctest.py
Trying:
addtwo(2)
Expecting:
4
ok
Trying:
addtwo(2.2)
Expecting:
4.2
ok
Trying:
addtwo(3) # Should fail
Expecting:
6
**********************************************************************
File "lambda_doctest.py", line 16, in __main__.addtwo
Failed example:
addtwo(3) # Should fail
Expected:
6
Got:
5
1 items had no tests:
__main__
**********************************************************************
1 items had failures:
1 of 3 in __main__.addtwo
3 tests in 2 items.
2 passed and 1 failed.
***Test Failed*** 1 failures.
Неудачный тест является результатом того же сбоя, что и при выполнении модульных тестов в предыдущем разделе.
Вы можете добавить docstring
к лямбде Python через присваивание __doc__
для документирования лямбда-функции. Хотя это возможно, синтаксис Python лучше приспособлен к docstring
для обычных функций, чем для лямбда-функций.
Для всестороннего обзора модульного тестирования в Python вы можете обратиться к Getting Started With Testing in Python.
Злоупотребления выражениями лямбда
Несколько примеров в этой статье, если бы они были написаны в контексте профессионального кода на Python, квалифицировались бы как злоупотребления.
Если вы обнаружите, что пытаетесь решить задачу, которую не поддерживает лямбда-выражение, это, вероятно, признак того, что лучше использовать обычную функцию. Хорошим примером является docstring
для лямбда-выражения в предыдущем разделе. Попытка преодолеть тот факт, что лямбда-функция Python не поддерживает операторы, - еще один тревожный знак.
Следующие разделы иллюстрируют несколько примеров использования лямбд, которых следует избегать. Такими примерами могут быть ситуации, когда в контексте лямбд Python код выглядит следующим образом:
- Не соответствует руководству по стилю Python (PEP 8)
- Он громоздкий и трудночитаемый.
- Это излишне умно ценой трудночитаемости.
Восстановление исключения
Попытка вызвать исключение в лямбде Python должна заставить вас дважды подумать. Есть несколько хитроумных способов сделать это, но даже чего-то подобного лучше избегать:
>>> def throw(ex): raise ex
>>> (lambda: throw(Exception('Something bad happened')))()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <lambda>
File "<stdin>", line 1, in throw
Exception: Something bad happened
<<<Поскольку утверждение не является синтаксически корректным в теле лямбды Python, обходной путь в приведенном выше примере заключается в абстрагировании вызова утверждения с помощью специальной функции throw()
. Использования такого обходного пути следует избегать. Если вы столкнулись с подобным кодом, вам следует рассмотреть возможность рефакторинга кода для использования обычной функции.
Криптовый стиль
Как и в любом другом языке программирования, в коде Python можно встретить трудночитаемый код из-за используемого стиля. Лямбда-функции, благодаря своей краткости, могут способствовать написанию кода, который трудно читать.
Следующий пример лямбды содержит несколько неудачных стилевых решений:
>>> (lambda _: list(map(lambda _: _ // 2, _)))([1,2,3,4,5,6,7,8,9,10])
[0, 1, 1, 2, 2, 3, 3, 4, 4, 5]
Подчеркивание (_
) обозначает переменную, на которую не нужно ссылаться явно. Но в этом примере три _
ссылаются на разные переменные. Первоначальной модернизацией этого лямбда-кода могло бы стать присвоение переменным имен:
>>> (lambda some_list: list(map(lambda n: n // 2,
some_list)))([1,2,3,4,5,6,7,8,9,10])
[0, 1, 1, 2, 2, 3, 3, 4, 4, 5]
Признаться, его все еще трудно читать. Пользуясь преимуществом lambda
, обычная функция могла бы сделать этот код более читабельным, распределив логику по нескольким строкам и вызовам функций:
>>> def div_items(some_list):
div_by_two = lambda n: n // 2
return map(div_by_two, some_list)
>>> list(div_items([1,2,3,4,5,6,7,8,9,10])))
[0, 1, 1, 2, 2, 3, 3, 4, 4, 5]
Это все еще не оптимально, но показывает возможный путь к тому, чтобы сделать код, и в частности лямбда-функции Python, более читабельным. В Альтернативы лямбдам вы узнаете, как заменить map()
и lambda
на списочные представления или генераторные выражения. Это значительно улучшит читабельность кода.
Классы Python
Вы можете, но не должны писать методы класса как лямбда-функции Python. Следующий пример является совершенно законным кодом Python, но демонстрирует нетрадиционный код Python, полагающийся на lambda
. Например, вместо того чтобы реализовать __str__
как обычную функцию, в нем используется lambda
. Аналогично, brand
и year
- это свойства, также реализованные с помощью лямбда-функций, а не обычных функций или декораторов:
class Car:
"""Car with methods as lambda functions."""
def __init__(self, brand, year):
self.brand = brand
self.year = year
brand = property(lambda self: getattr(self, '_brand'),
lambda self, value: setattr(self, '_brand', value))
year = property(lambda self: getattr(self, '_year'),
lambda self, value: setattr(self, '_year', value))
__str__ = lambda self: f'{self.brand} {self.year}' # 1: error E731
honk = lambda self: print('Honk!') # 2: error E731
Запуск такого инструмента, как flake8
, инструмента для соблюдения руководства по стилю, выдаст следующие ошибки для __str__
и honk
:
E731 do not assign a lambda expression, use a def
Хотя flake8
не указывает на проблему использования лямбда-функций Python в свойствах, их трудно читать и они склонны к ошибкам из-за использования нескольких строк, таких как '_brand'
и '_year'
.
Правильная реализация __str__
будет выглядеть следующим образом:
def __str__(self):
return f'{self.brand} {self.year}'
brand
будет записано следующим образом:
@property
def brand(self):
return self._brand
@brand.setter
def brand(self, value):
self._brand = value
Как правило, в контексте кода, написанного на Python, предпочтение отдается регулярным функциям, а не лямбда-выражениям. Тем не менее, есть случаи, когда лямбда-синтаксис приносит пользу, как вы увидите в следующем разделе.
Уместное использование лямбда-выражений
Лямбды в Python, как правило, являются предметом споров. Некоторые из аргументов против лямбд в Python таковы:
- Проблемы с читабельностью
- Навязывание функционального образа мышления
- Тяжелый синтаксис с ключевым словом
lambda
Несмотря на жаркие споры, ставящие под сомнение само существование этой функции в Python, лямбда-функции обладают свойствами, которые иногда представляют ценность для языка Python и для разработчиков.
Следующие примеры иллюстрируют сценарии, в которых использование лямбда-функций не только уместно, но и поощряется в коде Python.
Классические функциональные конструкции
Лямбда-функции регулярно используются со встроенными функциями map()
и filter()
, а также functools.reduce()
, раскрываемыми в модуле functools
. Следующие три примера иллюстрируют использование этих функций с лямбда-выражениями в качестве компаньонов:
>>> list(map(lambda x: x.upper(), ['cat', 'dog', 'cow']))
['CAT', 'DOG', 'COW']
>>> list(filter(lambda x: 'o' in x, ['cat', 'dog', 'cow']))
['dog', 'cow']
>>> from functools import reduce
>>> reduce(lambda acc, x: f'{acc} | {x}', ['cat', 'dog', 'cow'])
'cat | dog | cow'
Возможно, вам придется читать код, похожий на приведенные выше примеры, хотя и с более значимыми данными. По этой причине важно распознать эти конструкции. Тем не менее, у этих конструкций есть эквивалентные альтернативы, которые считаются более питоническими. В разделе Альтернативы ламбдам вы узнаете, как преобразовать функции высшего порядка и сопровождающие их ламбды в другие, более идиоматические формы.
Функции клавиш
Ключевые функции в Python - это функции высшего порядка, которые принимают параметр key
в качестве именованного аргумента. key
принимает функцию, которая может быть lambda
. Эта функция непосредственно влияет на алгоритм, управляемый самой ключевой функцией. Вот некоторые ключевые функции:
sort()
: метод спискаsorted()
,min()
,max()
: встроенные функцииnlargest()
иnsmallest()
: в модуле алгоритма очереди кучиheapq
Представьте себе, что вы хотите отсортировать список идентификаторов, представленных в виде строк. Каждый идентификатор - это конкатенация строки id
и числа. Сортировка этого списка с помощью встроенной функции sorted()
по умолчанию использует лексикографический порядок, поскольку элементы списка являются строками.
Чтобы повлиять на выполнение сортировки, вы можете присвоить лямбду именованному аргументу key
так, что при сортировке будет использоваться номер, связанный с идентификатором:
>>> ids = ['id1', 'id2', 'id30', 'id3', 'id22', 'id100']
>>> print(sorted(ids)) # Lexicographic sort
['id1', 'id100', 'id2', 'id22', 'id3', 'id30']
>>> sorted_ids = sorted(ids, key=lambda x: int(x[2:])) # Integer sort
>>> print(sorted_ids)
['id1', 'id2', 'id3', 'id22', 'id30', 'id100']
UI Frameworks
Такие фреймворки пользовательского интерфейса, как Tkinter, wxPython или .NET Windows Forms с IronPython, используют преимущества лямбда-функций для отображения действий в ответ на события пользовательского интерфейса.
Приведенная ниже наивная программа Tkinter демонстрирует использование lambda
, назначенного на команду кнопки Reverse:
import tkinter as tk
import sys
window = tk.Tk()
window.grid_columnconfigure(0, weight=1)
window.title("Lambda")
window.geometry("300x100")
label = tk.Label(window, text="Lambda Calculus")
label.grid(column=0, row=0)
button = tk.Button(
window,
text="Reverse",
command=lambda: label.configure(text=label.cget("text")[::-1]),
)
button.grid(column=0, row=1)
window.mainloop()
При нажатии на кнопку Reverse происходит событие, которое запускает лямбда-функцию, изменяя метку с Lambda Calculus на suluclaC adbmaL*:
Как wxPython, так и IronPython на платформе .NET имеют схожий подход к обработке событий. Обратите внимание, что lambda
- это один из способов обработки событий, но для той же цели можно использовать и функцию. В конечном итоге использование lambda
оказывается более компактным и менее многословным, если объем кода очень короткий.
Чтобы познакомиться с wxPython, прочитайте статью How to Build a Python GUI Application With wxPython.
Python Interpreter
Когда вы играете с кодом Python в интерактивном интерпретаторе, лямбда-функции Python часто оказываются благословением. Легко создать быструю однострочную функцию для изучения некоторых фрагментов кода, которые никогда не увидят свет за пределами интерпретатора. Лямбды, написанные в интерпретаторе ради быстрого открытия, похожи на бумагу, которую можно выбросить после использования.
timeit
В том же духе, что и эксперименты в интерпретаторе Python, модуль timeit
предоставляет функции для тайминга небольших фрагментов кода. В частности, timeit.timeit()
можно вызывать напрямую, передавая некоторый код Python в строке. Вот пример:
>>> from timeit import timeit
>>> timeit("factorial(999)", "from math import factorial", number=10)
0.0013087529951008037
Когда оператор передается в виде строки, timeit()
нужен полный контекст. В приведенном выше примере он обеспечивается вторым аргументом, который задает окружение, необходимое главной функции для тайминга. Если этого не сделать, то возникнет исключение NameError
.
Другой подход заключается в использовании lambda
:
>>> from math import factorial
>>> timeit(lambda: factorial(999), number=10)
0.0012704220062005334
Это решение чище, более читабельно и быстрее вводится в интерпретатор. Хотя время выполнения было немного меньше для версии lambda
, повторное выполнение функций может показать небольшое преимущество версии string
. Время выполнения setup
исключено из общего времени выполнения и не должно оказывать влияния на результат.
Патчинг
При тестировании иногда необходимо полагаться на повторяющиеся результаты, даже если при обычном выполнении данного программного обеспечения соответствующие результаты могут отличаться или даже быть совершенно случайными.
Допустим, вы хотите протестировать функцию, которая во время выполнения обрабатывает случайные значения. Но во время выполнения тестирования вам нужно подтвердить предсказуемость значений повторяющимся способом. Следующий пример показывает, как в случае с функцией lambda
вам может помочь обезьяний патч:
from contextlib import contextmanager
import secrets
def gen_token():
"""Generate a random token."""
return f'TOKEN_{secrets.token_hex(8)}'
@contextmanager
def mock_token():
"""Context manager to monkey patch the secrets.token_hex
function during testing.
"""
default_token_hex = secrets.token_hex
secrets.token_hex = lambda _: 'feedfacecafebeef'
yield
secrets.token_hex = default_token_hex
def test_gen_token():
"""Test the random token."""
with mock_token():
assert gen_token() == f"TOKEN_{'feedfacecafebeef'}"
test_gen_token()
Менеджер контекста помогает изолировать операцию обезьяньего исправления функции из стандартной библиотеки (secrets
, в данном примере). Лямбда-функция, назначенная на secrets.token_hex()
, заменяет поведение по умолчанию, возвращая статическое значение.
Это позволяет тестировать любую функцию, зависящую от token_hex()
, предсказуемым образом. Перед выходом из менеджера контекста поведение по умолчанию token_hex()
восстанавливается, чтобы исключить любые неожиданные побочные эффекты, которые могут повлиять на другие области тестирования, которые могут зависеть от поведения по умолчанию token_hex()
.
Unit test frameworks, такие как unittest
и pytest
, поднимают эту концепцию на более высокий уровень сложности.
С pytest
, по-прежнему используя функцию lambda
, тот же пример становится более элегантным и лаконичным :
import secrets
def gen_token():
return f'TOKEN_{secrets.token_hex(8)}'
def test_gen_token(monkeypatch):
monkeypatch.setattr('secrets.token_hex', lambda _: 'feedfacecafebeef')
assert gen_token() == f"TOKEN_{'feedfacecafebeef'}"
С помощью приспособления pytest monkeypatch
, secrets.token_hex()
перезаписывается лямбда, которая вернет детерминированное значение feedfacecafebeef
, позволяющее проверить тест. Фикстура pytest monkeypatch
позволяет контролировать область действия переопределения. В приведенном выше примере вызов secrets.token_hex()
в последующих тестах, без использования обезьяньего патча, выполнит обычную реализацию этой функции.
Выполнение теста pytest
дает следующий результат:
$ pytest test_token.py -v
============================= test session starts ==============================
platform linux -- Python 3.7.2, pytest-4.3.0, py-1.8.0, pluggy-0.9.0
cachedir: .pytest_cache
rootdir: /home/andre/AB/tools/bpython, inifile:
collected 1 item
test_token.py::test_gen_token PASSED [100%]
=========================== 1 passed in 0.01 seconds ===========================
Тест пройден, так как мы убедились, что gen_token()
был выполнен, и результаты были ожидаемыми в контексте теста.
Альтернативы ламбдам
Хотя есть много причин для использования lambda
, есть случаи, когда его использование не одобряется. Каковы же альтернативы?
Функции более высокого порядка, такие как map()
, filter()
и functools.reduce()
, могут быть преобразованы в более элегантные формы с помощью небольшого творческого подхода, в частности, с помощью списочных представлений или генераторных выражений.
Чтобы узнать больше о списочных выражениях, прочитайте статью Когда использовать списочные выражения в Python. Чтобы узнать больше о выражениях-генераторах, ознакомьтесь с Как использовать генераторы и yield в Python.
Map
Встроенная функция map()
принимает функцию в качестве первого аргумента и применяет ее к каждому из элементов своего второго аргумента - итерабельной переменной . Примерами итерируемых элементов являются строки, списки и кортежи. Более подробную информацию об итерациях и итераторах можно найти в разделе Итерации и итераторы.
map()
возвращает итератор, соответствующий преобразованной коллекции. Например, если вы хотите преобразовать список строк в новый список с заглавными буквами, вы можете использовать map()
, как показано ниже:
>>> list(map(lambda x: x.capitalize(), ['cat', 'dog', 'cow']))
['Cat', 'Dog', 'Cow']
Вам нужно вызвать list()
, чтобы преобразовать итератор, возвращаемый map()
, в расширенный список, который можно отобразить в интерпретаторе оболочки Python.
Использование понимания списка устраняет необходимость в определении и вызове лямбда-функции:
>>> [x.capitalize() for x in ['cat', 'dog', 'cow']]
['Cat', 'Dog', 'Cow']
Filter
Встроенная функция
filter()
, еще одна классическая функциональная конструкция, может быть преобразована в списковое представление. Она принимает предикат в качестве первого аргумента и итератор в качестве второго аргумента. Она строит итератор, содержащий все элементы исходной коллекции, которые удовлетворяют предикатной функции. Вот пример, который отфильтровывает все четные числа в заданном списке целых чисел:
>>> even = lambda x: x%2 == 0
>>> list(filter(even, range(11)))
[0, 2, 4, 6, 8, 10]
Обратите внимание, что filter()
возвращает итератор, поэтому необходимо вызвать встроенный тип list
, который строит список, заданный итератором.
Реализация, использующая конструкцию понимания списка, дает следующее:
>>> [x for x in range(11) if x%2 == 0]
[0, 2, 4, 6, 8, 10]
Reduce
Начиная с Python 3, reduce()
превратилась из встроенной функции в функцию модуля functools
. Как и в map()
и filter()
, ее первые два аргумента - это соответственно функция и итерируемый объект. В качестве третьего аргумента она может принимать инициализатор, который используется в качестве начального значения результирующего аккумулятора. Для каждого элемента итерируемого множества reduce()
применяет функцию и накапливает результат, который возвращается, когда итерируемое множество исчерпано.
Чтобы применить reduce()
к списку пар и вычислить сумму первого элемента каждой пары, можно написать так:
>>> import functools
>>> pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> functools.reduce(lambda acc, pair: acc + pair[0], pairs, 0)
6
Более идиоматичный подход, использующий выражение -генератор в качестве аргумента к sum()
в примере, выглядит следующим образом:
>>> pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> sum(x[0] for x in pairs)
6
Несколько иное и, возможно, более чистое решение устраняет необходимость явного доступа к первому элементу пары и вместо этого использует распаковку:
>>> pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
>>> sum(x for x, _ in pairs)
6
Использование подчеркивания (_
) - это соглашение Python, указывающее на то, что второе значение пары можно игнорировать.
sum()
принимает уникальный аргумент, поэтому выражение генератора не нужно заключать в круглые скобки.
Являются ли ламбды "питонячьими" или нет?
PEP 8, который является руководством по стилю для кода Python, гласит:
Всегда используйте оператор def вместо оператора присваивания, который связывает лямбда-выражение непосредственно с идентификатором. (Source)
Это настоятельно не рекомендует использовать лямбды, привязанные к идентификатору, в основном там, где следует использовать функции, которые имеют больше преимуществ. В PEP 8 не упоминаются другие варианты использования lambda
. Как вы видели в предыдущих разделах, лямбда-функции, безусловно, могут иметь хорошее применение, хотя и ограниченное.
Возможный способ ответить на этот вопрос заключается в том, что лямбда-функции вполне питоничны, если нет ничего более питоничного. Я воздерживаюсь от определения того, что значит "питонский", оставляя вам определение, которое лучше всего соответствует вашему менталитету, а также вашему личному стилю кодирования или стилю вашей команды.
За пределами узкой сферы применения Python lambda
, How to Write Beautiful Python Code With PEP 8 - отличный ресурс, который вы, возможно, захотите просмотреть относительно стиля кода в Python.
Заключение
Теперь вы знаете, как использовать функции Python lambda
и можете:
- Пишите лямбды Python и используйте анонимные функции
- Мудро выбирайте между лямбдами и обычными функциями Python
- Избегайте чрезмерного использования лямбд
- Используйте ламбды с функциями высшего порядка или ключевыми функциями Python
Если у вас есть склонность к математике, вы можете получить некоторое удовольствие, исследуя увлекательный мир лямбда-калькуляции.
Ламбды в Python - это как соль. Щепотка соли в спаме, ветчине и яйцах усилит вкус, но слишком много - испортит блюдо.
Back to Top