Перейти к основному содержимому

Коллекции в Python

list, tuple, set, dict - структуры, на которых держится почти весь код (Django и FastAPI гоняют их постоянно)
Здесь - какую когда брать и питоновские способы с ними работать: срезы, comprehensions, распаковка

Теория​

Коллекции - какую когда​

ТипИзменяемыйУпорядоченКогда
listдадапоследовательность, часто меняется
tupleнетданеизменяемая «запись»; можно ключом dict
setданетуникальность, быстрая проверка in
dictдада (по вставке)пары ключ → значение

list​

nums = [3, 1, 2]
nums[0] # 3 - доступ по индексу, с нуля
nums[-1] # 2 - отрицательный индекс - с конца
nums[10] # IndexError - индекса нет, за границами

nums.append(4) # [3, 1, 2, 4] - добавить в конец
nums.insert(0, 10) # [10, 3, 1, 2, 4] - вставить по индексу
nums.remove(1) # убрать первое вхождение значения 1
nums.pop() # убрать и вернуть последний элемент
nums.pop(0) # убрать и вернуть элемент по индексу

nums.sort() # сортировка НА МЕСТЕ - мутирует список, возвращает None
sorted(nums, reverse=True) # новый отсортированный список, исходный не трогает

len(nums) # количество элементов
3 in nums # проверка вхождения - линейный перебор, O(n)

tuple​

Неизменяемая последовательность - после создания нельзя добавить/удалить/заменить элемент:

point = (10, 20)
point[0] # 10 - индексация работает как у list
point[0] = 5 # TypeError - tuple immutable

Раз неизменяем - можно использовать как ключ dict или элемент set (list для этого не годится):

cache = {(1, 2): 'значение'} # tuple ключом - ок
cache = {[1, 2]: 'значение'} # TypeError: unhashable type: 'list'

set​

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

a = {1, 2, 3}
b = {2, 3, 4}

a | b # {1, 2, 3, 4} - объединение
a & b # {2, 3} - пересечение
a - b # {1} - разность: что есть в a, но нет в b
a ^ b # {1, 4} - симметрическая разность: в одном из двух, не в обоих

a.add(5) # добавить элемент
a.discard(1) # убрать элемент (не бросает ошибку, если его нет - в отличие от .remove())

dict​

Пары ключ → значение, с 3.7+ сохраняет порядок вставки:

d = {'a': 1, 'b': 2}

d['a'] # 1 - доступ по ключу
d['z'] # KeyError - такого ключа нет
d.get('z') # None - не падает, просто нет значения
d.get('z', 0) # 0 - со своим значением по умолчанию вместо None

d.keys() # ключи
d.values() # значения
d.items() # пары (ключ, значение) - удобно для распаковки в for

d.update({'c': 3}) # добавить/обновить сразу несколько пар
d.pop('a') # убрать ключ и вернуть его значение
'a' in d # проверка по КЛЮЧАМ, не по значениям

Срезы​

seq[start:stop:step] - работает для list, str, tuple
Все три компонента необязательны, stop в результат не включается:

xs = [0, 1, 2, 3, 4, 5]
xs[:3] # [0, 1, 2] первые три (start по умолчанию - 0)
xs[-2:] # [4, 5] последние два (отрицательный индекс - с конца)
xs[::-1] # [5, 4, 3, 2, 1, 0] реверс (шаг -1)
xs[::2] # [0, 2, 4] каждый второй
xs[1:4:2] # [1, 3] с 1 по 4 не включая, с шагом 2

В отличие от обычной индексации, срез никогда не бросает IndexError - индексы за границами просто обрезаются по факту:

xs[100:200] # [] - пустой список, а не ошибка
xs[2:100] # [2, 3, 4, 5] - обрезалось до конца списка
xs[10] # IndexError - а вот прямая индексация за границей - падает

Срез создаёт НОВЫЙ объект (копию), исходная коллекция не меняется - удобный способ скопировать список:

ys = xs[:] # полная копия xs (shallow copy)
ys is xs # False - разные объекты в памяти
ys == xs # True - но с одинаковым содержимым

Срезу можно и присваивать - для list это меняет исходный список на месте, причём длина заменяемого куска и нового значения не обязаны совпадать:

xs = [0, 1, 2, 3, 4, 5]
xs[1:3] = [10, 20, 30] # [0, 10, 20, 30, 3, 4, 5] - заменили 2 элемента на 3
xs[1:4] = [] # то же самое, что del xs[1:4] - удаление куска

Comprehensions​

Компактнее и быстрее цикла с append
Общий вид: [выражение for элемент in итерируемое if условие] (if - опционален):

squares = [n * n for n in range(5)] # [0, 1, 4, 9, 16]
evens = [n for n in range(10) if n % 2 == 0] # [0, 2, 4, 6, 8]
by_len = {w: len(w) for w in ['a', 'bb']} # {'a': 1, 'bb': 2}
uniq = {c for c in 'banana'} # {'a', 'b', 'n'}

if в конце - это фильтр (пропускает элементы)
if/else рядом с выражением, в начале - это трансформация (меняет значение, но не пропускает элемент)
Легко перепутать:

[n for n in range(5) if n % 2 == 0] # [0, 2, 4] - фильтр: нечётные пропущены
[n if n % 2 == 0 else -1 for n in range(5)] # [0, -1, 2, -1, 4] - трансформация: элементы все на месте

