23. Функции первого класса и функциональные техники
В предыдущих главах мы изучили, как определять и вызывать функции, работать с параметрами и аргументами, а также понимать область видимости переменных. Теперь мы рассмотрим мощную возможность, которая выделяет Python: функции — это объекты первого класса. Это означает, что с функциями можно обращаться как с любым другим значением: хранить в переменных, передавать в качестве аргументов другим функциям и возвращать из функций.
Эта возможность открывает элегантные техники программирования, которые делают код более гибким, переиспользуемым и выразительным. Мы рассмотрим, как использовать функции первого класса на практических примерах, разберём замыкания (функции, которые «помнят» своё окружение), применим lambda-выражения для кратких определений функций и используем встроенные функции вроде map(), filter(), any() и all() для эффективной работы с коллекциями.
23.1) Функции как объекты первого класса
23.1.1) Что означает «первый класс»
В Python функции — это объекты первого класса, а значит, их можно:
- Присваивать переменным
- Хранить в структурах данных (списках, словарях и т. д.)
- Передавать как аргументы другим функциям
- Возвращать как значения из других функций
Это отличается от некоторых языков программирования, где функции имеют особый статус и ими нельзя манипулировать как обычными значениями. В Python функция — это просто ещё один тип объекта, похожий на целые числа, строки или списки.
Посмотрим на это в действии:
# Определим простую функцию
def greet(name):
return f"Hello, {name}!"
# Присвоим функцию переменной
say_hello = greet
# Вызовем функцию через новую переменную
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# Проверим, что оба имени ссылаются на одну и ту же функцию
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: TrueОбратите внимание: когда мы пишем say_hello = greet, мы не вызываем функцию (нет круглых скобок). Мы создаём новое имя, которое ссылается на тот же объект функции. Теперь и greet, и say_hello указывают на одну и ту же функцию, что можно проверить с помощью оператора is.
23.1.2) Хранение функций в структурах данных
Поскольку функции — это объекты, мы можем хранить их в списках, словарях или любой другой коллекции:
# Калькулятор с операциями, сохранёнными в словаре
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
# Сохраним функции в словаре
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# Используем словарь для выполнения вычислений
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50Этот шаблон чрезвычайно полезен при построении гибких систем. Вместо того чтобы писать длинные цепочки операторов if-elif, чтобы выбрать, какую функцию вызвать, мы можем найти подходящую функцию в словаре и вызвать её напрямую.
23.2) Передача функций в качестве аргументов
23.2.1) Базовая концепция
Одно из самых мощных применений функций первого класса — передавать их как аргументы другим функциям. Это позволяет писать гибкий, переиспользуемый код, который может работать с разным поведением.
Вот простой пример:
# Функция, которая применяет другую функцию к значению
def apply_operation(value, operation):
"""Применить функцию operation, полученную как параметр, к value."""
return operation(value)
# Разные операции
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# Используем одну и ту же функцию apply_operation с разными операциями
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5Функция apply_operation не знает и не заботится о том, какую именно операцию она выполняет. Она просто вызывает любую функцию, которую ей передали. Такое разделение ответственности делает код более модульным и его проще расширять.
23.2.2) Обработка коллекций с пользовательскими функциями
Распространённый шаблон — обрабатывать каждый элемент коллекции, используя функцию, переданную как аргумент:
# Обрабатываем каждый элемент списка с помощью заданной функции
def process_list(items, processor):
"""Применить функцию processor к каждому элементу в списке."""
results = []
for item in items:
results.append(processor(item))
return results
# Разные функции обработки
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# Обрабатываем один и тот же список разными способами
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]Этот шаблон настолько полезен, что Python предоставляет встроенные функции вроде map() и filter(), которые работают таким образом (мы рассмотрим их в разделе 23.6).
23.2.3) Сортировка с передачей key-функции (краткое введение)
Функция Python sorted() принимает параметр key — функцию, которая определяет, как сравнивать элементы:
# Сортируем студентов по разным критериям
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# Функция для извлечения оценки
def get_grade(student):
return student["grade"]
# Функция для извлечения имени
def get_name(student):
return student["name"]
# Сортировка по оценке (по возрастанию)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# Сортировка по имени (в алфавитном порядке)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95Функция key вызывается один раз для каждого элемента, а её возвращаемое значение используется для сравнения. Это намного гибче, чем необходимость писать кастомную логику сортировки.
Этот шаблон передачи функций для настройки поведения чрезвычайно распространён в Python. Более продвинутые техники сортировки мы рассмотрим в главе 38.
23.3) Возврат функций из функций
23.3.1) Функции, которые создают функции
Так же как мы можем передавать функции как аргументы, мы можем и возвращать функции из других функций. Это позволяет динамически создавать специализированные функции:
# Функция, которая создаёт и возвращает новую функцию
def create_multiplier(factor):
"""Создать функцию, которая умножает на заданный factor."""
def multiplier(x):
return x * factor
return multiplier
# Создаём специализированные функции-умножители
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# Используем созданные функции
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50Что здесь происходит? Функция create_multiplier определяет внутреннюю функцию с именем multiplier и возвращает её. Каждый раз, когда мы вызываем create_multiplier с разным множителем, мы получаем новую функцию, которая «помнит» именно этот множитель. Это наш первый взгляд на замыкания, которые мы подробно рассмотрим в следующем разделе.
23.3.2) Создание настраиваемых валидаторов
Возврат функций особенно полезен для создания настраиваемых функций валидации или обработки:
# Динамически создаём валидаторы диапазона
def create_range_validator(min_value, max_value):
"""Создать функцию, которая проверяет, находится ли число в диапазоне."""
def validator(number):
return min_value <= number <= max_value
return validator
# Создаём конкретные валидаторы
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# Используем валидаторы
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) Понимание замыканий: функции, которые помнят
23.4.1) Что такое замыкание?
Замыкание(closure) — это функция, которая «помнит» переменные из области видимости, где она была создана, даже после того, как выполнение этой области видимости завершилось. В примерах из раздела 23.3 мы уже использовали замыкания, не называя их явно.
Рассмотрим, как работают замыкания:
def create_counter(start=0):
"""Создать функцию-счётчик, которая помнит своё значение."""
count = start # Эта переменная «захватывается» замыканием
def counter():
nonlocal count # Доступ к захваченной переменной
count += 1
return count
return counter
# Создаём два независимых счётчика
counter1 = create_counter(0)
counter2 = create_counter(100)
# Каждый счётчик хранит собственное значение
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)Внутренняя функция counter образует замыкание над переменной count. Хотя create_counter уже завершила выполнение, возвращённая функция counter по-прежнему имеет доступ к count. Каждый вызов create_counter создаёт новое независимое замыкание со своей переменной count.
23.4.2) Как замыкания захватывают переменные
Когда функция определяется внутри другой функции, она может получать доступ к переменным из области видимости внешней функции. Эти переменные «захватываются» и остаются доступными даже после того, как внешняя функция вернётся:
Когда Python создаёт внутреннюю функцию, он сохраняет не только код функции — он также сохраняет ссылки на любые переменные из внешней функции, которые использует внутренняя функция. Этот процесс называется «захватом» переменных.
def create_greeter(greeting):
"""Создать функцию приветствия с пользовательским приветствием."""
def greet(name):
return f"{greeting}, {name}!"
return greet
# Создаём разные функции приветствия
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# Каждая функция приветствия «помнит» своё конкретное приветствие
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!Параметр greeting захватывается замыканием. У каждой функции приветствия есть своё захваченное значение greeting, которое она использует при каждом вызове.
23.4.3) Практическое применение: конфигурационные функции
Замыкания отлично подходят для создания функций с заранее настроенным поведением:
# Создаём калькуляторы цен с разными налоговыми ставками
def create_price_calculator(tax_rate):
"""Создать калькулятор, который применяет конкретную tax_rate."""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# Создаём калькуляторы для разных регионов
us_calculator = create_price_calculator(0.07) # 7% tax
uk_calculator = create_price_calculator(0.20) # 20% VAT
japan_calculator = create_price_calculator(0.10) # 10% consumption tax
# Рассчитываем цены в разных регионах
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) Когда использовать замыкания
Замыкания особенно полезны, когда вам нужно:
- Создавать функции с заранее настроенным поведением
- Хранить состояние между вызовами функций без использования классов
- Реализовывать callback-функции, которым нужно помнить контекст
- Создавать фабрики функций, которые производят специализированные функции
23.5) Использование lambda для коротких анонимных функций
23.5.1) Что такое lambda-выражения?
lambda-выражение создаёт маленькую анонимную функцию — функцию без имени. Lambda-выражения полезны, когда вам нужна простая функция на короткое время и вы не хотите формально определять её через def.
Синтаксис:
lambda parameters: expressionLambda принимает параметры (как обычная функция) и возвращает результат вычисления выражения. Вот простой пример:
# Обычная функция
def add(x, y):
return x + y
# Эквивалентное lambda-выражение
add_lambda = lambda x, y: x + y
# Оба варианта работают одинаково
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8Lambda-выражения ограничены одним выражением — они не могут содержать операторы вроде if, for или несколько строк кода. Это ограничение делает их простыми и сфокусированными.
23.5.2) Lambda-выражения как аргументы
Lambda-выражения особенно хороши, когда нужно передать простую функцию как аргумент и не хочется определять отдельную именованную функцию:
# Сортируем студентов по оценке с использованием lambda
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Вместо определения отдельной функции:
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# Можно использовать lambda напрямую:
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95Так получается более лаконично, когда функция проста и используется только один раз. Lambda lambda student: student["grade"] эквивалентна функции, которая принимает студента и возвращает его оценку.
23.5.3) Lambda с несколькими параметрами
Lambda-выражения могут принимать несколько параметров, как и обычные функции:
# Операции калькулятора с использованием lambda
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# Используем lambda-выражения
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: ErrorОбратите внимание: мы можем использовать условное выражение (x / y if y != 0 else "Error") внутри lambda, но не можем использовать оператор if (для него потребовалось бы несколько строк).
23.5.4) Когда использовать lambda, а когда именованные функции
Используйте lambda-выражения, когда:
- Функция очень проста (одно выражение)
- Функция используется только один раз или в очень локальном контексте
- Определение именованной функции добавило бы ненужную многословность
Используйте именованную функцию, когда:
- Функция сложная или требует нескольких операторов
- Функция будет переиспользоваться в нескольких местах
- Функции нужно описательное имя для ясности
- Функции нужен docstring
23.5.5) Ограничения lambda и альтернативы
У lambda-выражений есть важные ограничения:
# ❌ Это не сработает — lambda не может содержать операторы
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ Вместо этого используйте условное выражение
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ Для нескольких операций используйте обычную функцию
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10Lambda-выражения — это инструмент для конкретных ситуаций. Когда они делают код понятнее и лаконичнее — используйте их. Когда они делают код труднее для понимания — вместо этого используйте обычную именованную функцию.
23.6) Использование map() и filter() с простыми функциями
23.6.1) Функция map()
Функция map() применяет заданную function к каждому элементу итерируемого объекта (например, списка, кортежа или строки) и возвращает итератор с результатами. Это способ преобразовать каждый элемент коллекции без написания явного цикла.
map(function, iterable, *iterables)Параметры:
function(обязательно): Функция, которая принимает один или несколько аргументов, обрабатывает их и возвращает значение. Функция вызывается один раз для каждого элемента вiterable(s).iterable(обязательно): Последовательность (список, кортеж, строка и т. д.), элементы которой будут переданы вfunction.*iterables(необязательно): Дополнительные итерируемые объекты дляfunction, принимающей несколько аргументов.
Если передано несколько итерируемых объектов, функция должна принимать соответствующее число аргументов
map() остановится, когда закончится самый короткий итерируемый объект
Возвращает:
Объект map (итератор), содержащий результаты, возвращённые function для каждого входного элемента.
Важно: объект map — это итератор, а не последовательность вроде list.
# Удвоим каждое число в списке
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# Применим double к каждому числу
doubled = map(double, numbers)
result = list(doubled) # Преобразуем объект map (итератор) в список
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) Использование map() с lambda
Lambda-выражения отлично подходят для map() при простых преобразованиях:
# Преобразуем температуру из Цельсия в Фаренгейт
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) Функция filter()
Функция filter() применяет заданную function к каждому элементу iterable и возвращает итератор, содержащий только те элементы, для которых функция возвращает True. Это способ отбирать элементы из коллекции без написания явного цикла.
filter(function, iterable)Параметры:
function: Функция, которая принимает один аргумент, проверяет его и возвращаетTrueилиFalse. Функция вызывается один раз для каждого элемента вiterable.iterable: Последовательность (список, кортеж, строка и т. д.), элементы которой будут проверятьсяfunction.
Возвращает:
Объект filter (итератор), содержащий только те элементы, для которых function вернула True.
Важно: объект filter — это итератор, а не последовательность вроде списка.
Пример:
# Оставим только чётные числа
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# Применим is_even к каждому числу и оставим только те, для которых вернётся True
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # Преобразуем объект filter в список
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) Использование filter() с lambda
Lambda-выражения часто используют с filter() для краткой фильтрации:
# Отфильтруем студентов, которые сдали (grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) Комбинирование map() и filter()
Вы можете объединять операции map() и filter(), чтобы выполнять более сложные преобразования:
# Получим квадраты чётных чисел
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Сначала отфильтруем чётные числа, затем возведём их в квадрат
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]Визуальное сравнение: map() vs filter()
Ключевые различия:
map(): Применяет функцию, чтобы преобразовать каждый элемент → выход имеет ту же длинуfilter(): Проверяет каждый элемент и оставляет только прошедшие проверку → выход имеет равную или меньшую длину
В этой главе мы рассмотрели мощные возможности Python для функционального программирования. Мы узнали, что функции являются объектами первого класса и могут передаваться так же, как любое другое значение, что позволяет создавать гибкие и переиспользуемые шаблоны кода. Мы увидели, как функции могут возвращать другие функции, создавая замыкания, которые помнят своё окружение. Мы изучили lambda-выражения для кратких определений функций и использовали map() и filter() для элегантной обработки коллекций.
Эти концепции формируют основу для продвинутых техник программирования на Python. В главе 38 мы развим эти знания, чтобы освоить декораторы, одну из самых элегантных возможностей Python.