junior: база, с которой начинают
Расскажи, чем list отличается от tuple и в каких случаях ты выберешь кортеж вместо списка?
как ответить List — изменяемый динамический массив: можно добавлять, удалять и заменять элементы. Tuple неизменяем: после создания состав и длина фиксированы, зато он легче по памяти и хешируем, поэтому может быть ключом словаря или элементом множества. Tuple беру для фиксированных «записей» вроде координат или возврата нескольких значений из функции, list — когда коллекция будет меняться или её длина заранее неизвестна.
разбор Обе структуры — массивы указателей на объекты, поэтому доступ по индексу в обеих O(1). Разница в контракте: list поддерживает append, pop, присваивание по индексу и потому перевыделяет память с запасом; tuple создаётся один раз ровно нужного размера, из-за чего компактнее и чуть быстрее создаётся.
Ключевое практическое следствие — хешируемость: tuple можно класть в set и использовать ключом dict, list — нельзя. Но tuple хешируем, только если хешируемы все его элементы: (1, [2]) в ключ словаря не пойдёт.
Типичная ловушка, которой добивают на follow-up: неизменяемость tuple — поверхностная (shallow). Кортеж хранит ссылки, и если внутри лежит список, содержимое этого списка менять можно — «заморожены» только сами ссылки.
Есть и семантическая конвенция: tuple — гетерогенная запись фиксированной структуры ((lat, lon), строка из БД), list — однородная последовательность произвольной длины. Если нужны именованные поля, следующая ступень — namedtuple или dataclass(frozen=True).
чтобы прозвучать сильнее Упомяни, что неизменяемость tuple — поверхностная (внутри могут лежать изменяемые объекты), и что list перевыделяет память с запасом, а tuple — нет, поэтому на больших объёмах однотипных «записей» кортежи заметно экономят память.
python-collections-001 · junior · high Как dict устроен внутри и за счёт чего доступ по ключу работает за O(1)?
как ответить Dict — это хеш-таблица: от ключа берётся hash(), по нему вычисляется слот в массиве, коллизии разрешаются открытой адресацией — пробированием других слотов. Поэтому поиск, вставка и удаление в среднем O(1), в худшем случае при массовых коллизиях — O(n). Ключи обязаны быть хешируемыми, а сам dict гарантированно сохраняет порядок вставки.
разбор Схема поиска: hash(key) → индекс слота → если слот занят чужим ключом, пробируем следующие слоты по детерминированной последовательности (open addressing с «пертурбацией» битов хеша). Сравнение ключей идёт сначала по identity, потом через ==.
CPython использует компактную раскладку: отдельно разреженный индексный массив, отдельно плотный массив записей в порядке вставки — отсюда и гарантированный порядок ключей при итерации, и экономия памяти. Когда таблица заполняется примерно на две трети, происходит resize с перехешированием — поэтому вставка тоже амортизированная O(1).
Типичные follow-up'ы:
- почему list не может быть ключом — он изменяемый и не хешируемый: если бы ключ «уехал» после вставки, его было бы не найти;
- контракт
__hash__/__eq__: равные объекты обязаны иметь равный хеш; если в классе переопределить __eq__ и не задать __hash__, объект перестаёт быть хешируемым; - что деградирует до O(n): все ключи с одинаковым хешем попадают в одну цепочку проб — на этом строятся hash-DoS-атаки, поэтому хеш строк в Python рандомизирован между запусками.
чтобы прозвучать сильнее Расскажи про compact dict (индексный массив + плотный массив записей — отсюда порядок вставки и экономия памяти), порог заполнения ~2/3 с resize и рандомизацию хеша строк как защиту от hash-DoS.
python-collections-002 · junior · high Что такое set, как он устроен и в каких задачах ты его реально используешь вместо списка?
как ответить Set — коллекция уникальных хешируемых элементов, внутри та же хеш-таблица, что у dict, только без значений. Проверка x in s, добавление и удаление — в среднем O(1), тогда как у списка in — это O(n) линейный проход. Беру его для дедупликации, быстрых проверок принадлежности и операций над множествами: пересечение, объединение, разность.
полный разбор и проверка ответа арбитром — в приложении
python-collections-003 · junior · high Что может быть ключом словаря или элементом множества, а что — нет, и почему вообще есть такое ограничение?
как ответить Ключом dict и элементом set может быть только хешируемый объект: у него есть __hash__, возвращающий одно и то же значение всю жизнь объекта, и согласованный с ним __eq__. Хешируемы неизменяемые встроенные типы — числа, строки, bytes, кортежи из хешируемых элементов, frozenset; изменяемые list, dict, set — нет. Ограничение нужно потому, что dict ищет ключ по его хешу: изменился хеш — ключ уже не найти.
полный разбор и проверка ответа арбитром — в приложении
python-collections-004 · junior · high Смотри на функцию с аргументом по умолчанию — что выведут два вызова и почему?
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item(1))
print(add_item(2))
как ответить Выведет [1], потом [1, 2]. Значение по умолчанию вычисляется один раз — в момент выполнения def, и один и тот же список живёт в объекте функции между вызовами. Оба вызова мутируют его. Правильный паттерн — сентинел: bucket=None и внутри if bucket is None: bucket = [].
полный разбор и проверка ответа арбитром — в приложении
python-collections-005 · junior · high Чем поверхностная копия отличается от глубокой — copy vs deepcopy — и когда ты берёшь какую?
как ответить Shallow copy — copy.copy, list(a), a[:], dict.copy — создаёт новый внешний контейнер, но элементы в нём те же самые объекты, копируются только ссылки. deepcopy рекурсивно копирует и вложенные объекты, так что структуры полностью независимы. Shallow хватает, когда элементы иммутабельны или общие вложенные объекты — это ок; deepcopy нужен для вложенных изменяемых структур, но он заметно дороже.
полный разбор и проверка ответа арбитром — в приложении
python-collections-006 · junior · high Присвоил b = a, где a — список, добавил элемент в b — и он появился в a. Расскажи, что тут происходит и как получить независимую копию.
как ответить Присваивание в Python никогда не копирует: оно привязывает ещё одно имя к тому же объекту. a и b — два алиаса одного списка, id(a) == id(b), поэтому мутация через любое имя видна через оба. Независимую копию даёт a.copy(), list(a) или срез a[:] — это shallow copy; для вложенных структур — copy.deepcopy.
полный разбор и проверка ответа арбитром — в приложении
python-collections-007 · junior · high Чем генераторное выражение отличается от list comprehension и когда на практике берёшь одно, а когда другое?
как ответить List comprehension сразу строит весь список в памяти, генераторное выражение возвращает ленивый итератор — элементы вычисляются по одному, когда их запрашивают, память почти константная. Генератор беру для одного прохода по большим данным и передачи в sum, any, max; список — когда нужны len, индексация или несколько проходов. Важно: генератор одноразовый — после исчерпания он пуст.
полный разбор и проверка ответа арбитром — в приложении
python-collections-008 · junior · high Расскажи, как под капотом работает цикл for в Python — что такое iterable и iterator и чем они отличаются?
как ответить for вызывает у объекта iter(), получает итератор и дальше зовёт на нём next(), пока тот не бросит StopIteration — это и есть протокол итерации. Iterable — всё, у чего есть __iter__ и что можно обходить (список, строка, dict); iterator — объект с __next__, который сам хранит позицию и отдаёт элементы по одному. Список можно обойти много раз, потому что iter() каждый раз даёт свежий итератор, а генератор — сам итератор, поэтому он одноразовый.
полный разбор и проверка ответа арбитром — в приложении
python-collections-009 · junior · high Почему склеивать строки через += в цикле — плохая идея и что использовать вместо этого?
как ответить Строки в Python иммутабельны: каждый += создаёт новую строку и копирует в неё обе части, поэтому в цикле получается квадратичная сложность O(n²). Правильный способ — накапливать куски в список и в конце склеить одним вызовом ''.join(parts): он заранее считает итоговую длину и собирает результат за O(n). Для пары-тройки строк разницы нет — там нормальны и f-строки.
полный разбор и проверка ответа арбитром — в приложении
python-collections-010 · junior · high Смотри на код: кортеж неизменяемый, внутри него список. Что выведет и почему?
t = (1, [2, 3])
try:
t[1] += [4, 5]
except TypeError:
print("TypeError")
print(t)
как ответить Выведет TypeError, а затем (1, [2, 3, 4, 5]) — исключение будет, но список внутри кортежа всё равно изменится. t[1] += [4, 5] — это два шага: сначала __iadd__ списка расширяет его на месте, потом Python пытается записать результат обратно в t[1], и вот это присваивание в кортеж падает. Мутация к тому моменту уже произошла.
полный разбор и проверка ответа арбитром — в приложении
python-collections-011 · junior · medium middle: где отделяют уверенных
Ты переопределяешь у своего класса __eq__. Расскажи про контракт __hash__ и __eq__ — что нужно соблюсти, чтобы объекты корректно работали в dict и set?
как ответить Главное правило: если a == b, то hash(a) обязан быть равен hash(b); обратное не требуется — равные хеши у неравных объектов это просто коллизия. Если определить __eq__ и не трогать __hash__, Python сам выставит __hash__ = None, и объект станет нехешируемым. Поэтому хеш определяют по тем же полям, что участвуют в __eq__, и только по неизменяемым — обычно hash от кортежа полей или dataclass(frozen=True).
полный разбор и проверка ответа арбитром — в приложении
python-collections-013 · middle · high Расскажи, как живёт генератор между вызовами next — и что он умеет, кроме простой выдачи значений?
как ответить Генератор хранит замороженный фрейм — локальные переменные и точку останова на yield, и каждый next() продолжает исполнение с этого места. Кроме next есть send(), который возобновляет генератор и делает yield выражением с переданным значением; throw() бросает исключение в точке остановки; close() кидает GeneratorExit, давая отработать finally. Плюс yield from делегирует вложенному генератору, прозрачно пробрасывая send и исключения.
полный разбор и проверка ответа арбитром — в приложении
python-collections-014 · middle · high Расскажи про модуль collections: чем defaultdict, Counter и deque отличаются от обычных dict и list, и когда ты их реально берёшь?
как ответить defaultdict при обращении к отсутствующему ключу вызывает default_factory и вставляет результат — удобно для группировок: d[key].append(x) без проверок. Counter — словарь-счётчик: принимает iterable, отдаёт most_common(n), поддерживает сложение и вычитание счётчиков. deque — двусторонняя очередь на связанных блоках: append/pop с обоих концов за O(1), тогда как list.pop(0) — O(n); плюс maxlen превращает её в кольцевой буфер для «последних N».
полный разбор и проверка ответа арбитром — в приложении
python-collections-015 · middle · high Что выведет этот код и почему? Какую роль здесь играет стабильность сортировки?
records = [("math", 90), ("eng", 90), ("math", 70), ("eng", 80)]
records.sort(key=lambda r: r[1], reverse=True)
records.sort(key=lambda r: r[0])
print(records)
как ответить Выведет [('eng', 90), ('eng', 80), ('math', 90), ('math', 70)]. Сортировка в Python стабильна: элементы с равным ключом сохраняют взаимный порядок. Первый проход выстраивает по баллам по убыванию, второй сортирует по предмету — и внутри каждого предмета порядок по баллам из первого прохода не разрушается. Это стандартный приём многоключевой сортировки: несколько проходов от младшего ключа к старшему.
полный разбор и проверка ответа арбитром — в приложении
python-collections-016 · middle · high