← Все публикации

Python на собеседовании junior-разработчика: как тренировать ответы и задачи

Interview Ninja

На техническом собеседовании недостаточно узнать знакомые слова в вопросе. Важно суметь объяснить их своими словами и применить в небольшой задаче. Ниже разберём несколько основ Python и покажем, как на них тренироваться.

Сначала посмотрите требования вакансии и выберите темы, которые в ней упоминаются. На каждый вопрос попробуйте ответить вслух, а код сначала продумайте сами. Затем проверьте результат, в том числе на пустом вводе и других пограничных случаях, и сверитесь с документацией. Вопросы зависят от команды: эта подборка не гарантирует, что на встрече спросят именно их.

Начинающий разработчик делает заметки и указывает на карточки во время подготовки к собеседованию.

Иллюстрация самостоятельной подготовки к собеседованию.

Теоретические вопросы по Python

Ниже базовые темы, с которых удобно начать подготовку к junior-собеседованию. Сначала попробуйте ответить своими словами, а затем сравните ответ с объяснением. К каждому вопросу мы добавили, что с его помощью могут проверять, и на чём полезно остановиться в ответе.

Это не ключ к мыслям любого интервьюера: один и тот же вопрос разные люди задают с разной целью. Не нужно угадывать идеальную формулировку. Назовите основную мысль, приведите короткий пример и будьте готовы объяснить его или уточнить условие.

Чем == отличается от is

== проверяет равенство объектов. Для встроенных списков это означает сравнение их содержимого. is проверяет, ссылаются ли переменные на один и тот же объект.

a = [1, 2]
b = [1, 2]

print(a == b)  # True: содержимое списков одинаковое
print(a is b)  # False: это два разных списка

Уточняющий вопрос: почему обычно пишут value is None, а не value == None?

None обозначает специальный единственный объект. Оператор is проверяет именно его идентичность, тогда как поведение == может определяться самим типом сравниваемого объекта. Например, value is None удобно использовать, когда нужно проверить, передано ли значение в функцию.

Что могут проверять: различаете ли вы равенство значений и идентичность объекта. Полезно не ограничиваться словами "одно сравнивает значения, другое адреса", а показать два равных списка из примера и объяснить, почему это разные объекты.

Что значит "изменяемый объект"

Список можно изменить на месте: например, добавить элемент с помощью append. Строку изменить таким способом нельзя: если нужна другая строка, создают значение с нужным содержимым.

numbers = [1, 2]
numbers.append(3)
print(numbers)  # [1, 2, 3]

text = 'кот'
# text[0] = 'К'  # TypeError: элемент строки нельзя заменить

Уточняющий вопрос: можно ли изменить список внутри кортежа?

Да. Сам кортеж нельзя изменить так, чтобы заменить его элементы, но его элементом может быть список, который остаётся изменяемым. Это важно учитывать при копировании и передаче объектов в функции.

Что могут проверять: понимаете ли вы разницу между заменой элемента кортежа и изменением объекта внутри него. В ответе можно сказать: "Кортеж хранит тот же список, но содержимое списка может измениться". Так вы объясните нюанс, а не просто перечислите типы.

Чем список отличается от кортежа?

list можно менять после создания: добавлять, удалять и заменять элементы. У tuple нельзя заменить или удалить отдельный элемент. Оба типа сохраняют порядок элементов и допускают повторения. Например, [1, 2] можно дополнить методом append, а (1, 2) нет.

При этом неизменяемый кортеж может содержать изменяемый список, как в вопросе выше.

Как развить ответ: приведите пример, где данные нужно дополнять, и пример, где достаточно неизменяемой последовательности. Если вас спросят, что выбрать, уточните задачу: одного правила "кортеж всегда лучше" нет.

Чем словарь отличается от множества?

Словарь dict хранит пары "ключ: значение": например, {'имя': 'Анна'}. По ключу можно получить связанное с ним значение. Множество set хранит уникальные элементы без соответствующих значений: например, {1, 2, 2} содержит только 1 и 2. На порядок элементов множества полагаться не стоит.

Ключи словаря и элементы множества должны быть хешируемыми. Например, строку можно использовать как ключ, а изменяемый список нельзя. Об этих структурах подробнее рассказывает руководство Python.

Что могут проверять: умеете ли вы выбрать структуру под задачу. Например, для количества повторов слов пригодится словарь, а чтобы оставить только уникальные слова, можно использовать множество. Если важен исходный порядок, скажите об этом: простое преобразование в set не сохраняет его как требуемый результат.

Что такое list comprehension?

Это способ создать новый список из элементов итерируемого объекта, при необходимости преобразовав или отфильтровав их. Например, [x * 2 for x in [1, 2, 3]] создаст [2, 4, 6]. Такой же результат можно получить обычным циклом; выбирайте запись, которую легко прочитать. Синтаксис с примерами есть в документации Python.

Как объяснить: сначала скажите, что получится новый список, затем прочитайте пример по шагам: берём каждое x, умножаем на два и собираем результаты. Если запись стала слишком сложной, можно предложить обычный цикл: важно объяснить решение, а не показать самую короткую строку.

Для чего нужны *args и **kwargs?

В определении функции *args собирает дополнительные позиционные аргументы в кортеж, а **kwargs собирает дополнительные именованные аргументы в словарь. Имена args и kwargs распространены по соглашению; важны сами * и **.

Например, в def show(*args, **kwargs): ... вызов show(1, 2, name='Анна') даст внутри функции args == (1, 2) и kwargs == {'name': 'Анна'}. Эти же операторы используются и при вызове для распаковки последовательности или словаря; контекст определения и вызова важно различать.

Что могут уточнить: что окажется внутри функции после конкретного вызова. В ответе разделите позиционные и именованные аргументы и назовите тип каждой собранной коллекции. Так вы покажете смысл звёздочек, а не только расшифруете сокращения.

Как работают исключения try и except?

Код в try может вызвать исключение. Подходящий except позволяет его обработать, например превратить ошибку преобразования строки в понятное сообщение. Обычно стоит ловить конкретное ожидаемое исключение, а не скрывать все ошибки подряд. Блок finally, если он указан, выполняется при выходе из try, в том числе когда возникло исключение.

try:
    number = int('не число')
except ValueError:
    print('Нужно ввести число')

Как объяснить: назовите операцию, которая может не получиться, и ожидаемое исключение. В примере int() не может преобразовать эту строку, поэтому обрабатываем ValueError. Не стоит обещать, что этот except исправит любую другую ошибку программы.

Что такое итератор и генератор?

Итератор выдаёт элементы по одному при вызове next(). Когда элементов больше нет, он сообщает об этом исключением StopIteration; цикл for обрабатывает завершение сам.

Функция с yield создаёт генератор: её выполнение продолжается по мере получения следующих значений и приостанавливается на yield. Генератор является итератором. Например, (x * 2 for x in [1, 2]) создаёт генераторное выражение, а не готовый список [2, 4].

Что могут проверять: понимаете ли вы, когда появляются значения и что происходит после окончания итерации. Можно предложить пример с двумя вызовами next(), а затем объяснить, почему третий вызов для такого генератора вызовет StopIteration. Не обязательно начинать с деталей реализации, если вопрос был о базовой разнице.

Что такое класс и объект?

Класс описывает поведение и данные своих экземпляров. Объект-экземпляр создаётся на основе класса и может хранить собственные значения атрибутов. Например, у двух экземпляров класса User могут быть разные имена, при этом оба используют методы, определённые в классе.

Это только начало: интервьюер может уточнить, как работает метод или зачем нужен self. Конкретную глубину подготовки выбирайте по вакансии.

Как развить ответ: покажите разницу на двух пользователях с разными именами. Если спросят про обычный метод экземпляра, объясните, что через self он получает доступ к тому экземпляру, для которого вызван. Не нужно сразу перечислять все возможности ООП, если вопрос был только о классе и объекте.

Задача 1. Почему список запоминает предыдущий вызов

Посмотрите на функцию и попробуйте предсказать результат до запуска:

def add_item(value, items=[]):
    items.append(value)
    return items

print(add_item(1))
print(add_item(2))

Если печатать результат сразу после каждого вызова, увидим:

[1]
[1, 2]

Значение по умолчанию создаётся один раз при определении функции. Оба вызова без аргумента items используют один и тот же список. Поэтому второй вызов добавляет 2 к уже существующему [1].

Что здесь могут проверять: замечаете ли вы, что вызовы используют один объект, и можете ли объяснить, когда создано значение по умолчанию. Удобно рассуждать по шагам: "Первый вызов добавляет 1 в общий список. Второй получает тот же список и добавляет 2". Если ответ получился неожиданным, можно проговорить предположение и проверить его на простом примере, а не делать вид, что уверены.

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

def add_item(value, items=[]):
    items.append(value)
    return items

first = add_item(1)
second = add_item(2)
print(first)   # [1, 2]
print(second)  # [1, 2]
print(first is second)  # True

Если для каждого вызова без списка нужен новый список, можно использовать None:

def add_item(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

Теперь add_item(1) и add_item(2) вернут отдельные списки. Но если явно передать свой список, функция добавит элемент именно в него. Если по условию входные данные менять нельзя, понадобится другое решение. Подробнее механизм описан в документации Python о значениях аргументов по умолчанию.

Задача 2. Посчитайте повторения слов

Напишите функцию, которая получает список слов и возвращает словарь: ключом будет слово, а значением станет число его появлений.

Например:

['кот', 'пёс', 'кот'] → {'кот': 2, 'пёс': 1}
[] → {}

Прежде чем писать код, уточните условие: слова точно передаются списком строк? Нужно ли считать 'Кот' и 'кот' одним словом? Здесь предположим список строк и сохранение регистра. Если интервьюер задаст другие условия, решение можно будет изменить.

Попробуйте объяснить план вслух: "Заведу словарь со счётчиками, пройду по словам и для каждого увеличу значение. Если слова ещё нет, начну с нуля". Это показывает ход решения ещё до того, как вы вспомнили точный синтаксис get().

Один из вариантов кода:

def count_words(words):
    counts = {}
    for word in words:
        counts[word] = counts.get(word, 0) + 1
    return counts

counts.get(word, 0) возвращает уже записанное число или 0, если слова ещё нет в словаре. После этого мы добавляем единицу и сохраняем новое значение.

Проверьте себя на трёх случаях:

  • пустой список даёт пустой словарь;
  • повторяющееся слово увеличивает свой счётчик;
  • 'Кот' и 'кот' считаются разными словами, если условие задачи не требует игнорировать регистр.

Если интервьюер просит не учитывать регистр, уточните, как именно нормализовать слова, прежде чем менять решение. Основы работы со словарями есть в руководстве Python.

Что могут проверять: как вы уточняете требования, выбираете структуру данных и проверяете свой код. Если спросят про сложность, можно пояснить: мы один раз проходим по n словам; если считать среднюю стоимость обращения к словарю и хеширования слова постоянной, решение занимает O(n) времени и до O(k) дополнительной памяти; k здесь означает число разных слов. Это оценка при названных допущениях, а не гарантия для любого поведения хеширования или произвольной длины слов.

Почему копия списка не всегда независима

Поверхностная копия создаёт новый внешний список, но его элементы остаются ссылками на те же объекты. Попробуйте объяснить вывод:

original = [[1], [2]]
copy = original.copy()
copy[0].append(3)

print(original)          # [[1, 3], [2]]
print(original is copy)  # False

Внешние списки разные, но первый внутренний список у них общий. Поэтому изменение через copy[0] видно и в original. Здесь полезный уточняющий вопрос: достаточно ли копировать только внешний контейнер или по условию нужно отдельно скопировать и вложенные объекты?

Как рассуждать вслух: "copy() создал новый внешний список, но его первый элемент ссылается на тот же вложенный список. append меняет вложенный объект, поэтому изменение видно через обе переменные". Так ответ объясняет причину вывода, а не только называет его.

Как провести тренировку

Выберите тему из вакансии и задайте себе один вопрос, например: "Почему здесь нужен is None?". Ответьте вслух, затем решите небольшую задачу и объясните, что произойдёт на пустом вводе или при повторном вызове. После этого сверитесь с документацией, запишите пробел и попробуйте ответить ещё раз без подсказки.

На самой встрече можно держаться простой последовательности:

  1. Уточните неясное условие, прежде чем делать предположения.
  2. Коротко опишите идею решения и почему выбрали её.
  3. Напишите код или объясните ответ по шагам.
  4. Проверьте результат на простом и граничном примере; если заметили ошибку, скажите об этом и исправьте.
  5. Если чего-то не знаете, обозначьте это и расскажите, что вы можете проверить или какие варианты рассматриваете.

Например, в задаче со словами можно начать так: "Уточню: регистр важен? Если да, буду считать слова как есть. Для подсчёта использую словарь: для каждого слова увеличу счётчик. Проверю пустой список и повтор". Интервью проходит по-разному: это не сценарий, который нужно произносить наизусть, а способ сделать свои рассуждения понятными собеседнику.

Если вы использовали Interview Ninja во время разговора, вернитесь к доступной истории. В посте о сохранённых чатах и экспорте описано, как сохранить историю в HTML или Markdown. Запись и распознавание могут быть неполными, поэтому отделяйте свои слова от ответа ИИ и при необходимости дополняйте заметки по памяти. Если истории нет, обычных заметок тоже достаточно.

В статье о разборе после собеседования мы показываем, как превратить затруднение в тему для следующей подготовки. Другие материалы проекта можно найти в Telegram-канале Interview Ninja. Не передавайте ИИ чужие закрытые задания, персональные данные и секреты компании.

Начните с одного вопроса из этой статьи: закройте объяснение и попробуйте рассказать ответ своими словами. Затем откройте пример, запустите его и проверьте, что вы правильно предсказали результат.