Полезные конструкции Python, которые упростят работу с данными

от автора

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

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

Игорь Мартюшев

Бэкенд-разработчик на Python. Помог с подготовкой этого материала.

Базовые приёмы

zip() — объединение элементов из нескольких коллекций

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

Первое, что приходит в голову новичкам, — использовать индексы через range(len(...)):

players = ["Сергей", "Антон", "Михаил"] teams = ["Волки", "Тигры", "Ястребы"] scores = [15, 20, 17]  for i in range(len(players)):     print(f"{players[i]} ({teams[i]}) — {scores[i]} очков")

Результат:

Сергей (Волки) — 15 очков Антон (Тигры) — 20 очков Михаил (Ястребы) — 17 очков

Так можно, но это не самый читаемый вариант, потому что приходится следить и за индексами, и за данными. А если один список окажется короче, можно получить IndexError.

Вместо индексов можно использовать zip(). Он сам пройдёт по спискам синхронно и вернёт кортежи:

for player, team, score in zip(players, teams, scores):     print(f"{player} ({team}) — {score} очков")

Результат:

Сергей (Волки) — 15 очков Антон (Тигры) — 20 очков Михаил (Ястребы) — 17 очков

Получим то же самое, но код при этом читается проще, а ошибок при разной длине списков не будет: zip() просто остановится на самом коротком.

Объединяем строки и числа

zip() работает не только со списками, но и с любыми итерируемыми объектами. Например, пронумеруем буквы:

letters = "Python" positions = range(1, 7)  for letter, pos in zip(letters, positions):     print(f"{pos}: {letter}")

Результат:

1: P 2: y 3: t 4: h 5: o 6: n

Создаём словарь «ключ-значение»

Если у нас есть два списка, например даты матчей и количество зрителей, их можно объединить в словарь:

dates = ["2025-08-01", "2025-08-02", "2025-08-03"] viewers = [1200, 980, 1430]  attendance = dict(zip(dates, viewers)) print(attendance)

Результат:

{'2025-08-01': 1200, '2025-08-02': 980, '2025-08-03': 1430}

Разделяем данные на части

zip() умеет работать и «в обратную сторону». Если у нас есть координаты в виде пар (x, y), можно получить отдельные коллекции X и Y.

points = [(2, 5), (4, 8), (1, 3)] x_coords, y_coords = zip(*points)  print(x_coords)  # (2, 4, 1) print(y_coords)  # (5, 8, 3)

Здесь * — это оператор распаковки списка аргументов в функцию.

Результат: 

(2, 4, 1) (5, 8, 3)

enumerate() — перебор с индексами

Иногда при работе с данными важно знать не только сам элемент, но и его положение в последовательности. Это может пригодиться в самых разных сценариях, например:

  • при поиске ошибок в логах, когда нужно вывести строку вместе с её номером в файле;

  • при анализе данных, когда важно не потерять исходный порядок записей;

  • при отладке алгоритма, чтобы видеть, на каком шаге что-то пошло не так.

Самый простой способ сделать — завести цикл с range(len(...)) и доставать элемент по индексу:

log = [     "INFO: старт системы",     "WARNING: низкий заряд батареи",     "ERROR: модуль датчика недоступен",     "INFO: перезапуск",     "ERROR: превышен лимит памяти" ]  for i in range(len(log)):     if log[i].startswith("ERROR"):         print(f"Строка {i + 1}: {log[i]}")

Результат:

Строка 3: ERROR: модуль датчика недоступен Строка 5: ERROR: превышен лимит памяти

Но у этого подхода есть минусы: приходится отдельно получать длину, вручную обращаться к элементам по индексу и прибавлять 1, если хочешь начать не с нуля. А с генераторами и другими неиндексируемыми объектами он вообще не сработает.

Тот же пример с enumerate():

for line_num, entry in enumerate(log, start=1):     if entry.startswith("ERROR"):         print(f"Строка {line_num}: {entry}")

Результат:

Строка 3: ERROR: модуль датчика недоступен Строка 5: ERROR: превышен лимит памяти

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

В этом примере индекс (line_num) указывает точное место ошибки в логе, а не просто служит для нумерации.

Сохранение исходного порядка при сортировке

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

Пример. У нас есть список товаров с их ценами, и мы хотим отсортировать их по убыванию цены, но при этом знать, на каком месте они были изначально в каталоге.

products = ["Хлеб", "Сыр", "Молоко", "Шоколад"] prices = [50, 320, 90, 150]  indexed_prices = list(enumerate(prices)) indexed_prices.sort(key=lambda x: x[1], reverse=True)  for index, price in indexed_prices:     print(f"{products[index]} (позиция {index + 1}): {price} ₽")

Результат:

Сыр (позиция 2): 320 ₽ Шоколад (позиция 4): 150 ₽ Молоко (позиция 3): 90 ₽ Хлеб (позиция 1): 50 ₽

