Коллекции в 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вместо ручных индексов