Comprehension можно вложить - например, развернуть список списков в плоский:

matrix = [[1, 2], [3, 4], [5, 6]]
flat = [n for row in matrix for n in row] # [1, 2, 3, 4, 5, 6]

Круглые скобки вместо квадратных - генераторное выражение: то же самое, но лениво (не строит список целиком в памяти)
Пригождается, когда список сам по себе не нужен - нужен только итог:

Распаковка​

a, b, c = [1, 2, 3] # a=1, b=2, c=3 - строго по порядку, количество должно совпасть
a, b = [1, 2, 3] # ValueError: too many values to unpack

* собирает "остаток" в список - можно поставить в начало, середину или конец:

first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]
first, *middle, last = [1, 2, 3, 4, 5] # first=1, middle=[2, 3, 4], last=5

_ - соглашение для значения, которое не нужно:

_, y = (10, 20) # интересует только y

Поток управления - питоновские идиомы​

names = ['Аня', 'Боря']
for i, name in enumerate(names, start=1):
print(i, name) # 1 Аня / 2 Боря
for num, letter in zip([1, 2], ['x', 'y']):
print(num, letter) # 1 x / 2 y

grade = 'pass' if score >= 60 else 'fail' # тернарный оператор

Итерируемся по коллекции напрямую (не по индексам range(len(...))) - это и есть питоновский стиль

Примеры​

Разобрать и агрегировать список словарей (типичная задача - суммы по пользователю):

orders = [
{'user': 'a', 'sum': 100},
{'user': 'b', 'sum': 50},
{'user': 'a', 'sum': 30},
]

total_by_user = {}
for order in orders:
user = order['user']
user_sum = order['sum']
total_by_user[user] = total_by_user.get(user, 0) + user_sum

# total_by_user == {'a': 130, 'b': 50}

Проверь себя​

1. Чем tuple отличается от list?

tuple неизменяем (нельзя добавить/удалить/заменить элемент) и может быть ключом dict/элементом set
list изменяем
Кортеж - для фиксированных «записей»

2. d['k'] и d.get('k') - в чём разница?

d['k'] бросает KeyError, если ключа нет; d.get('k') вернёт None (или заданный default)

3. Что вернёт [x for x in range(5) if x % 2][::-1]?

[3, 1] - сначала нечётные [1, 3], затем реверс

4. Как обменять значения a и b без временной переменной?

a, b = b, a - распаковка кортежа

5. Почему bool([]) - это False?

Пустые коллекции (как и 0, '', None) ложны по правилам truthiness; непустые - истинны

6. Как слить два словаря в новый?

{**d1, **d2} (или d1 | d2 в Python 3.9+)
При совпадении ключей побеждает правый

7. Что вернёт {1, 2, 3} & {2, 3, 4}?

{2, 3} - пересечение множеств: элементы, которые есть в обоих

8. Почему {[1, 2]: 'x'} бросает TypeError, а {(1, 2): 'x'} - нет?

list изменяем и поэтому не хешируется (unhashable type) - не может быть ключом dict/элементом set
tuple неизменяем - хешируется, можно

9. Почему xs[100:200] не бросает IndexError, а xs[100] - бросает?

Срез просто обрезает границы по факту - выход за пределы даёт пустой или укороченный результат, а не ошибку
Прямая индексация по несуществующему индексу - IndexError

10. Что делает xs[:]?

Возвращает копию всего списка (shallow copy) - новый объект с тем же содержимым
Полезно, когда нужно скопировать список, а не получить вторую ссылку на тот же объект

11. Чем [n for n in xs if n > 0] отличается от [n if n > 0 else 0 for n in xs]?

Первое - фильтр: элементы без n > 0 пропускаются, результат может быть короче xs
Второе - трансформация: длина сохраняется, не подошедшие элементы заменяются на 0, а не пропадают

12. Почему sum(n * n for n in range(1000)) экономнее по памяти, чем sum([n * n for n in range(1000)])?

Круглые скобки - генераторное выражение: значения считаются лениво, по одному, без построения всего списка из 1000 элементов в памяти сразу

13. Что делает greet(*args, **kwargs)?

Разворачивает список/кортеж args в позиционные аргументы вызова, а словарь kwargs - в именованные

Итог / чек-лист​

  • list - меняется, tuple - фиксирован, set - уникальность/in, dict - пары ключ→значение
  • sorted(seq, key=...) - сортировка без мутации исходной коллекции, по любому критерию
  • Операции над set: | объединение, & пересечение, - разность, ^ симметрическая разность
  • Ключом dict/элементом set может быть только хешируемое (неизменяемое) значение - tuple, не list
  • Срезы seq[a:b:step] не бросают IndexError за границами и возвращают копию; xs[:] - скопировать список, срезу можно присваивать (xs[1:3] = [...], xs[1:4] = [] - удаление куска)
  • Comprehensions вместо циклов с append: if в конце - фильтр, if/else у выражения - трансформация; круглые скобки вместо квадратных - генератор (лениво, экономит память)
  • Распаковка: a, *rest (звёздочка - где угодно), _ для ненужного значения, k, v in items(), {**d1, **d2}; *args/**kwargs разворачивают последовательность/словарь при вызове функции
  • Итерируйся по коллекции напрямую; enumerate/zip вместо ручных индексов

Дополнительно​