Python: структуры данных, срезы, генераторы
Списки, кортежи, словари и множества и когда что применять, срезы, генераторы списков и выражения-генераторы, распаковка, сортировка по ключу, модуль collections.
Питон ценят за то, что задача, требующая двадцати строк в C++, здесь решается одной. Разберём инструменты, которые это обеспечивают, — и правила выбора между ними.
Четыре базовых структуры
| Тип | Изменяемый | Порядок | Дубликаты | Поиск | Когда применять |
|---|---|---|---|---|---|
| list | Да | Есть | Да | O(n) | Последовательность, нужен индекс |
| tuple | Нет | Есть | Да | O(n) | Неизменяемая запись, ключ словаря |
| dict | Да | Порядок вставки | Ключи уникальны | O(1) | Соответствие ключ-значение |
| set | Да | Нет | Нет | O(1) | Уникальность, операции над множествами |
nums = [4, 8, 15, 16, 23, 42] # список
point = (10.5, 20.3) # кортеж — нельзя изменить
grades = {'математика': 5, 'физика': 4} # словарь
unique = {1, 2, 3} # множество
# Кортеж как ключ словаря — список так использовать нельзя
cache = {(1, 2): 'результат', (3, 4): 'другой'}
# Операции над множествами
a, b = {1, 2, 3, 4}, {3, 4, 5}
print(a & b) # {3, 4} пересечение
print(a | b) # {1,2,3,4,5} объединение
print(a - b) # {1, 2} разность
print(a ^ b) # {1,2,5} симметрическая разность
Срезы
последовательность[начало:конец:шаг] начало включается, конец — нет отрицательные индексы считаются с конца a[2:5] элементы 2, 3, 4 a[:3] первые три a[-3:] последние три a[::2] каждый второй a[::-1] в обратном порядке a[:] полная копия списка
s = 'алгоритмы'
print(s[:4]) # алго
print(s[::-1]) # ымтирогла
nums = list(range(10))
print(nums[2:8:2]) # [2, 4, 6]
# Срез создаёт новый список — исходный не меняется
copy = nums[:]
copy[0] = 99
print(nums[0]) # 0
# Присваивание в срез изменяет список на месте
nums[1:3] = [100, 200, 300] # длина может измениться
Генераторы списков
# Вместо цикла с append
squares = [x**2 for x in range(10)]
# С условием
evens = [x for x in range(20) if x % 2 == 0]
# С преобразованием и условием
names = ['иванов', 'петров', 'сидоров']
short = [n.capitalize() for n in names if len(n) <= 6]
# Вложенные циклы: порядок такой же, как в обычной записи
pairs = [(i, j) for i in range(3) for j in range(3) if i != j]
# Матрица и её транспонирование
m = [[1, 2, 3], [4, 5, 6]]
t = [[row[i] for row in m] for i in range(len(m[0]))]
# Словарь и множество тем же способом
sq_dict = {x: x**2 for x in range(5)}
lengths = {len(n) for n in names}
Выражения-генераторы
# Квадратные скобки создают весь список в памяти
total = sum([x**2 for x in range(10_000_000)]) # ~400 МБ
# Круглые — значения вычисляются по одному
total = sum(x**2 for x in range(10_000_000)) # память не растёт
# Своя функция-генератор
def read_large_file(path):
with open(path, encoding='utf-8') as f:
for line in f:
yield line.rstrip()
# Файл на гигабайт обрабатывается без загрузки в память
for line in read_large_file('log.txt'):
if 'ERROR' in line:
print(line)
# Генератор одноразовый: после обхода он исчерпан
gen = (x for x in range(3))
print(list(gen)) # [0, 1, 2]
print(list(gen)) # [] — уже пусто
Распаковка
a, b = 1, 2
a, b = b, a # обмен без временной переменной
first, *middle, last = [1, 2, 3, 4, 5]
print(middle) # [2, 3, 4]
# Обход словаря с распаковкой
for subject, grade in grades.items():
print(f'{subject}: {grade}')
# Параллельный обход
names = ['А', 'Б', 'В']
scores = [90, 85, 78]
for name, score in zip(names, scores):
print(name, score)
# С индексом
for i, name in enumerate(names, start=1):
print(i, name)
# Передача в функцию
def area(width, height): return width * height
size = (3, 4)
print(area(*size))
params = {'width': 3, 'height': 4}
print(area(**params))
Сортировка
students = [
{'name': 'Иванов', 'course': 3, 'grade': 4.5},
{'name': 'Петров', 'course': 2, 'grade': 4.9},
{'name': 'Сидоров', 'course': 3, 'grade': 3.8},
]
# По одному полю
by_grade = sorted(students, key=lambda s: s['grade'], reverse=True)
# По нескольким: сначала курс, внутри — оценка по убыванию
by_two = sorted(students, key=lambda s: (s['course'], -s['grade']))
# sort меняет список на месте и возвращает None
nums.sort() # правильно
# nums = nums.sort() # ошибка: nums станет None
# Максимум по ключу
best = max(students, key=lambda s: s['grade'])
Модуль collections
from collections import Counter, defaultdict, deque, namedtuple
# Подсчёт частот
words = 'алгоритм анализ алгоритм данные анализ алгоритм'.split()
print(Counter(words).most_common(2)) # [('алгоритм', 3), ('анализ', 2)]
# Словарь с значением по умолчанию — не нужно проверять наличие ключа
groups = defaultdict(list)
for w in words:
groups[len(w)].append(w)
# Двусторонняя очередь: O(1) с обоих концов
queue = deque([1, 2, 3])
queue.appendleft(0)
queue.pop()
# Именованный кортеж: читается как класс, весит как кортеж
Point = namedtuple('Point', 'x y')
p = Point(3, 4)
print(p.x, p.y)
Частые ошибки
- Изменяемый объект как значение по умолчанию: def f(items=[]) — список создаётся один раз и накапливает данные между вызовами.
- Изменение списка во время обхода — элементы пропускаются. Идите по копии: for x in items[:].
- Копирование через b = a: это ссылка на тот же список. Нужен a[:], list(a) или copy.deepcopy для вложенных.
- Присваивание результата sort() или append() — они возвращают None.
- Сравнение чисел с плавающей точкой через ==: 0.1 + 0.2 != 0.3. Используйте math.isclose.
Частые вопросы
Кортеж или список?
Кортеж, если набор значений не должен меняться: координаты, запись из базы, ключ словаря. Он немного быстрее и защищает от случайного изменения. В остальных случаях список.
Чем генератор отличается от списка?
Список хранит все элементы сразу, генератор вычисляет их по одному при обходе. Для миллиона значений это разница между сотнями мегабайт и нулём — но генератор можно обойти только один раз.
Почему словарь ищет за O(1)?
Он основан на хеш-таблице: по ключу вычисляется адрес ячейки, и обращение идёт сразу туда, без перебора. Поэтому ключ обязан быть неизменяемым — иначе его хеш изменится и значение станет недоступным.