Почти все junior-задачи по Python проверяют одно: понимаете ли вы, что переменная — ссылка на объект, когда объект создаётся и когда изменяется. Проговаривайте это вслух, называйте сложность решения и проверяйте граничные случаи: пустой вход, повторы, равные ключи.
Задачи 1–2: список списков, copy и deepcopy, is и ==
Первая задача: что напечатает grid после grid[0][0] = 1, если сетку создали как [[0] * 3] * 3? Частый ответ — изменится только первая строка. На самом деле внешнее умножение копирует ссылки, все три строки — один и тот же список, и вывод [[1, 0, 0], [1, 0, 0], [1, 0, 0]]. Генератор списка создаёт три разных объекта, и тогда меняется только первая строка.
Вторая задача про копии словаря с вложенным списком. После config["tags"].append("sql") поверхностная копия видит новый тег, потому что copy.copy скопировал только внешний словарь, а глубокая — нет; замена config["level"] не задевает ни одну копию. Последняя строка, True False False, отделяет равенство значений от идентичности: срез создаёт новый список. Интервьюер проверяет модель «имя → объект»; изменяемые аргументы по умолчанию разобраны в отдельной статье.
import copy
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)
config = {"tags": ["python"], "level": "junior"}
shallow = copy.copy(config)
deep = copy.deepcopy(config)
config["tags"].append("sql")
config["level"] = "middle"
print(shallow)
print(deep)
a = [1, 2]
b = [1, 2]
print(a == b, a is b, a is a[:])Задача 3: самые частые слова
Задача: найти два самых частых слова в строке без учёта регистра и знаков препинания. Короткое решение — re.findall с шаблоном \w+ и Counter.most_common(2). Вывод: [('код', 3), ('тесты', 3)]. Почему «код» идёт раньше «тестов» при равных частотах? Документация Counter обещает, что элементы с одинаковым счётом выводятся в порядке первого появления, — интервьюер может спросить именно это.
Вторая строка вывода, код True, показывает ручной вариант через dict.get и max с key=manual.get: его просят, чтобы проверить, понимаете ли вы, что делает Counter внутри. Частые ошибки: забыть lower() и посчитать «Код» и «код» разными словами, разбивать по пробелу и получить «тесты.» с точкой, сортировать весь словарь ради одного максимума. Сложность — O(n) по длине текста.
import re
from collections import Counter
text = "Код ревью, код тесты. Ревью — и снова код; тесты, тесты."
words = re.findall(r"\w+", text.lower())
counts = Counter(words)
print(counts.most_common(2))
manual = {}
for w in words:
manual[w] = manual.get(w, 0) + 1
print(max(manual, key=manual.get), manual == counts)
print(Counter("abracadabra").most_common(3))Задача 4: генератор списка и генераторное выражение
Что напечатает sum(squares_gen) дважды подряд? Многие отвечают «14 14», но генераторное выражение — одноразовый итератор: первый проход его исчерпывает, и вывод 14 0. Список хранит значения и даёт 14 14. Квадратные скобки сразу строят весь список, круглые — объект generator, который вычисляет элементы по запросу и не держит их в памяти. Правило выбора простое: нужен один проход и данных много — генератор; нужно обращаться по индексу, считать len() или обходить несколько раз — список.
Вторая часть проверяет ленивость функций-генераторов. Вызов read_lines() не выполняет тело: сначала печатается «до next», только next() приводит к «открыли файл» и first, а list() забирает остаток ['second']. Отсюда растут вопросы про построчное чтение больших файлов, про yield и про то, почему генератор нельзя перемотать — нужно создать новый. Хороший ответ на junior-уровне: генератор хранит состояние между вызовами next(); повторный обход исчерпанного генератора через for, sum или list просто ничего не даёт, а явный next() бросит StopIteration.
squares_list = [x * x for x in range(4)]
squares_gen = (x * x for x in range(4))
print(squares_list, squares_gen.__class__.__name__)
print(sum(squares_gen), sum(squares_gen)) # второй проход пустой
print(sum(squares_list), sum(squares_list))
def read_lines():
print("открыли файл")
yield "first"
yield "second"
lines = read_lines() # тело ещё не выполнялось
print("до next")
print(next(lines))
print(list(lines))Задача 5: сортировка по нескольким ключам
Отсортируйте людей по возрасту по убыванию, а при равном возрасте — по имени. Решение в одну строку — кортеж в key с отрицанием числового поля: key=lambda p: (-p[1], p[0]). Второй способ опирается на стабильность сортировки, которую гарантирует документация: сначала сортировка по вторичному ключу, потом по основному. Обе первые строки вывода совпадают: Борис 31, Иван 31, Аня 25, Оля 25.
Частая ошибка — проходы в обратном порядке: третья строка отсортирована по имени, возраст потерян. Ещё одна ловушка: nums.sort() сортирует на месте и возвращает None, поэтому print(nums.sort(), nums) печатает None [1, 2, 3]. Если спросят про русские имена, вспомните, что строки сравниваются по кодовым точкам Unicode: «ё» окажется после «я», и для словарного порядка нужен свой ключ.
people = [("Оля", 25), ("Иван", 31), ("Аня", 25), ("Борис", 31)]
# возраст по убыванию, при равном возрасте — имя по алфавиту
print(sorted(people, key=lambda p: (-p[1], p[0])))
# то же через стабильность: сначала вторичный ключ, потом основной
by_name = sorted(people, key=lambda p: p[0])
print(sorted(by_name, key=lambda p: p[1], reverse=True))
# частая ошибка: сортировки в обратном порядке
wrong = sorted(people, key=lambda p: p[1], reverse=True)
print(sorted(wrong, key=lambda p: p[0]))
nums = [3, 1, 2]
print(nums.sort(), nums)Задача 6: lambda в цикле и замыкания
Классика: три функции lambda: i, созданные в цикле, и вызов каждой. Ожидают [0, 1, 2], получают [2, 2, 2]. Lambda запоминает не значение, а переменную i из охватывающей области, и к моменту вызова цикл закончился с i = 2. Этот случай разобран в официальном FAQ Python. Исправления — аргумент по умолчанию lambda i=i: i, который вычисляется при создании функции, или functools.partial.
Вторая половина примера — замыкание со состоянием: make_counter возвращает inc, а nonlocal позволяет менять count. Вывод 1 2 2 — два вызова и содержимое ячейки замыкания. Без nonlocal присваивание count += 1 сделало бы count локальной переменной, и первый вызов упал бы с UnboundLocalError: cannot access local variable 'count' where it is not associated with a value. Интервьюер проверяет, отличаете ли вы создание функции от её вызова.
handlers = [lambda: i for i in range(3)]
print([h() for h in handlers])
handlers = [lambda i=i: i for i in range(3)] # значение по умолчанию фиксирует i
print([h() for h in handlers])
from functools import partial
def show(i):
return i
handlers = [partial(show, i) for i in range(3)]
print([h() for h in handlers])
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
c = make_counter()
print(c(), c(), c.__closure__[0].cell_contents)Задачи 7–8: палиндром и удаление из словаря в цикле
Палиндром без учёта регистра и знаков: оставить символы, для которых str.isalnum() истинно, привести через casefold и сравнить со срезом [::-1]. Вывод True, True, False, и первая проверка — «А роза упала на лапу Азора». casefold, в отличие от lower, по документации предназначен именно для сравнения без учёта регистра. Дополнительный вопрос интервьюера — решить за O(1) памяти двумя указателями.
Восьмая задача: удалить из словаря товары с нулевым остатком. Удаление прямо в цикле по словарю падает с RuntimeError: dictionary changed size during iteration — документация предупреждает, что изменение словаря во время обхода может вызвать эту ошибку. Правильные варианты — собрать новый словарь выражением {name: qty for … if qty > 0} или обходить копию list(stock). Результат: {'pear': 3}.
def is_palindrome(s: str) -> bool:
chars = [ch.casefold() for ch in s if ch.isalnum()]
return chars == chars[::-1]
print(is_palindrome("А роза упала на лапу Азора"))
print(is_palindrome("Was it a car or a cat I saw?"))
print(is_palindrome("python"))
stock = {"apple": 0, "pear": 3, "plum": 0}
try:
for name in stock:
if stock[name] == 0:
del stock[name]
except RuntimeError as e:
print("RuntimeError:", e)
stock = {"apple": 0, "pear": 3, "plum": 0}
stock = {name: qty for name, qty in stock.items() if qty > 0}
print(stock)Как тренироваться и что ещё повторить
Прогоняйте задачи так: прочитайте код, запишите предсказанный вывод, запустите, а при расхождении объясните причину одной фразой — про ссылки, ленивость или момент вычисления. Затем поменяйте одну деталь, например генератор списка на умножение списка, и снова предскажите. Таблица собирает частые неверные ответы: если вы узнали в ней свой вариант, вернитесь к задаче через пару дней и решите её заново, не глядя в разбор.
На самом собеседовании проговаривайте не только ответ, но и проверку: «запущу на пустом списке и на повторах». Для широты пройдите сборник из ста вопросов по Python и план подготовки. Изменяемые аргументы по умолчанию — ещё одна частая ловушка junior-собеседования, и мы не повторяем её здесь: у неё есть отдельный разбор.
| Задача | Неверный ответ | Верно |
|---|---|---|
| [[0] * 3] * 3 | меняется одна строка | меняются все три |
| copy.copy(config) | копия независима | вложенный список общий |
| sum(gen) дважды | 14 14 | 14 0 |
| lambda: i в цикле | [0, 1, 2] | [2, 2, 2] |
| print(nums.sort()) | [1, 2, 3] | None |
| del в цикле по dict | удалит нули | RuntimeError |
Коротко
Частые вопросы
Нужны ли junior-разработчику алгоритмы на Python-собеседовании?
Базовые — да: оценка сложности, словари и множества вместо вложенных циклов, сортировка с ключом. Объём алгоритмической части зависит от компании, поэтому уточняйте формат у рекрутера. Фраза «это O(n): один проход и поиск в словаре за O(1) в среднем» уместна на любом уровне.
Можно ли на собеседовании пользоваться Counter и itertools?
Обычно да, и это плюс: знание стандартной библиотеки — часть ответа. Будьте готовы реализовать то же вручную, если интервьюер попросит, и назвать сложность: Counter строится за один проход по входу.
Что делать, если не знаешь, что выведет код?
Рассуждайте вслух: какие объекты создаются, какие имена на них ссылаются, когда вычисляется выражение. Даже при неверном итоге интервьюер увидит модель мышления. Затем предложите проверить гипотезу маленьким примером в REPL — это нормальная инженерная практика.
Чем copy.copy отличается от среза или list()?
Для списка срез, list(x) и copy.copy(x) дают одинаковую поверхностную копию: новый внешний список с теми же элементами. Разница видна на произвольных объектах: класс может управлять копированием через __copy__ и __deepcopy__, а срез поддерживают только последовательности.
Источники
Источники и редакционная политика
Сведения проверены 7 ноября 2026 г. Ссылки рядом с разделами указывают источники фактов и технических объяснений. Выводы, учебные сценарии и рекомендации по подготовке — редакционная работа RecallDeck.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.