Перейти к содержанию
Языки

Задачи на собеседовании Python для junior: 8 разборов с выводом и ошибками

На junior-собеседовании по Python редко просят сложный алгоритм. Чаще показывают короткий фрагмент и спрашивают, что он напечатает, или просят написать функцию на десять строк и объяснить решение. Ниже восемь таких задач: сначала ответьте сами, затем сверьтесь с выводом. У каждой есть частый неверный ответ и пояснение, что на самом деле проверяет интервьюер. Код запускался на Python 3.12 и 3.14, вывод совпал.

Автор: Опубликовано

8 мин чтенияРедакционный разборОбновлено
  • Python
  • junior
  • собеседование
  • задачи
Главная мысль

Почти все 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 1414 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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку