MidТеория5 min

Распаковка и продвинутые паттерны

*args, zip(), sorted(), walrus operator, bisect, heapq и deque

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

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

# Star unpacking (PEP 3132)
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

*init, last = [1, 2, 3, 4, 5]
print(init)   # [1, 2, 3, 4]
print(last)   # 5

first, *middle, last = [1, 2, 3, 4, 5]
print(middle)  # [2, 3, 4]

# Nested unpacking
data = [("Иван", [85, 90, 78]), ("Мария", [92, 88, 95])]
for name, [first_score, *other_scores] in data:
    print(f"{name}: первый балл {first_score}, остальные {other_scores}")

# Ignore values
_, _, third = (1, 2, 3)
print(third)  # 3

# Unpack in function calls
def add(a, b, c):
    return a + b + c

args = [1, 2, 3]
print(add(*args))  # 6

kwargs = {"a": 1, "b": 2, "c": 3}
print(add(**kwargs))  # 6

# Merge iterables with unpacking
list1 = [1, 2, 3]
list2 = [4, 5, 6]
merged = [*list1, *list2]       # [1, 2, 3, 4, 5, 6]
merged_set = {*list1, *list2}   # {1, 2, 3, 4, 5, 6}

dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
merged_dict = {**dict1, **dict2}  # {'a': 1, 'b': 2, 'c': 3, 'd': 4}

Walrus Operator (:=)

Оператор присваивания выражения (PEP 572, Python 3.8+):

# Read and check in one step
import re

text = "Контакт: +7 (999) 123-45-67"
if match := re.search(r'\d{3}-\d{2}-\d{2}', text):
    print(f"Найден номер: {match.group()}")

# Filter with computation
data = [1, 5, 12, 3, 18, 7, 25]
large = [y for x in data if (y := x * 2) > 10]
print(large)  # [24, 36, 14, 50]

# While loop with assignment
lines = []
while (line := input(">>> ")) != "quit":
    lines.append(line)

# Avoid repeated expensive calls
import math
coords = [(1, 2), (3, 4), (10, 20), (5, 12)]
close_points = [
    (x, y, dist)
    for x, y in coords
    if (dist := math.sqrt(x**2 + y**2)) < 15
]
print(close_points)
# [(1, 2, 2.236...), (3, 4, 5.0), (5, 12, 13.0)]

# Process file chunks
# with open("data.bin", "rb") as f:
#     while chunk := f.read(8192):
#         process(chunk)

sorted() и ключи сортировки

# Basic sorting
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(numbers))              # [1, 1, 2, 3, 4, 5, 6, 9]
print(sorted(numbers, reverse=True)) # [9, 6, 5, 4, 3, 2, 1, 1]

# Sort by key function
words = ["banana", "apple", "cherry", "date"]
print(sorted(words, key=len))          # ['date', 'apple', 'banana', 'cherry']
print(sorted(words, key=str.lower))    # Case-insensitive sort

# Sort complex objects
students = [
    {"name": "Иван", "grade": 85, "age": 20},
    {"name": "Мария", "grade": 92, "age": 19},
    {"name": "Пётр", "grade": 78, "age": 21},
    {"name": "Анна", "grade": 92, "age": 20},
]

# Sort by grade (descending), then by name
from operator import itemgetter
by_grade = sorted(students, key=itemgetter("grade"), reverse=True)
for s in by_grade:
    print(f"{s['name']}: {s['grade']}")

# Multiple sort keys with tuple
by_grade_name = sorted(students, key=lambda s: (-s["grade"], s["name"]))
for s in by_grade_name:
    print(f"{s['name']}: {s['grade']}")
# Анна: 92
# Мария: 92
# Иван: 85
# Пётр: 78

# attrgetter for objects
from operator import attrgetter
from dataclasses import dataclass

@dataclass
class Student:
    name: str
    grade: int

students = [Student("Иван", 85), Student("Мария", 92), Student("Пётр", 78)]
by_grade = sorted(students, key=attrgetter("grade"), reverse=True)