Здесь используется анонимная функция lambda (про неё подробно расскажем ниже). Она берёт второй элемент кортежа и возвращает его для сортировки.

Обновление части элементов на месте

Если нужно изменить не все элементы, а только часть по определённым правилам, индекс от enumerate() меняет значения прямо в оригинальном списке, не создавая копию.

Пример. У нас есть список цен в магазине, и мы хотим поднять на 10% только те, что ниже 100 рублей.

prices = [50, 120, 80, 200, 90]  for i, price in enumerate(prices):     if price < 100:         prices[i] = round(price * 1.1, 2)  print(prices)

Результат:

[55.0, 120, 88.0, 200, 99.0]

Мы перебираем элементы вместе с индексами, проверяем условие и сразу обновляем нужные значения в исходном списке.

Списковые включения (list comprehension) — генерация и фильтрация списков

Один из самых частых шаблонов — создать список на основе другого списка или любого итерируемого объекта, возможно с фильтрацией или преобразованием элементов. Самый прямой способ — пройтись по исходным данным в цикле for, добавляя новые элементы в пустой список.

Например, у нас есть список цен и мы хотим перевести их в доллары по курсу:

prices_rub = [1200, 850, 3100] prices_usd = []  for price in prices_rub:     prices_usd.append(round(price / 90, 2))  print(prices_usd)

Результат:

[13.33, 9.44, 34.44]

Мы заводим пустой список, вызываем .append() и пишем лишние строки, хотя задача простая.

Тот же пример через list comprehension. Списковое включение делает то же самое в одну строку:

prices_rub = [1200, 850, 3100] prices_usd = [round(price / 90, 2) for price in prices_rub]  print(prices_usd)

Результат:

[13.33, 9.44, 34.44]

Внутри квадратных скобок описывается сразу и цикл, и логика преобразования.

Фильтрация элементов

Включения поддерживают условие if для фильтрации. Например, оставим только цены выше 1 000 ₽ и сразу переведём их в доллары:

prices_rub = [1200, 850, 3100] high_prices_usd = [round(p / 90, 2) for p in prices_rub if p > 1000]  print(high_prices_usd)

Результат:

[13.33, 34.44]

Генерация списков по формуле

List comprehension полезны не только для работы с готовыми данными, но и для генерации. Например, создадим список всех квадратов чисел от 1 до 10:

squares = [n**2 for n in range(1, 11)] print(squares)

Результат:

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Вложенные циклы

Внутри включения можно писать и несколько циклов. Например, получим все координаты на маленькой сетке 2×3:

coords = [(x, y) for x in range(1, 3) for y in range(1, 4)] print(coords)

Результат:

[(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)]

Общая рекомендация. List comprehension стоит применять, когда: 

  • нужно быстро и наглядно преобразовать или отфильтровать данные;

  • генерация списка может быть описана одной логической конструкцией;

  • важно сократить количество вспомогательных строк, не жертвуя читаемостью.

Но если логика сложная, с несколькими условиями и вложенными циклами, — такой код будет труднее читать, чем обычный цикл.

map() и filter() — преобразование и фильтрация данных

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

Самый очевидный способ — пройтись циклом for, проверяя или изменяя элементы.

Например, хотим перевести список температур из градусов Цельсия в градусы Фаренгейта:

temps_c = [0, 12, 24, 30] temps_f = []  for t in temps_c:     temps_f.append(round(t * 9/5 + 32, 1))  print(temps_f)

Результат:

[32.0, 53.6, 75.2, 86.0]

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

Преобразование с map()

Функция map(func, iterable) применяет func ко всем элементам iterable и возвращает итератор с результатами. В нашем примере:

temps_c = [0, 12, 24, 30] temps_f = list(map(lambda t: round(t * 9/5 + 32, 1), temps_c))  print(temps_f)

Результат:

[32.0, 53.6, 75.2, 86.0]

map() полезен, если уже есть готовая функция для обработки одного элемента и её нужно применить ко всем данным.

Фильтрация с filter()

filter(func, iterable) оставляет только те элементы, для которых func вернёт True. Например, выберем только положительные числа:

numbers = [-5, 0, 7, -2, 10] positive = list(filter(lambda x: x > 0, numbers))  print(positive)

Результат:

[7, 10]

Общая рекомендация. List comprehension часто делают то же самое, что map() и filter(), но выглядят привычнее для разработчиков и читаются проще. При этом map() и filter() удобны, когда уже есть готовая функция для обработки или фильтрации (особенно если она импортируется из другого модуля) и её нужно применить ко всем элементам без написания отдельного цикла.

Синтаксические сокращения

lambda — компактные анонимные функции

В Python можно создать функцию без имени — через lambda. Её используют, когда нужно что-то простое: посчитать длину строки при сортировке, проверить поле словаря при фильтрации. 

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

Сортировка

Допустим, нужно отсортировать список книг по длине названия:

