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

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

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

Бэкенд-разработчик на 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 accumulate

hourly_views = [120, 340, 290, 410, 560]
total_views = list(accumulate(hourly_views))
print(total_views)

Результат:

[120, 460, 750, 1160, 1720]

Мы видим, что к пятому часу статья набрала 1 720 просмотров.

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, полный список можно изучить в документации.

Резюмируем

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


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

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

Комментарии (1)


  1. Femistoklov
    28.08.2025 03:30

    У вашего кота разное кол-во пальцев на передних лапах.