Стабильность сортировки

# Python's sort is STABLE (preserves relative order of equal elements)
data = [("Иван", "А"), ("Мария", "Б"), ("Пётр", "А"), ("Анна", "Б")]

# Sort by group - students within same group keep original order
by_group = sorted(data, key=lambda x: x[1])
print(by_group)
# [('Иван', 'А'), ('Пётр', 'А'), ('Мария', 'Б'), ('Анна', 'Б')]

# Multi-level sort using stability: sort by secondary key first, then primary
# Sort by grade ascending, then by name
data = [("Иван", 85), ("Мария", 92), ("Пётр", 85), ("Анна", 92)]
result = sorted(sorted(data, key=lambda x: x[0]), key=lambda x: x[1])
print(result)
# [('Иван', 85), ('Пётр', 85), ('Анна', 92), ('Мария', 92)]

zip() и itertools

# zip() - parallel iteration
names = ["Иван", "Мария", "Пётр"]
ages = [25, 30, 28]
cities = ["Москва", "Петербург", "Казань"]

for name, age, city in zip(names, ages, cities):
    print(f"{name}, {age}, {city}")

# Create dict from two lists
user_dict = dict(zip(names, ages))
print(user_dict)  # {'Иван': 25, 'Мария': 30, 'Пётр': 28}

# Unzip (transpose)
pairs = [(1, "a"), (2, "b"), (3, "c")]
numbers, letters = zip(*pairs)
print(numbers)  # (1, 2, 3)
print(letters)  # ('a', 'b', 'c')

# zip with strict mode (Python 3.10+)
# list(zip([1, 2], [3, 4, 5], strict=True))  # ValueError!

# itertools essentials
from itertools import chain, islice, takewhile, dropwhile, accumulate

# chain - combine multiple iterables
combined = list(chain([1, 2], [3, 4], [5, 6]))
print(combined)  # [1, 2, 3, 4, 5, 6]

# islice - slice any iterable (lazy)
from itertools import count
first_10_evens = list(islice((x for x in count() if x % 2 == 0), 10))
print(first_10_evens)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# accumulate - running totals
from itertools import accumulate
running_sum = list(accumulate([1, 2, 3, 4, 5]))
print(running_sum)  # [1, 3, 6, 10, 15]

running_max = list(accumulate([3, 1, 4, 1, 5, 9], max))
print(running_max)  # [3, 3, 4, 4, 5, 9]

bisect -- бинарный поиск

import bisect

# Maintain a sorted list efficiently
sorted_list = [1, 3, 5, 7, 9]

# Find insertion point
idx = bisect.bisect(sorted_list, 4)   # 2 (insert after existing equal elements)
idx_l = bisect.bisect_left(sorted_list, 5)  # 2 (insert before equal elements)
idx_r = bisect.bisect_right(sorted_list, 5)  # 3

# Insert maintaining sorted order
bisect.insort(sorted_list, 4)
print(sorted_list)  # [1, 3, 4, 5, 7, 9]

bisect.insort(sorted_list, 6)
print(sorted_list)  # [1, 3, 4, 5, 6, 7, 9]

# Grade classification using bisect
def grade(score: int) -> str:
    """Classify score into a letter grade."""
    breakpoints = [60, 70, 80, 90]
    grades = ["F", "D", "C", "B", "A"]
    idx = bisect.bisect(breakpoints, score)
    return grades[idx]

scores = [33, 60, 72, 85, 91, 100]
for s in scores:
    print(f"{s}: {grade(s)}")
# 33: F, 60: D, 72: C, 85: B, 91: A, 100: A

heapq -- куча (приоритетная очередь)

import heapq

# heapq implements a min-heap
data = [3, 1, 4, 1, 5, 9, 2, 6]

# Create a heap
heapq.heapify(data)
print(data)  # [1, 1, 2, 6, 5, 9, 4, 3] (heap property)