books = ["Война и мир", "Мастер и Маргарита", "Тихий Дон"]  sorted_books = sorted(books, key=lambda title: len(title)) print(sorted_books)

Результат: 

['Тихий Дон', 'Война и мир', 'Мастер и Маргарита']

Без lambda пришлось бы писать отдельную функцию def by_length(word): return len(word) ради одного вызова.

Применение в map()

map() применяет функцию ко всем элементам последовательности. Здесь у нас список названий книг, и мы хотим сделать так, чтобы каждое слово начиналось с заглавной буквы. Функция lambda name: name.title() делает это для одного названия, а map() — для всего списка.

books = ["война и мир", "мастер и маргарита", "тихий дон"] titles = list(map(lambda name: name.title(), books))  print(titles)

Результат:

['Война И Мир', 'Мастер И Маргарита', 'Тихий Дон']

Метод .title() делает заглавной первую букву каждого слова, поэтому союз «и» тоже будет с большой буквы — это нормально для этого метода.

Фильтрация с filter()

Есть список отзывов о книгах, и мы хотим оставить только те, что длиннее 30 символов, — например, для дальнейшего анализа:

reviews = [     "Отличная книга!",     "Читал взахлёб, потрясающая история.",     "Неплохо, но местами скучно.",     "Самая сильная книга автора, советую." ]  long_reviews = list(filter(lambda r: len(r) > 30, reviews))  print(long_reviews)

Результат:

['Читал взахлёб, потрясающая история.', 'Самая сильная книга автора, советую.']

Сортировка словарей по полю

Есть список авторов и количество проданных книг. Нужно отсортировать их по тиражу:

authors = [     {"name": "Лев Толстой", "sold": 5000000},     {"name": "Михаил Шолохов", "sold": 3200000},     {"name": "Михаил Булгаков", "sold": 4100000} ]  sorted_authors = sorted(authors, key=lambda a: a["sold"], reverse=True) print(sorted_authors)

Результат:

[{'name': 'Лев Толстой', 'sold': 5000000}, {'name': 'Михаил Булгаков', 'sold': 4100000}, {'name': 'Михаил Шолохов', 'sold': 3200000}]

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

Тернарный оператор (x if … else y) — короткое ветвление

Обычно для выбора одного из двух значений в зависимости от условия используют полную конструкцию if / else.

Допустим, приложение показывает прогноз погоды. Если температура ниже нуля — пишем «мороз», иначе — «плюс»:

temperature = -5  if temperature < 0:     weather = "мороз" else:     weather = "плюс"  print(weather)

Результат:

мороз

Тернарный оператор позволяет сделать то же самое компактнее:

temperature = -5 weather = "мороз" if temperature < 0 else "плюс" print(weather)

Результат:

мороз

Читается так: «Присвоить "мороз", если температура меньше нуля, иначе "плюс"».

Пример 1: статус бронирования

В системе бронирования столика в ресторане нужно показать «Подтверждено» или «Ожидает подтверждения» в зависимости от статуса.

confirmed = False status = "Подтверждено" if confirmed else "Ожидает подтверждения" print(status)

Результат:

Ожидает подтверждения

Пример 2: расчёт стоимости доставки

Если сумма заказа больше 3 000 рублей — доставка бесплатная, иначе — 250 рублей.

order_total = 2800 delivery_cost = 0 if order_total > 3000 else 250 print(delivery_cost)

Результат:

250

Общая рекомендация. Тернарный оператор полезен, когда логика выбора простая и значение можно определить в одну строку. Если проверок несколько или условие сложное, лучше использовать обычный if / else для читаемости.

Множественное присваивание (a, b = b, a) — замена значений и распаковка

В Python можно присваивать значения сразу нескольким переменным. Это упрощает код и избавляет от повторов, особенно если данные уже упакованы в список, кортеж или другую структуру.

Распаковка данных из списка или кортежа

Например, сайт доставки еды хранит координаты курьера в виде списка из двух чисел — широта и долгота. 

Классический способ выглядел бы так:

courier_location = [55.7522, 37.6156]  lat = courier_location[0] lon = courier_location[1]  print(lat, lon)

Результат:

55.7522 37.6156

С множественным присваиванием мы можем записать то же самое короче и понятнее:

courier_location = [55.7522, 37.6156] lat, lon = courier_location  print(lat, lon)

Результат:

55.7522 37.6156

Обмен значений без временной переменной

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

first = "взрослый" second = "ребёнок" first, second = second, first  print(first, second)

Результат:

ребёнок взрослый

Перебор кортежей в цикле

Например, у нас есть список заказов с названием блюда и именем клиента:

orders = [     ("Пицца Маргарита", "Анна"),     ("Суши с лососем", "Игорь"),     ("Шаверма", "Марина") ]  for dish, client in orders:     print(f"{client} заказал(а) {dish}")

Результат:

Анна заказал(а) Пицца Маргарита Игорь заказал(а) Суши с лососем Марина заказал(а) Шаверма

Игнорирование ненужных данных с _

Если в кортеже или списке есть значения, которые в этом месте не нужны, их можно пропустить, присвоив переменной _.

Например, к заказам добавили цену, но она нам сейчас не нужна.

order = ("Пицца Маргарита", "Анна", 650) dish, client, _ = order  print(f"{client} заказал(а) {dish}")

Результат:

Анна заказал(а) Пицца Маргарита

Средний уровень

Дополнительный приём с zip()

В базовом уровне мы посмотрели, как zip() объединяет списки или строки. Но на практике у этой функции есть и более гибкие сценарии. Один из самых распространённых — транспонирование таблицы с помощью zip(*temps).

Разберём его подробнее.

Представим, что у нас есть матрица температур за три дня по городам. Нужно изменить таблицу, чтобы по строкам были дни, а по столбцам — города. Сделать это можно одной строчкой с помощью распаковки * в вызове zip().

temps = [     [20, 21, 19],  # Москва, СПб, Казань     [18, 20, 17],     [22, 23, 21] ]  transposed = list(zip(*temps)) for row in transposed:     print(row)

Результат:

(20, 18, 22) (21, 20, 23) (19, 17, 21)

Генераторы — создание последовательностей без лишней нагрузки на память

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

Генераторы позволяют выдавать элементы по одному без хранения всего списка. Это полезно, когда:

  • данные нужно просто перебрать и больше не использовать;

  • элементов очень много;

  • значения вычисляются постепенно, а не известны сразу.

Есть два основных способа сделать генератор: генераторное выражение и функция-генератор с yield.

Генераторное выражение ()

Похоже на списковое включение, только в круглых скобках. Разница в том, что оно возвращает не готовый список, а объект-генератор, который выдаёт элементы по запросу.

Например, у нас есть список книг и мы хотим посчитать количество символов в каждом названии. Списковое включение создаст сразу весь список, а генератор — нет.

books = ["Война и мир", "Преступление и наказание", "Мастер и Маргарита"]  title_lengths = (len(title) for title in books)  for length in title_lengths:     print(length)

Результат:

12 26 21

Генератор не хранит все значения в памяти, поэтому подходит для работы с большими наборами данных.

Функции-генераторы и yield

Иногда логику генерации данных сложно записать одной строчкой. В этом случае можно написать обычную функцию, но вместо return использовать yield. Каждый вызов yield отдаёт значение и «замораживает» выполнение функции до следующего запроса.

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

def seat_numbers(rows, seats_per_row):     for row in range(1, rows + 1):         for seat in range(1, seats_per_row + 1):             yield f"Ряд {row}, место {seat}"  for place in seat_numbers(2, 3):     print(place)

Результат:

Ряд 1, место 1 Ряд 1, место 2 Ряд 1, место 3 Ряд 2, место 1 Ряд 2, место 2 Ряд 2, место 3

Генератор остановится сам, когда закончатся значения, и при этом не будет хранить в памяти весь список мест.

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

def read_orders(filename):     with open(filename, encoding="utf-8") as f:         for line in f:             yield line.strip()  for order in read_orders("orders_2025_08_09.txt"):     if "кофемашина" in order.lower():         print(f"Нашли заказ: {order}")

Результат:

Нашли заказ: Заказ #1524 — кофемашина DeLonghi, 2 шт. Нашли заказ: Заказ #1589 — кофемашина Philips, 1 шт.

Множества (set) — работа с уникальными значениями и сравнением коллекций

В Python множество (set) — это коллекция, которая автоматически хранит только уникальные значения и оптимизирована для быстрых операций сравнения и поиска. В отличие от списков, множества не запоминают порядок элементов, зато позволяют за один оператор найти пересечение, разность или объединение данных.

Создаются множества либо с помощью фигурных скобок {}, либо функцией set() для преобразования из других типов. 

Пример. Интернет-магазин провёл две акции: на кофемашины и на тостеры. Покупатели могли участвовать в обеих, и в списках есть повторы.

coffee_buyers = {"Анна", "Игорь", "Марина", "Олег", "Анна"} toaster_buyers = {"Марина", "Дмитрий", "Олег"}

Удаление дубликатов

print(coffee_buyers)

Результат:

{'Марина', 'Олег', 'Анна', 'Игорь'}

Дубликаты исчезли, порядок элементов может отличаться.

Пересечение множеств

Узнаем, кто в нашем примере участвовал в обеих акциях.

both = coffee_buyers & toaster_buyers print(both)

Результат:

{'Марина', 'Олег'}

Разность множеств

А теперь узнаем, кто купил кофемашину, но не покупал тостер:

only_coffee = coffee_buyers - toaster_buyers print(only_coffee)

Результат:

{'Анна', 'Игорь'}

Объединение множеств

Чтобы получить список всех клиентов, которые купили хотя бы один из товаров, используем объединение:

all_buyers = coffee_buyers | toaster_buyers print(all_buyers)

Результат:

{'Анна', 'Олег', 'Дмитрий', 'Марина', 'Игорь'}

Проверка вхождения

Проверить, купил ли клиент конкретный товар, можно через оператор in. Это работает очень быстро, даже на больших данных:

if "Анна" in coffee_buyers:     print("Анна покупала кофемашину") else:     print("Анна не покупала кофемашину")

Результат:

Анна покупала кофемашину

collections.Counter — подсчёт количества повторяющихся элементов

Есть задачи, когда нужно быстро посчитать, сколько раз каждый элемент встречается в коллекции. Можно написать цикл, вручную проверять словарь и увеличивать счётчики — а можно использовать collections.Counter, который делает всё это за нас.

Counter принимает любую последовательность (строку, список, кортеж) и возвращает словарь-подобный объект, где ключ — элемент, а значение — количество его повторений.

Пример 1. Представим, что у нас есть список товаров, которые клиенты добавляли в корзину за день. Нужно понять, что сегодня было в топе.

from collections import Counter  cart_items = [     "кофемашина", "чайник", "кофемашина", "тостер",     "чайник", "чайник", "кофемашина" ]  item_counts = Counter(cart_items) print(item_counts)

Counter можно сразу попросить показать только несколько самых популярных позиций:

top_items = item_counts.most_common(2) print(top_items)

Результат:

[('кофемашина', 3), ('чайник', 3)]

Пример 2. Допустим, мы анализируем отзывы покупателей и хотим понять, какие слова встречаются чаще всего.

from collections import Counter  reviews = """ Кофемашина отличная, работает тихо. Чайник хороший, но кофемашина лучше. Кофемашина выглядит стильно и варит вкусный кофе. """  # Преобразовываем всё в нижний регистр и разбиваем на слова words = reviews.lower().replace(".", "").split()  word_counts = Counter(words) print(word_counts.most_common(3))

Результат:

[('кофемашина', 3), ('и', 1), ('отличная,', 1)]

В реальном проекте мы бы ещё очистили текст от пунктуации и стоп-слов («и», «но», «а» и т. п.), но даже базовый пример показывает, как Counter помогает быстро получить статистику.

collections.defaultdict — словарь с автоматическим значением по умолчанию

Обычный словарь в Python вызывает ошибку KeyError, если обратиться к несуществующему ключу. Поэтому часто перед записью приходится проверять, есть ли ключ, и, если нет, создавать его.

defaultdict при обращении к новому ключу автоматически создаёт для него значение по умолчанию, которое мы задаём при создании словаря.

Пример. Представим, что у нас есть логи чата и каждое сообщение хранится в виде кортежа (дата, текст). Нужно сгруппировать их так, чтобы у каждой даты был список всех сообщений за этот день.

from collections import defaultdict  messages = [     ("2025-08-08", "Встречаемся в 14:00"),     ("2025-08-08", "Не забудь документы"),     ("2025-08-09", "Презентация готова?"),     ("2025-08-09", "Проверь почту"),     ("2025-08-10", "Отправил отчёт") ]  messages_by_date = defaultdict(list)  for date, text in messages:     messages_by_date[date].append(text)  for date, texts in messages_by_date.items():     print(date)     for msg in texts:         print(f"  - {msg}")

Результат:

2025-08-08   - Встречаемся в 14:00   - Не забудь документы 2025-08-09   - Презентация готова?   - Проверь почту 2025-08-10   - Отправил отчёт

В обычном словаре пришлось бы писать что-то вроде:

if date not in messages_by_date:     messages_by_date[date] = []

defaultdict(list) убирает этот шаг: при первом обращении к новой дате он сам создаёт пустой список, куда можно сразу добавлять сообщения.

Распаковка списков — доступ к частям коллекции без использования индексов

В Python можно «распаковывать» коллекции в несколько переменных, не обращаясь к элементам по индексам. 

Простая распаковка

Если известно, что в коллекции ровно столько элементов, сколько переменных слева, можно присвоить их напрямую. 

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

pair = ["Москва", "Россия"] city, country = pair print(city)     # Москва print(country)  # Россия

Здесь задаётся команда: «Возьми первый элемент и положи в city, а второй — в country».

Результат:

Город: Москва Страна: Россия

Расширенная распаковка с *

Если элементов больше, чем переменных, можно использовать *, чтобы собрать «остаток» в список. Например, у нас есть список заказов за день, но мы хотим отдельно сохранить первый заказ, а остальные — сгруппировать.

orders = ["Заказ #101", "Заказ #102", "Заказ #103", "Заказ #104"]  first, *rest = orders print(f"Первый заказ: {first}") print(f"Остальные заказы: {rest}")

Результат:

Первый заказ: Заказ #101 Остальные заказы: ['Заказ #102', 'Заказ #103', 'Заказ #104']

* собирает «остаток» элементов в отдельный список.

Распаковка внутри цикла

​​Пример 1. Пары ключ-значение в словаре.

Часто коллекции уже содержат данные в виде пар или кортежей. Допустим, у нас есть словарь столиц и мы хотим вывести фразу «<столица> — столица <страны>».

capitals = {"Россия": "Москва", "Франция": "Париж"}  for country, capital in capitals.items():     print(f"{capital} — столица {country}")

dict.items() возвращает пары (ключ, значение)

Результат:

Москва — столица Россия Париж — столица Франция

Пример 2. Несколько списков с zip().

У нас есть список студентов и список их оценок. Нужно вывести в формате «имя — баллы».

students = ["Аня", "Борис", "Сергей"] scores = [85, 92, 78]  for name, score in zip(students, scores):     print(f"{name} — {score} баллов")

Результат:

Аня — 85 баллов Борис — 92 балла Сергей — 78 баллов

*args, **kwargs — работа с переменным числом аргументов в функциях

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

  • *args — собирает все позиционные аргументы в кортеж;

  • **kwargs — собирает все именованные аргументы в словарь.

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

*args — произвольное количество позиционных аргументов

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

def total_score(*args):     return sum(args)  print(total_score(5, 10, 15))   # три раунда print(total_score(3, 7))        # два раунда 

Результат:

30 10

Все переданные числа собираются в кортеж args, а дальше мы просто используем sum() для подсчёта общей суммы.

**kwargs — произвольное количество именованных аргументов

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

def show_profile(name, **kwargs):     print(f"Имя: {name}")     for key, value in kwargs.items():         print(f"{key.capitalize()}: {value}")  show_profile("Анна", age=28, city="Москва", hobby="чтение")

Результат:

Имя: Анна Age: 28 City: Москва Hobby: чтение

Все дополнительные именованные аргументы собираются в словарь kwargs. Благодаря этому функция остаётся универсальной и подстраивается под набор данных.

Комбинирование *args и **kwargs

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

Используем *args и **kwargs:

def send_notifications(*args, **kwargs):     for message in args:         print(f"Отправка: {message}")     print("Параметры доставки:", kwargs)  send_notifications(     "Сервер перезапущен",     "Новая версия задеплоена",     channel="dev-team",     priority="high",     author="CI/CD бот" )

Результат:

Отправка: Сервер перезапущен Отправка: Новая версия задеплоена Параметры доставки: {'channel': 'dev-team', 'priority': 'high', 'author': 'CI/CD бот'}

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

Продвинутый уровень

Дополнительные приёмы

Комбинация zip() и enumerate()

В базовом уровне мы посмотрели простое объединение списков через zip(). В среднем уровне — познакомились с приёмом транспонирования таблицы через zip(*temps).

Теперь разберём более сложный случай: когда данные нужно не только объединить, но и сразу пронумеровать. Для этого можно совместить zip() и enumerate()

Например, у нас есть список игроков, их команд и количество очков. Нужно получить список строк вида "1. Иван (Волки) — 15 очков", но только для тех, кто набрал больше 10 очков.

players = ["Сергей", "Антон", "Михаил"] teams = ["Волки", "Тигры", "Ястребы"] scores = [15, 8, 17]  report = [     f"{i}. {player} ({team}) — {score} очков"     for i, (player, team, score) in enumerate(zip(players, teams, scores), start=1)     if score > 10 ]  for line in report:     print(line)

Результат:

1. Сергей (Волки) — 15 очков 3. Михаил (Ястребы) — 17 очков

Здесь zip() объединяет три списка, enumerate() даёт нумерацию с единицы, а list comprehension фильтрует игроков по количеству очков и формирует готовые строки.

Комбинация map() и filter()

В разделе для базового уровня мы разобрали map() и filter() по отдельности, теперь посмотрим, как они работают вместе. 

Допустим, у нас список строк с числами и нужно:

  1. Убрать пустые строки.

  2. Преобразовать всё в числа.

  3. Оставить только чётные.

raw_data = ["10", "15", "", "22", "9"]  result = list(     filter(lambda x: x % 2 == 0,            map(int, filter(None, raw_data))) )  print(result)

Результат:

[10, 22]

Повторим здесь ремарку. Объединять map() и filter() в одну цепочку можно, когда нужно записать несколько шагов обработки максимально компактно. Но в большинстве случаев list comprehension выглядит нагляднее и читается проще.

Пример реальной задачи: обработка данных из CSV

Представим, что у нас есть список словарей с заказами и нужно получить список имён клиентов, оформивших заказ дороже 1 000 рублей:

orders = [     {"name": "Анна", "amount": 1250},     {"name": "Иван", "amount": 980},     {"name": "Мария", "amount": 1430} ]  big_orders = list(     map(lambda o: o["name"],         filter(lambda o: o["amount"] > 1000, orders)) )  print(big_orders)

Результат:

['Анна', 'Мария']

with — управление ресурсами с автоматическим закрытием

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

Если делать по старинке, придётся открывать ресурс, оборачивать работу с ним в try…finally и в конце вызывать .close(). Например, читаем список пользователей из файла:

file = open("users.txt", encoding="utf-8") try:     data = file.read() finally:     file.close()

С with код короче, плюс Python сам закроет ресурс, даже если в процессе возникнет ошибка:

with open("users.txt", encoding="utf-8") as file:     data = file.read()

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

purchases = ["Ноутбук", "Беспроводная мышь", "Рюкзак для ноутбука"]  with open("last_purchases.txt", "w", encoding="utf-8") as file:     for item in purchases:         file.write(item + "\n")

Вышли из блока with — значит, покупки уже записаны, файл закрыт и их можно будет без проблем прочитать позже.

with применяют не только для файлов. Он работает с любыми объектами, у которых определены методы __enter__ и __exit__. Это может быть, например:

  • работа с подключением к базе данных,

  • временное изменение настроек,

  • управление блокировками в многопоточном коде,

  • открытие и автоматическое закрытие сетевых соединений.

dataclasses — описание структур данных с минимальным кодом

Когда нужно хранить связанные между собой данные, например сведения о заказе, пользователе или товаре, можно сделать обычный класс. Но в нём придётся писать конструктор __init__, методы сравнения, возможно, __repr__ для красивого вывода. 

Пример. Представим, что у нас есть список участников с именами, городами и временем забега. Нам нужно его хранить и сортировать по времени. 

Классический способ:

class Runner:     def __init__(self, name, city, time):         self.name = name         self.city = city         self.time = time  # время в минутах      def __repr__(self):         return f"Runner({self.name!r}, {self.city!r}, {self.time})"  runners = [     Runner("Анна", "Москва", 245),     Runner("Игорь", "Казань", 230),     Runner("Марина", "СПб", 260) ]  print(sorted(runners, key=lambda r: r.time))

Результат:

[Runner('Игорь', 'Казань', 230), Runner('Анна', 'Москва', 245), Runner('Марина', 'СПб', 260)]

С модулем dataclasses достаточно объявить класс с нужными полями и пометить его декоратором @dataclass. Python сам сгенерирует все базовые методы.

from dataclasses import dataclass  @dataclass(order=True) class Runner:     time: int     name: str     city: str  runners = [     Runner(245, "Анна", "Москва"),     Runner(230, "Игорь", "Казань"),     Runner(260, "Марина", "СПб") ]  print(sorted(runners))

Результат:

[Runner(time=230, name='Игорь', city='Казань'), Runner(time=245, name='Анна', city='Москва'), Runner(time=260, name='Марина', city='СПб')]

order=True позволяет сразу сравнивать и сортировать такие объекты. Порядок определяется тем, какие поля объявлены первыми, поэтому здесь всё упорядочилось по времени.

functools.partial — создание версии функции с заданными аргументами

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

Удобно, когда логика используется в разных местах, но с одними и теми же параметрами.

Пример. Представим, что у нас есть функция отправки уведомлений, в которую нужно передавать канал (email, sms, push), сообщение и имя получателя:

def send_notification(channel, message, user):     print(f"[{channel}] {user}: {message}")

В большинстве случаев мы отправляем письма на email, так что параметр channel почти всегда одинаковый. 

Можно писать каждый раз:

send_notification("email", "Ваш заказ готов", "Анна") send_notification("email", "Пароль успешно изменён", "Игорь")

А можно вместо этого создать отдельную функцию для email с помощью partial:

from functools import partial  email_notify = partial(send_notification, "email")  email_notify("Ваш заказ готов", "Анна") email_notify("Пароль успешно изменён", "Игорь")

Результат:

[email] Анна: Ваш заказ готов [email] Игорь: Пароль успешно изменён

Теперь первый аргумент всегда будет «email», а остальные передаются при вызове.

Ещё пример. Допустим, у нас есть функция, которая форматирует дату в заданном формате: 

from datetime import datetime  def format_date(date_obj, fmt):     return date_obj.strftime(fmt)

Формат %d.%m.%Y (день.месяц.год) у нас используется почти везде. Вместо того чтобы передавать его в каждом вызове, можно прописать:

from functools import partial  format_russian = partial(format_date, fmt="%d.%m.%Y")  today = datetime(2025, 8, 12) print(format_russian(today))

Результат:

12.08.2025

Здесь мы фиксируем второй аргумент (fmt), а дату передаём уже при вызове. 

itertools — набор инструментов для работы с итераторами

Модуль itertools в стандартной библиотеке Python — это набор функций, которые помогают удобно работать с последовательностями и генераторами. Он пригодится, когда нужно обрабатывать данные без копирования и хранения в памяти.

Рассмотрим три часто используемые функции.

chain — объединение последовательностей

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

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

from itertools import chain  orders_moscow = ["Заказ 101", "Заказ 102"] orders_spb = ["Заказ 201", "Заказ 202"]  for order in chain(orders_moscow, orders_spb):     print(f"Обрабатываем {order}")

Результат:

Обрабатываем Заказ 101 Обрабатываем Заказ 102 Обрабатываем Заказ 201 Обрабатываем Заказ 202

Так мы перебираем все заказы, как будто они лежат в одной очереди.

groupby — группировка данных

groupby группирует соседние элементы с одинаковым ключом. 

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

Пример. У нас есть выгрузка заказов с датами, и мы хотим сгруппировать их по дню, чтобы, например, сформировать отчёт по продажам.

from itertools import groupby from operator import itemgetter  orders = [     {"date": "2025-08-10", "order_id": 1, "amount": 1200},     {"date": "2025-08-10", "order_id": 2, "amount": 800},     {"date": "2025-08-11", "order_id": 3, "amount": 1500},     {"date": "2025-08-11", "order_id": 4, "amount": 500},     {"date": "2025-08-12", "order_id": 5, "amount": 2000}, ]  # groupby требует, чтобы данные были отсортированы по ключу группировки orders.sort(key=itemgetter("date"))  for date, group in groupby(orders, key=itemgetter("date")):     total_amount = sum(order["amount"] for order in group)     print(f"{date}: всего заказов на {total_amount} руб.")

Результат:

2025-08-10: всего заказов на 2000 руб. 2025-08-11: всего заказов на 2000 руб. 2025-08-12: всего заказов на 2000 руб.

Если не отсортировать, группы будут разбиты по первому вхождению категории.

islice — «срез» на итераторах

islice в itertools делает срез по итерируемому объекту, но без предварительного преобразования его в список. Это важно, если источник данных большой или бесконечный: элементы будут извлекаться только по мере необходимости, а не целиком в память.

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

from itertools import islice  with open("big_log.txt", encoding="utf-8") as f:     for line in islice(f, 5):         print(line.strip())

Результат (просто для примера):

[2025-08-10 12:01] INFO: старт системы [2025-08-10 12:02] WARNING: низкий заряд батареи [2025-08-10 12:03] ERROR: модуль датчика недоступен [2025-08-10 12:04] INFO: перезапуск [2025-08-10 12:05] ERROR: превышен лимит памяти

Несколько дополнительных примеров

accumulate — накопление промежуточных значений

accumulate из itertools считает промежуточные итоги. По умолчанию он суммирует, но можно передать любую другую функцию (например, умножение или поиск максимума).

Пример. У нас есть список продаж за каждый день. Если просто вывести этот список, мы увидим только дневные суммы. Но часто в отчётах нужно показать, сколько всего накопилось с начала периода к каждой дате.

accumulate делает это автоматически:

from itertools import accumulate  sales = [1000, 2000, 1500, 3000] cumulative = list(accumulate(sales)) print(cumulative)

Результат:

[1000, 3000, 4500, 7500]

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

takewhile — отбор до первого несоответствия условию

takewhile идёт по последовательности и возвращает элементы, пока условие истинно. Как только встречается первый элемент, который не проходит проверку, работа останавливается.

Пример. У нас есть статистика температуры за день, и мы хотим взять только те часы, когда температура была выше нуля.

from itertools import takewhile  # (час, температура) temps = [     (6, -3), (7, -1), (8, 0),     (9, 2), (10, 4), (11, 5),     (12, 5), (13, 4), (14, 3),     (15, 2), (16, 1), (17, 0),     (18, -1), (19, -2), (20, -3) ]  # Берём только часы с температурой выше нуля warm_hours = list(takewhile(lambda t: t[1] > 0, temps)) print(warm_hours)

Результат:

[(9, 2), (10, 4), (11, 5), (12, 3), (13, 1)]

takewhile вернул часы, когда температура была положительной — с 9:00 до 16:00.

zip_longest — объединение с заполнением пропущенных элементов

Обычный zip останавливается на самом коротком списке. zip_longest из itertools проходит до конца самого длинного и подставляет недостающие значения (по умолчанию None).

Пример. Допустим, есть список авторов и список их последних статей. У кого-то публикации уже есть, а у кого-то пока нет. Если воспользоваться обычным zip, то авторы без статьи вообще не попадут в результат.

Поэтому используем zip_longest:

from itertools import zip_longest  authors = ["Анна", "Игорь", "Марина", "Дмитрий"] articles = ["Python и данные", "Асинхронка в реальной жизни"]  for author, article in zip_longest(authors, articles, fillvalue="— нет статьи —"):     print(f"{author}: {article}")

Результат:

Анна: Python и данные Игорь: Асинхронка в реальной жизни Марина: — нет статьи — Дмитрий: — нет статьи —

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

Мы разобрали только несколько функций itertools, полный список можно изучить в документации.

Резюмируем

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


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

Или можно стать востребованным сотрудником и открыть открыть бóльшие перспективы в карьере с профессиональным обучением:


ссылка на оригинал статьи https://habr.com/ru/articles/940890/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *