Функции высшего порядка
Применение функционального подхода с использованием map и filter для трансформации коллекций.
Введение в функциональную парадигму и функции высшего порядка
Добро пожаловать в двадцать четвертый урок нашего курса. Сегодня мы совершим качественный скачок в вашем понимании архитектуры программ на Python. Мы переходим от классического императивного программирования (где мы указываем компьютеру, как именно делать каждый шаг с помощью циклов и условных операторов) к элементам декларативного и функционального программирования (где мы описываем, что мы хотим получить в результате). Функциональное программирование — это парадигма, которая рассматривает вычисления как вычисление математических функций и избегает изменения состояния и мутабельных данных. В основе этой парадигмы в Python лежат так называемые функции высшего порядка (Higher-Order Functions - HOF).
Прежде чем мы перейдем к самим функциям map и filter, нам необходимо осознать один из важнейших принципов Python: функции являются объектами первого класса (First-Class Citizens). Что это означает на практике? В некоторых языках программирования функции — это просто инструкции, жестко привязанные к своему месту в коде. В Python же функция — это такой же объект в оперативной памяти, как число `int`, строка `str` или список `list`. Это открывает колоссальные возможности. Если функция — это объект, значит, мы можем:
- Присваивать функцию переменной.
- Передавать функцию в качестве аргумента в другую функцию.
- Возвращать функцию в качестве результата из другой функции.
- Хранить функции в структурах данных (например, в списках или словарях).
Функция высшего порядка — это логическое продолжение этой концепции. По определению, функцией высшего порядка называется функция, которая принимает одну или несколько функций в качестве аргументов ИЛИ возвращает функцию в качестве результата. Это мощнейший инструмент абстракции. Вместо того чтобы писать один и тот же цикл для обработки данных, мы можем написать одну функцию, которая обходит данные, и передавать ей разные функции для выполнения конкретных действий над элементами. Это делает код невероятно гибким, модульным (принцип DRY - Don't Repeat Yourself) и легко тестируемым. В этом уроке мы применим методики Microlearning и Active Recall, чтобы пошагово и глубоко закрепить эти концепции. Мы начнем с механики передачи функций, а затем перейдем к встроенным функциям высшего порядка: map для трансформации и filter для отсева данных. Понимание этих механизмов критически важно для уровня Intermediate, так как оно подготавливает вас к работе с более сложными концепциями, такими как декораторы, замыкания и асинхронное программирование.
def shout(text):
return text.upper()
def whisper(text):
return text.lower()
# Функция высшего порядка
def greet(func, message):
# Вызываем переданную функцию
result = func(message)
print(f"Результат: {result}")
# Передаем функции как аргументы (без скобок!)
greet(shout, "Привет, мир!")
greet(whisper, "Привет, мир!")
Анатомия функции высшего порядка
Давайте детально разберем код из предыдущего примера. Это классическая иллюстрация работы функции высшего порядка и концепции функций как объектов первого класса. У нас есть две простые базовые функции: shout (которая переводит текст в верхний регистр) и whisper (которая переводит текст в нижний регистр). Обе они принимают одну строку и возвращают новую строку. Обратите внимание, что они ничего не выводят на экран (не используют print), а только возвращают значение, что является хорошей практикой (чистые функции).
Далее мы определяем функцию greet. Посмотрите на ее сигнатуру: def greet(func, message). Первый аргумент называется func. В теле функции greet мы используем этот аргумент со скобками: func(message). Это означает, что greet ожидает, что в качестве func ей будет передан вызываемый объект (callable) — то есть другая функция. Функция greet не знает и не должна знать, что именно делает функция func. Ее задача (ее зона ответственности) — просто вызвать эту функцию, передав ей message, и затем распечатать результат. Это великолепный пример разделения ответственности и инверсии контроля.
Самый важный момент происходит при вызове функции greet. Посмотрите внимательно на строку: greet(shout, "Привет, мир!"). Заметьте, что мы передаем shout без круглых скобок. Если бы мы написали shout(), мы бы вызвали функцию shout прямо там, и в функцию greet передался бы результат ее работы (строка), а не сама функция. Передавая имя функции без скобок, мы передаем ссылку на объект функции в памяти. Функция greet получает эту ссылку, присваивает ее локальной переменной func, и уже внутри себя добавляет скобки func(message), инициируя выполнение кода, расположенного по этому адресу в памяти. Это типичная ошибка новичков — путать вызов функции и передачу самого объекта функции. Запомните: со скобками — вызов (получаем результат), без скобок — объект (передаем инструмент).
Что произойдет, если при передаче функции в качестве аргумента в функцию высшего порядка вы добавите к ее имени круглые скобки? Например: `higher_order(my_func())`
Как называется функция, которая может принимать другие функции в качестве аргументов или возвращать их в качестве результата?
Флеш-карточки
First-Class Citizen (Объект первого класса)
Нажмите, чтобы увидеть ответ
Сущность в программировании, которую можно передавать как аргумент, возвращать из функции и присваивать переменной (в Python все функции таковы).
Нажмите, чтобы вернуться
Higher-Order Function (Функция высшего порядка)
Нажмите, чтобы увидеть ответ
Функция, принимающая другую функцию в качестве аргумента или возвращающая функцию.
Нажмите, чтобы вернуться
Передача функции: func vs func()
Нажмите, чтобы увидеть ответ
func - передача самого объекта функции. func() - немедленный вызов функции и передача её результата.
Нажмите, чтобы вернуться
| Свойство | Обычная функция | Функция высшего порядка |
|---|---|---|
| Аргументы | Принимает скалярные значения, коллекции, объекты (int, str, list) | Может принимать другие функции (callable объекты) |
| Возвращаемое значение | Возвращает данные (строки, числа, коллекции) | Может возвращать новую сгенерированную функцию |
| Уровень абстракции | Низкий: выполняет конкретную бизнес-логику | Высокий: управляет поведением других функций |
| Примеры встроенных | len(), sum(), max() | map(), filter(), sorted() |
Введение в функцию map()
Теперь, когда мы твердо усвоили концепцию функций высшего порядка, мы готовы познакомиться с одним из самых популярных встроенных инструментов Python — функцией map(). Название "map" происходит из математики и означает "отображение". Задача функции map — взять какую-либо коллекцию (или любой итерируемый объект) и применить заданную функцию к каждому элементу этой коллекции по очереди, создав новую коллекцию из результатов. Это идеальная замена для простого цикла for, цель которого — трансформировать данные. Представьте, что у вас есть список цен в рублях, и вам нужно перевести их все в доллары. Или у вас есть список строк с именами, и вам нужно сделать первую букву каждого имени заглавной. Это классические задачи для map.
Синтаксис функции map выглядит следующим образом: map(function, iterable, *iterables). Разберем аргументы. Первый аргумент — это function. Это та самая функция высшего порядка в действии: вы передаете сюда имя функции (без скобок!), которую хотите применить. Эта функция должна принимать как минимум один аргумент. Второй аргумент — iterable. Это любая последовательность или коллекция в Python, по которой можно пройтись циклом (список, кортеж, строка, множество, словарь и т.д.). Важный момент: функция map написана на языке C (под капотом CPython), поэтому она выполняет цикл по итерируемому объекту значительно быстрее, чем эквивалентный цикл for, написанный на чистом Python. Это делает map не только коротким и читаемым способом трансформации данных, но и высокопроизводительным.
Еще одна критически важная особенность: в Python 3 функция map() не возвращает список. Она возвращает специальный объект-итератор (класса map). Это проявление концепции ленивых вычислений (lazy evaluation). Ленивые вычисления означают, что Python не выполняет трансформацию всех элементов сразу в момент вызова функции map. Вместо этого он подготавливает итератор, который будет вычислять и отдавать элементы по одному, только когда вы явно их запросите (например, проходя по объекту циклом for или оборачивая его в list()). Это колоссально экономит оперативную память. Если вы применяете map к файлу из миллиона строк, программа не загрузит в память миллион обработанных строк сразу, а будет держать в памяти только одну текущую строку. Мы подробно рассмотрим механику ленивых вычислений в следующих блоках, а пока посмотрим на базовый пример.
# Задача: возвести все числа списка в квадрат
numbers = [1, 2, 3, 4, 5]
# 1. Определяем функцию трансформации
def square(n):
return n ** 2
# 2. Применяем map (передаем функцию square без скобок)
result_map = map(square, numbers)
# Посмотрим, что внутри
print("Объект map:", result_map)
# Вывод: <map object at 0x...>
# 3. Превращаем итератор обратно в список, чтобы увидеть данные
squared_numbers = list(result_map)
print("Результат:", squared_numbers)
# Вывод: [1, 4, 9, 16, 25]
Что возвращает встроенная функция map() в Python 3?
Глубокий разбор: Ленивые вычисления и исчерпаемость итераторов
Только что мы затронули тему ленивых вычислений. Это концепция настолько важна для уровня Intermediate, что требует отдельного, детального рассмотрения. Когда вы вызываете result_map = map(square, numbers), физически вычисления (возведение в квадрат) еще не происходят. Процессор в этот момент не загружен математическими операциями. Объект map просто запоминает две вещи: какую функцию нужно применить (square) и откуда брать данные (numbers). Он стоит на стартовой линии и ждет команды. Эта архитектура защищает нас от ситуации, известной как "Out of Memory" (нехватка оперативной памяти), когда мы работаем с Big Data (большими данными). Вы можете создать map объект для бесконечной последовательности, и программа не зависнет в ту же секунду.
Когда же происходят вычисления? Они происходят, когда мы начинаем потреблять этот итератор. Самый частый способ потребить итератор — обернуть его в конструктор списка: list(result_map). В этот момент встроенная функция list() начинает дергать итератор map под капотом, вызывая метод __next__(). Итератор берет первое число (1), передает его в функцию square, вычисляет результат (1), отдает его списку, и забывает. Затем берет второе (2), вычисляет (4), отдает. И так до конца коллекции. Этот процесс называется активным извлечением (evaluation).
Но здесь кроется одна из самых коварных ловушек для начинающих разработчиков: итераторы в Python одноразовые (исчерпываемые). Как только вы прошлись по объекту map (например, превратив его в список), он становится пустым. Вы не можете прочитать его дважды. Если вы напишете код: m = map(int, ["1", "2"])
l1 = list(m)
l2 = list(m)
то l1 будет содержать [1, 2], а l2 будет пустым списком []! Это происходит потому, что указатель итератора дошел до конца и выбросил исключение StopIteration. Он не возвращается в начало автоматически. Чтобы пройтись по данным снова, вам нужно создать новый объект map. Понимание этой механики спасет вас от долгих часов дебаггинга, когда ваши данные "неожиданно исчезают" в середине программы.
Как называется концепция, при которой вычисления (например, в функции map) откладываются до момента фактического запроса данных?
Дан код: numbers = ['10', '20', '30'] mapped_nums = map(int, numbers) list_1 = list(mapped_nums) list_2 = list(mapped_nums) Какое значение будет содержать переменная list_2?
Флеш-карточки
map(func, iterable)
Нажмите, чтобы увидеть ответ
Применяет функцию func к каждому элементу iterable. Возвращает ленивый итератор.
Нажмите, чтобы вернуться
Ленивые вычисления (Lazy Evaluation)
Нажмите, чтобы увидеть ответ
Стратегия вычисления, при которой код выполняется только тогда, когда результат действительно требуется. Экономит память.
Нажмите, чтобы вернуться
Исчерпываемость итераторов
Нажмите, чтобы увидеть ответ
Свойство объектов (включая map и filter) в Python: после одного полного прохода они становятся пустыми и не могут быть использованы повторно без пересоздания.
Нажмите, чтобы вернуться
Задание
Практическое задание: Конвертация типов. Вам часто придется получать данные из файлов или сети в виде строк, которые на самом деле являются числами. Используйте map для очистки данных.
- 1. Создайте список строк raw_data = ['42', '15', '8', '100'].
- 2. Создайте переменную clean_data и присвойте ей результат выполнения функции map, которая применит встроенную функцию int к списку raw_data.
- 3. Оберните результат map в list(), чтобы получить список чисел.
- 4. Выведите clean_data на экран и убедитесь, что кавычки вокруг чисел исчезли.
Применение map() со встроенными функциями и методами
До сих пор мы писали собственные функции с помощью def и передавали их в map. Однако сила функционального подхода раскрывается в полной мере, когда мы комбинируем map со встроенными функциями Python и методами классов. Python предоставляет огромный арсенал готовых функций. Мы уже видели пример использования int() для конвертации строк в числа: list(map(int, ["1", "2"])). Обратите внимание, что мы передаем имя встроенного класса int как функцию. В Python классы тоже являются вызываемыми объектами (callables), и при их вызове создается новый экземпляр класса. Аналогично можно использовать float(), str(), bool().
Но что, если мы хотим применить строковый метод к списку строк? Например, у нас есть список имен в нижнем регистре names = ["alice", "bob", "charlie"], и мы хотим сделать первые буквы заглавными, используя метод строки .capitalize(). Мы не можем написать map(capitalize, names), потому что capitalize — это метод конкретного объекта строки, а не самостоятельная функция в глобальной области видимости. Как же быть? В Python методы привязаны к классу. Мы можем обратиться к методу через само имя класса: str.capitalize. Это ссылка на метод (unbound method в старых версиях Python, сейчас просто функция внутри пространства имен класса), которая первым аргументом ожидает сам объект строки (тот самый неявный self). Таким образом, мы можем написать: list(map(str.capitalize, names)). Функция map будет брать каждую строку из names и передавать ее в str.capitalize в качестве аргумента. Это невероятно элегантный, быстрый и "питоничный" (Pythonic) способ массовой обработки строковых данных. Мы экономим строки кода и используем оптимизированные методы языка, написанные на C.
Такой подход применим ко всем встроенным типам. Нужно получить длины всех строк в списке? Используйте встроенную функцию len: map(len, names). Нужно округлить список чисел с плавающей точкой? Используйте map(round, float_numbers). Важно лишь помнить: функция, которую вы передаете в map, должна уметь принимать ровно столько аргументов, сколько элементов она будет получать на каждой итерации. Если вы обходите один список, функция должна принимать один аргумент. Это строгое правило. Если функция требует два аргумента, а получает один, Python выбросит исключение TypeError. О том, как передавать несколько аргументов с помощью map, мы поговорим в следующем разделе.
words = ["hello", "world", "python", "rocks"]
# 1. Использование встроенной функции len()
lengths = list(map(len, words))
print("Длины слов:", lengths) # [5, 5, 6, 5]
# 2. Использование метода класса str.upper
uppercase_words = list(map(str.upper, words))
print("ВЕРХНИЙ РЕГИСТР:", uppercase_words) # ['HELLO', 'WORLD', 'PYTHON', 'ROCKS']
# 3. Использование класса float для конвертации
string_prices = ["9.99", "4.50", "12.00"]
float_prices = list(map(float, string_prices))
print("Цены (float):", float_prices) # [9.99, 4.5, 12.0]
# 4. Получение списка булевых значений (проверка на непустоту)
mixed_data = ["Text", "", 0, 42, None, []]
truth_values = list(map(bool, mixed_data))
print("Булевы значения:", truth_values) # [True, False, False, True, False, False]
У вас есть список email-адресов `emails = ['Admin@mail.com', 'USER@TEST.ORG']`. Вы хотите перевести их все в нижний регистр. Какой код является правильным и наиболее Pythonic?
| Задача | Императивный подход (цикл for) | Декларативный подход (map) |
|---|---|---|
| Округление | res = []\nfor x in nums: res.append(round(x)) | res = list(map(round, nums)) |
| Преобразование в строку | res = []\nfor x in nums: res.append(str(x)) | res = list(map(str, nums)) |
| Очистка от пробелов | res = []\nfor x in strings: res.append(x.strip()) | res = list(map(str.strip, strings)) |
Продвинутый map(): Работа с несколькими итерируемыми объектами
До сих пор мы рассматривали функцию map с одним итерируемым объектом (например, одним списком). Однако сигнатура функции, как мы упоминали ранее, выглядит так: map(function, iterable, *iterables). Наличие звездочки перед *iterables означает, что мы можем передать в map два, три, пять или любое другое количество коллекций через запятую. Как это работает? Механика невероятно элегантна. Если вы передаете несколько итерируемых объектов, функция, переданная первым аргументом, обязана принимать такое же количество аргументов. map будет брать по одному элементу из каждой коллекции параллельно (как если бы они были склеены с помощью zip) и передавать их в вашу функцию.
Рассмотрим реальный сценарий. У вас есть два списка одинаковой длины: список цен на товары и список скидок на эти товары. Вы хотите получить список финальных цен. Вы можете написать функцию calculate_discount(price, discount), которая принимает два аргумента и возвращает price - discount. Затем вы вызываете map(calculate_discount, prices_list, discounts_list). map возьмет первый элемент из prices_list и первый элемент из discounts_list, передаст их в calculate_discount. Затем возьмет вторые элементы и так далее. Это позволяет выполнять сложные параллельные вычисления над наборами данных без использования индексов и громоздких циклов.
Важное примечание о поведении с коллекциями разной длины: Что произойдет, если мы передадим в map два списка, но в одном будет 5 элементов, а в другом — только 3? Будет ли ошибка? Нет. Функция map в Python останавливает свою работу, как только заканчивается самый короткий из переданных итерируемых объектов. Это называется поведением "короткого замыкания" или усечением по кратчайшему (truncation). Элементы из более длинных коллекций просто игнорируются. Это безопасное поведение, которое защищает вас от ошибок выхода за пределы списка (IndexError), но вы должны быть внимательны, если ожидаете, что все элементы длинного списка будут обработаны. Знание этой особенности отличает новичка от разработчика уровня Intermediate.
# Задача: сложить элементы двух списков попарно
list_a = [1, 2, 3, 4]
list_b = [10, 20, 30]
# Определяем функцию для двух аргументов
def add_two_numbers(x, y):
return x + y
# Передаем функцию и оба списка в map
# Обратите внимание: list_a длиннее, чем list_b
result = list(map(add_two_numbers, list_a, list_b))
# Результат: [11, 22, 33]
# Четверка из list_a была проигнорирована, так как list_b закончился
print("Попарная сумма:", result)
# Пример со встроенными функциями (pow - возведение в степень)
bases = [2, 3, 4]
powers = [3, 2, 0.5]
# 2**3=8, 3**2=9, 4**0.5=2.0
results_pow = list(map(pow, bases, powers))
print("Результаты возведения в степень:", results_pow)
Если в map(func, iter1, iter2) передан iter1 длиной 10 элементов и iter2 длиной 5 элементов, какой длины будет результирующий итератор?
Какую встроенную функцию, ожидающую 2 аргумента (основание и показатель степени), можно использовать вместе с map и двумя списками для попарного возведения чисел из первого списка в степень из второго?
Флеш-карточки
map с несколькими аргументами
Нажмите, чтобы увидеть ответ
map(func, iter1, iter2). Функция func обязана принимать количество аргументов, равное количеству переданных итерируемых объектов.
Нажмите, чтобы вернуться
Поведение map с разной длиной коллекций
Нажмите, чтобы увидеть ответ
map останавливается, когда исчерпывается самый короткий итерируемый объект. Остальные элементы игнорируются.
Нажмите, чтобы вернуться
pow() и map()
Нажмите, чтобы увидеть ответ
list(map(pow, [2, 3], [3, 2])) вернет [8, 9]. Идеально для параллельных математических вычислений.
Нажмите, чтобы вернуться
Интеграция анонимных функций (lambda) с map()
До этого момента для передачи логики в функцию map мы создавали полноценные именованные функции с помощью ключевого слова def. Если логика трансформации сложная и занимает несколько строк, это правильный подход: код легко читается и тестируется. Но что, если нам нужно выполнить простейшую операцию? Например, умножить каждое число на 10. Писать для этого отдельную функцию с def, придумывать ей имя (например, multiply_by_ten), размещать ее где-то в коде — это излишняя многословность. В философии Python есть принцип: "Простое лучше, чем сложное". И здесь на сцену выходят lambda-функции (анонимные функции). Они идеально дополняют map и filter.
Lambda-функция — это однострочная функция без имени, которая создается с помощью ключевого слова lambda. Синтаксис: lambda аргументы: выражение. Выражение вычисляется и автоматически возвращается (не нужно писать слово return). Таким образом, комбинация map и lambda позволяет нам создавать и применять функцию прямо в момент вызова ("на лету"). Конструкция list(map(lambda x: x * 10, numbers)) читается как книга: "создай список, применив функцию 'умножить на 10' к каждому 'x' в 'numbers'". Это мощный и лаконичный паттерн функционального программирования. Мы избавляемся от засорения глобального пространства имен мелкими одноразовыми функциями.
Использование lambda с несколькими итерируемыми объектами в map также становится интуитивно понятным. Если мы передаем два списка (например, списки x и y координат), лямбда должна принимать два аргумента. Синтаксис будет выглядеть так: lambda x, y: x + y. Это позволяет описывать сложную логику взаимодействия элементов из разных коллекций прямо в одной строке. Тем не менее, будьте осторожны: если ваше лямбда-выражение становится слишком длинным или требует сложных условных конструкций, лучше вернуться к использованию def. PEP 8 (руководство по стилю кода Python) рекомендует использовать лямбды только для очень простых операций, чтобы не ухудшать читаемость кода.
numbers = [1, 2, 3, 4, 5]
# Использование def (классический подход)
def multiply_by_10(x):
return x * 10
res1 = list(map(multiply_by_10, numbers))
# Использование lambda (функциональный подход)
res2 = list(map(lambda x: x * 10, numbers))
print("Результат с lambda:", res2) # [10, 20, 30, 40, 50]
# Пример lambda с несколькими списками (расчет площади прямоугольников)
widths = [10, 20, 30]
heights = [5, 4, 10]
# lambda принимает два аргумента: w (из widths) и h (из heights)
areas = list(map(lambda w, h: w * h, widths, heights))
print("Площади:", areas) # [50, 80, 300]
# Трансформация словарей внутри списка
users = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 30}]
# Извлекаем только имена
names = list(map(lambda user: user["name"], users))
print("Имена:", names) # ['Alice', 'Bob']
Как правильно написать lambda функцию для использования в map(), которая прибавляет к каждому элементу списка строку "_id"? Список: items = ['user1', 'user2']
Задание
Практическое задание: Извлечение данных с помощью map и lambda. У вас есть список объектов (словарей), представляющих товары в корзине покупок. Вам нужно получить массив всех цен для последующего суммирования.
- 1. Создайте список словарей: cart = [{'product': 'A', 'price': 100}, {'product': 'B', 'price': 250}, {'product': 'C', 'price': 50}].
- 2. Напишите код, используя list(), map() и lambda-функцию.
- 3. Lambda-функция должна принимать один аргумент (товар) и возвращать значение по ключу 'price'.
- 4. Выведите полученный список на экран. Ожидаемый результат: [100, 250, 50].
Введение в функцию filter()
Мы досконально разобрали трансформацию данных с помощью map. Однако в реальных задачах мы часто сталкиваемся с необходимостью не изменить элементы, а отсеять их. Например, найти всех пользователей старше 18 лет, отфильтровать пустые строки из файла логов или выбрать только те товары, которые есть в наличии. Для таких задач в арсенале функционального программирования Python есть функция высшего порядка filter(). Как и map, она работает с итерируемыми объектами и применяет к ним функцию. Но её логика кардинально отличается. Если map отвечает на вопрос "как изменить этот элемент?", то filter отвечает на вопрос "стоит ли оставлять этот элемент?".
Синтаксис filter() очень похож на map(): filter(function, iterable). Разница кроется в том, какую функцию вы должны передать. Функция, передаваемая в filter, должна быть предикатом (predicate). Предикат — это математический термин, обозначающий функцию, которая возвращает булево значение (True или False). Механика работы filter следующая: она берет элемент из iterable, передает его в функцию-предикат. Если функция возвращает True, этот элемент проходит фильтр и сохраняется в результирующем наборе. Если функция возвращает False, элемент отбрасывается. Очень важно понимать: filter не изменяет сами элементы. Она возвращает элементы в их исходном виде. Если вы передали в filter список целых чисел, на выходе вы получите элементы из этого же списка (подмножество), а не строки, словари или что-то еще.
Точно так же, как и map, функция filter в Python 3 использует ленивые вычисления. При вызове filter(func, seq) вычисления не запускаются немедленно. Возвращается специальный объект filter object, который является итератором. Данные фильтруются "на лету", по одному элементу, в момент, когда вы пытаетесь прочитать их из итератора (например, при обертывании в list()). И, как и любой итератор, он одноразовый. Это делает filter крайне эффективным инструментом при работе с гигантскими объемами данных, так как мы не дублируем всю отфильтрованную коллекцию в оперативной памяти.
# Задача: отфильтровать только четные числа
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 1. Создаем функцию-предикат (возвращает True/False)
def is_even(n):
# n % 2 == 0 вернет True для четных, False для нечетных
return n % 2 == 0
# 2. Применяем filter
# Передаем предикат без скобок
even_numbers_iter = filter(is_even, numbers)
print("Объект filter:", even_numbers_iter)
# Вывод: <filter object at 0x...>
# 3. Извлекаем данные в список
even_numbers = list(even_numbers_iter)
print("Отфильтрованные числа:", even_numbers)
# Вывод: [2, 4, 6, 8, 10]
# Использование лямбда-функции для лаконичности
odd_numbers = list(filter(lambda x: x % 2 != 0, numbers))
print("Нечетные числа (через lambda):", odd_numbers)
Какую функцию следует передавать в качестве первого аргумента во встроенную функцию filter()?
Какой логический тип данных (одно слово на английском) должна возвращать функция, переданная в filter(), чтобы элемент остался в итоговой коллекции?
Флеш-карточки
filter(func, iterable)
Нажмите, чтобы увидеть ответ
Возвращает итератор, содержащий те элементы из iterable, для которых функция func вернула True.
Нажмите, чтобы вернуться
Предикат (Predicate)
Нажмите, чтобы увидеть ответ
В программировании - функция, которая отвечает на вопрос «да или нет» и возвращает булево значение (True/False).
Нажмите, чтобы вернуться
Разница map и filter
Нажмите, чтобы увидеть ответ
map изменяет (трансформирует) элементы, сохраняя их количество. filter отсеивает элементы, сохраняя их оригинальное состояние.
Нажмите, чтобы вернуться
Глубокое понимание filter() и Truthiness (истинность)
Функция filter хранит в себе одну скрытую, но невероятно мощную особенность, связанную с концепцией "Truthiness" (истинности) в Python. Чтобы понять её, давайте вспомним, как Python оценивает различные объекты в логическом контексте (например, в условии if). Мы знаем, что число 0, пустая строка "", пустой список [], пустой словарь {} и специальное значение None оцениваются как False (falsy значения). Все остальные объекты (непустые строки, числа отличные от нуля, непустые коллекции) оцениваются как True (truthy значения). Это фундаментальное поведение интерпретатора Python.
Как это связано с filter()? Дело в том, что функция-предикат, которую мы передаем в filter, не обязана возвращать строго булев тип (объект класса bool). Если функция возвращает строку, список или число, filter автоматически оценит это значение на истинность по правилам Python. Если значение оценивается как True, элемент остается. Если как False — отбрасывается. Но и это еще не все. Что произойдет, если мы вообще не передадим функцию в filter? В документации Python сказано, что первый аргумент filter может быть None: filter(None, iterable). Это специальный, оптимизированный случай. Если вместо функции передан None, filter не применяет никакую функцию, а просто проверяет на истинность сами элементы итерируемого объекта.
Это открывает потрясающий "питоничный" способ очистки данных. Представьте, что после парсинга веб-страницы или чтения грязного CSV файла у вас получился список: data = ["apple", "", "banana", None, "cherry", 0, []]. Вам нужно избавиться от всего "пустого" мусора и оставить только реальные данные. Вместо написания громоздких циклов for x in data: if x:..., вы можете написать одну короткую строчку: clean_data = list(filter(None, data)). Функция filter пройдет по списку, увидит пустую строку (False) — отбросит, увидит None (False) — отбросит, увидит "banana" (True) — оставит. Результатом будет идеальный чистый список ["apple", "banana", "cherry"]. Этот трюк настолько эффективен, что его должен знать каждый Python разработчик, претендующий на уровень Intermediate. Это демонстрация того, как глубокое знание механизмов языка позволяет писать лаконичный и быстрый код.
# Очистка "грязных" данных с помощью filter(None, ...)
mixed_data = ["Valid text", "", 0, 42, None, False, [1, 2], [], "Hello"]
# Использование None вместо функции-предиката
# Оставит только объекты, которые оцениваются как True
clean_data = list(filter(None, mixed_data))
print("Исходные данные:", mixed_data)
print("Очищенные данные:", clean_data)
# Вывод: ['Valid text', 42, [1, 2], 'Hello']
# Пример с функцией, которая не возвращает строго bool
def string_has_content(s):
# Возвращает длину строки (число), а не True/False
# Число > 0 оценивается как True, 0 - как False
return len(s)
words = ["apple", "", "pie", ""]
valid_words = list(filter(string_has_content, words))
print("Слова с содержимым:", valid_words)
# Вывод: ['apple', 'pie']
Что будет результатом выполнения кода: list(filter(None, [1, 0, 2, False, '', 'Python', []]))?
| Значение элемента | Оценка Python (Truthiness) | Пройдет ли filter(None, ...) |
|---|---|---|
| "Привет" | True | Да (останется) |
| "" | False | Нет (отбросится) |
| 0 | False | Нет (отбросится) |
| -42 | True | Да (останется) |
| [1, 2, 3] | True | Да (останется) |
| None | False | Нет (отбросится) |
Комбинирование map() и filter(): Функциональные конвейеры
До сих пор мы рассматривали map и filter по отдельности. Однако их истинная мощь раскрывается при комбинировании. В функциональном программировании принято строить так называемые "конвейеры данных" (data pipelines). Идея заключается в том, что данные проходят через ряд независимых шагов: сначала они фильтруются, затем трансформируются, затем снова фильтруются и так далее. Поскольку и map, и filter возвращают итераторы (в Python 3) и принимают итерируемые объекты в качестве аргументов, мы можем вкладывать их друг в друга или передавать результат одной функции на вход другой. При этом, благодаря ленивым вычислениям, элементы будут обрабатываться по одному, проходя через весь конвейер, без создания промежуточных списков в памяти. Это эталон эффективности памяти.
Давайте рассмотрим типичную задачу. У нас есть база данных пользователей (список словарей). Каждый словарь содержит имя, возраст и статус подписки. Нам нужно получить список имен всех пользователей старше 18 лет, у которых активна подписка, и при этом имена должны быть переведены в верхний регистр. Решение в императивном стиле потребовало бы создания пустого списка, цикла for, внутри которого было бы сложное условие if, извлечение ключа словаря, вызов метода upper() и метод append(). Это процедурный, многословный код. Как мы решим это в функциональном стиле?
Мы разобьем задачу на шаги. Шаг 1: отфильтровать пользователей по возрасту и подписке с помощью filter() и лямбда-функции. Шаг 2: применить map() к результату filter(), чтобы извлечь имя пользователя и перевести его в верхний регистр. Синтаксически мы можем вложить вызов filter внутрь map. Читать такой код нужно изнутри наружу: сначала выполняется внутренняя фильтрация (подготавливается итератор), а затем внешняя трансформация. Выглядит это примерно так: map(transform_func, filter(predicate_func, data)). Это классический паттерн Map-Filter-Reduce (мы рассмотрим Reduce в другом уроке). Главное преимущество такого подхода — четкое разделение логики. Одно действие (фильтрация) не смешано с другим (трансформация данных). Код становится модульным и декларативным.
users = [
{"name": "alice", "age": 17, "active": True},
{"name": "bob", "age": 25, "active": False},
{"name": "charlie", "age": 30, "active": True},
{"name": "diana", "age": 19, "active": True}
]
# Шаг 1: Фильтрация (возраст >= 18 и active == True)
# Результатом будет итератор словарей
filtered_users = filter(lambda u: u["age"] >= 18 and u["active"], users)
# Шаг 2: Трансформация (берем имя и делаем upper())
# Применяем map к результату filter
final_names = map(lambda u: u["name"].upper(), filtered_users)
# Шаг 3: Потребление итератора
result = list(final_names)
print("Активные совершеннолетние пользователи:", result)
# Вывод: ['CHARLIE', 'DIANA']
# Вариант "в одну строку" (вложенный):
# Читается изнутри (filter) наружу (map)
result_inline = list(map(lambda u: u["name"].upper(), filter(lambda u: u["age"] >= 18 and u["active"], users)))
print("Однострочный результат:", result_inline)
Какой архитектурный принцип позволяет эффективно (с точки зрения памяти) использовать вложенные конструкции вида map(func, filter(pred, data)) в Python 3?
Задание
Проектная задача: Анализ датчиков температуры. Вы получаете сырые данные с датчиков в виде строк. Некоторые датчики сломались и шлют слово 'ERROR'. Вам нужно найти все корректные температуры, которые выше 25 градусов, и перевести их в фаренгейты (формула: C * 9/5 + 32).
- 1. Данные: raw_temps = ['20', '28', 'ERROR', '15', '30', 'ERROR', '26']
- 2. Используйте filter(), чтобы отсеять 'ERROR'.
- 3. Используйте map(), чтобы перевести оставшиеся строки в числа (int).
- 4. Используйте filter(), чтобы оставить числа > 25.
- 5. Используйте map(), чтобы перевести их в фаренгейты.
- 6. *Совет: можете разбить на шаги (отдельные переменные) для читаемости.
map/filter против Генераторов списков (List Comprehensions)
Начиная с Python 2.0 в язык были добавлены генераторы списков (List Comprehensions). Это синтаксический сахар, заимствованный из математики и функционального языка Haskell. Вы уже знакомы с ними: [x * 2 for x in data if x > 10]. Это элегантный, очень питоничный способ объединить фильтрацию (условие if) и трансформацию (выражение x * 2) в одной строке кода. В связи с этим в сообществе Python уже много лет не утихают споры: что лучше использовать — связку map + filter или генераторы списков? Создатель Python Гвидо ван Россум является сторонником генераторов списков и даже когда-то предлагал убрать map и filter из Python 3, но сообщество настояло на их сохранении. Давайте разберем плюсы и минусы обоих подходов, чтобы вы могли принимать архитектурные решения как Senior разработчик.
Читаемость (Readability): Для многих разработчиков генераторы списков выглядят более естественно. Конструкция [x.upper() for x in words if x.startswith('a')] читается как обычное английское предложение. Вложенная конструкция map(lambda x: x.upper(), filter(lambda x: x.startswith('a'), words)) выглядит более громоздко из-за обилия скобок и ключевых слов lambda. В случаях, когда вам нужно написать сложную лямбду, генератор списка почти всегда выигрывает в читаемости. Однако, если у вас уже есть готовые именованные функции (например, встроенная str.upper), map выглядит чище: map(str.upper, words).
Производительность (Performance): В скорости выполнения есть тонкие нюансы. Если вы используете встроенную функцию на C (например, map(len, words)), то map отработает быстрее генератора списка [len(w) for w in words], потому что map не тратит время на создание и вызов цикла на уровне байт-кода Python. Но если вы используете lambda с map (например, map(lambda x: x*2, nums)), генератор списка [x*2 for x in nums] будет быстрее, так как вызов функции lambda на каждой итерации добавляет существенные накладные расходы.
Потребление памяти: Здесь map и filter выигрывают с разгромным счетом, если речь идет о больших данных. Генератор списка (в квадратных скобках) вычисляет всё сразу и помещает готовый список в память. map и filter являются ленивыми итераторами. Справедливости ради, в Python есть генераторные выражения (в круглых скобках), которые также ленивы: (x * 2 for x in nums). Они объединяют читаемость comprehensions и ленивость map. Выбор остается за командой и стандартами вашего проекта (PEP 8).
| Критерий | map() и filter() | List/Generator Comprehensions |
|---|---|---|
| Синтаксис для простых операций | Часто требует громоздких lambda | Естественный, математический синтаксис |
| Со встроенными функциями (int, len) | Максимально лаконично: map(int, data) | Чуть длиннее: [int(x) for x in data] |
| Производительность с лямбдой | Медленнее (накладные расходы на вызов) | Быстрее (вычисляется на уровне байт-кода) |
| Работа с памятью | Ленивые вычисления по умолчанию | Ленивые только при использовании () вместо [] |
В каком из следующих случаев использование map() будет наиболее оправданным и быстрым по сравнению с генератором списка (list comprehension)?
Перепишите выражение list(map(lambda x: x**2, numbers)) с использованием генератора списка (list comprehension). Напишите код внутри скобок: [ ... ]
Продвинутые техники: Замыкания (Closures) и функции высшего порядка
Мы изучили map и filter как функции, принимающие другие функции. Теперь давайте кратко затронем вторую часть определения функции высшего порядка: функцию, которая возвращает функцию. Эта концепция лежит в основе одной из самых красивых архитектурных возможностей Python — замыканий (closures) и декораторов. Представьте ситуацию: вам нужна функция, которая умножает число на 2. Затем вам нужна функция, которая умножает на 3, на 4, на 5... Писать десятки функций (multiply_by_2, multiply_by_3) глупо. Что, если мы напишем функцию высшего порядка, которая создает умножители?
Мы можем определить функцию make_multiplier(n). Внутри нее мы определяем внутреннюю функцию multiplier(x), которая возвращает x * n. Функция make_multiplier просто возвращает объект функции multiplier. Когда мы вызываем times_two = make_multiplier(2), переменная times_two становится полноценной функцией, которая "помнит" значение n = 2. Это свойство памяти внутренней функции о переменных внешней области видимости называется замыканием. Теперь мы можем использовать эту сгенерированную функцию вместе с map. Мы пишем list(map(times_two, [1, 2, 3])). Это вершина функционального стиля: мы динамически сгенерировали логику (функцию), а затем применили эту логику к коллекции данных. Такой подход делает код невероятно модульным. Вы отделяете конфигурацию логики от её выполнения на данных.
Завершая наш глубокий погружение в функциональное программирование с использованием map и filter, важно сделать вывод. Python — это мультипарадигмальный язык. Он позволяет вам писать объектно-ориентированный код (классы), процедурный код (циклы и функции) и функциональный код. Лучшие программисты не становятся фанатиками одной парадигмы. Они знают сильные стороны каждой. Используйте map(str, nums) для конвертации типов — это питонично. Используйте filter(None, data) для очистки мусора — это гениально. Используйте генераторы списков, если логика сложна. И используйте замыкания, если вам нужны настраиваемые функции для конвейеров обработки данных. Практикуйтесь с этими инструментами, и вы увидите, как ваш код станет более кратким, быстрым и элегантным.
# Функция высшего порядка, ВОЗВРАЩАЮЩАЯ функцию (Фабрика функций)
def make_multiplier(n):
# Внутренняя функция "замыкает" в себе значение n
def multiplier(x):
return x * n
return multiplier # Возвращаем саму функцию, без скобок
# Генерируем нужные нам функции
times_2 = make_multiplier(2)
times_10 = make_multiplier(10)
numbers = [1, 2, 3, 4, 5]
# Применяем сгенерированную функцию в map()
result_2 = list(map(times_2, numbers))
result_10 = list(map(times_10, numbers))
print("Умножение на 2:", result_2) # [2, 4, 6, 8, 10]
print("Умножение на 10:", result_10) # [10, 20, 30, 40, 50]
# Тот же концепт с lambda (более коротко)
def make_adder(n):
return lambda x: x + n
add_5 = make_adder(5)
print("Прибавление 5:", list(map(add_5, numbers))) # [6, 7, 8, 9, 10]
Что такое замыкание (closure) в контексте функций высшего порядка в Python?
Флеш-карточки
Фабрика функций (Function Factory)
Нажмите, чтобы увидеть ответ
Функция высшего порядка, которая динамически создает и возвращает новые функции на основе переданных параметров.
Нажмите, чтобы вернуться
Замыкание (Closure)
Нажмите, чтобы увидеть ответ
Механизм, при котором функция 'запоминает' переменные окружения, в котором она была создана.
Нажмите, чтобы вернуться
Мультипарадигмальность Python
Нажмите, чтобы увидеть ответ
Способность языка поддерживать ООП, процедурное и функциональное программирование в рамках одного проекта.
Нажмите, чтобы вернуться