# Push and pop
heapq.heappush(data, 0)
print(heapq.heappop(data))  # 0 (smallest element)
print(heapq.heappop(data))  # 1

# Push and pop in one step
result = heapq.heappushpop(data, 7)  # push 7, pop smallest
print(result)

# N largest/smallest
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(heapq.nlargest(3, numbers))   # [9, 6, 5]
print(heapq.nsmallest(3, numbers))  # [1, 1, 2]

# With key function
tasks = [
    {"name": "Отчёт", "priority": 3},
    {"name": "Баг", "priority": 1},
    {"name": "Фича", "priority": 2},
]
urgent = heapq.nsmallest(2, tasks, key=lambda t: t["priority"])
print([t["name"] for t in urgent])  # ['Баг', 'Фича']

# Max-heap trick (negate values)
max_heap = []
for val in [3, 1, 4, 1, 5]:
    heapq.heappush(max_heap, -val)

# Pop gives maximum
print(-heapq.heappop(max_heap))  # 5
print(-heapq.heappop(max_heap))  # 4

# Merge sorted iterables
list1 = [1, 3, 5, 7]
list2 = [2, 4, 6, 8]
merged = list(heapq.merge(list1, list2))
print(merged)  # [1, 2, 3, 4, 5, 6, 7, 8]

collections.deque

Двусторонняя очередь с O(1) операциями на обоих концах:

from collections import deque

# Create a deque
d = deque([1, 2, 3, 4, 5])

# O(1) operations on both ends
d.append(6)         # Add to right: [1, 2, 3, 4, 5, 6]
d.appendleft(0)     # Add to left:  [0, 1, 2, 3, 4, 5, 6]
d.pop()             # Remove from right: 6
d.popleft()         # Remove from left: 0

# Extend on both ends
d.extend([7, 8])        # Add multiple to right
d.extendleft([-2, -1])  # Add multiple to left (reversed!)
print(d)  # deque([-1, -2, 1, 2, 3, 4, 5, 7, 8])

# Rotate
d = deque([1, 2, 3, 4, 5])
d.rotate(2)     # Rotate right: deque([4, 5, 1, 2, 3])
d.rotate(-2)    # Rotate left: deque([1, 2, 3, 4, 5])

# Bounded deque (maxlen) - automatically discards oldest
recent = deque(maxlen=3)
for i in range(5):
    recent.append(i)
    print(recent)
# deque([0], maxlen=3)
# deque([0, 1], maxlen=3)
# deque([0, 1, 2], maxlen=3)
# deque([1, 2, 3], maxlen=3)
# deque([2, 3, 4], maxlen=3)

# Sliding window using deque
def moving_average(data: list[float], window: int) -> list[float]:
    """Calculate moving average with a sliding window."""
    result = []
    window_deque = deque(maxlen=window)
    for value in data:
        window_deque.append(value)
        if len(window_deque) == window:
            result.append(sum(window_deque) / window)
    return result

prices = [100, 102, 104, 103, 105, 107, 106]
print(moving_average(prices, 3))
# [102.0, 103.0, 104.0, 105.0, 106.0]

Итоги

  • Star unpacking * -- гибкое извлечение элементов из последовательностей
  • Walrus operator := -- присваивание внутри выражений
  • sorted() с key -- мощная сортировка по любым критериям; кортежи для множественной сортировки
  • zip() -- параллельная итерация, strict=True в Python 3.10+
  • bisect -- бинарный поиск и вставка в отсортированный список
  • heapq -- min-куча для приоритетных очередей и Top-N задач
  • deque -- O(1) операции на обоих концах, ограниченный размер с maxlen
  • itertools -- chain, islice, accumulate и другие инструменты для ленивой обработки

Проверь себя

Что будет в переменной middle после: first, *middle, last = [1, 2, 3]?

Как отсортировать список словарей по нескольким ключам?

Какова временная сложность bisect.insort() для вставки элемента в отсортированный список?

В чём преимущество deque перед list для реализации очереди?