Введение

Техническое собеседование — это проверка не столько памяти, сколько глубины понимания фундаментальных концепций языка. Интервьюера интересует не заученный ответ, а ваша способность рассуждать и понимать внутренние механизмы Python.

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

Цель статьи — систематизировать знания по основным темам и подготовить вас к демонстрации не только синтаксиса, но и логики работы языка.

Вопрос 1: В чем ключевое различие между list и tuple?

Краткий ответ:
Ключевое различие заключается в изменяемости (mutability). list — изменяемый тип данных, его содержимое можно изменить после создания. tuple — неизменяемый (immutable), его изменить нельзя.

Развернутый ответ:
Из-за неизменяемости у tuple есть важные следствия и отличия от list:

  1. Хешируемость (Hashability): tuple является хешируемым объектом, поэтому его можно использовать в качестве ключа в словаре (dict) или как элемент множества (set). list не является хешируемым.

  2. Производительность: Как правило, tuple занимает меньше памяти и создается быстрее, чем list с теми же элементами.

  3. Семантика и назначение: По принятому соглашению, list используется для хранения гомогенных (однородных) последовательностей данных, а tuple — для гетерогенных (разнородных), фактически играя роль структуры или записи (например, (ip_address, port)).

Что проверяет этот вопрос:
Понимание фундаментальной концепции изменяемых и неизменяемых типов в Python. Это базовое знание, которое влияет на безопасность данных, производительность и архитектуру кода.

Вопрос 2: Опишите разницу между list, set и dict. В каких случаях вы будете использовать каждый из них?

Краткий ответ:

  • list — упорядоченная, индексируемая коллекция, допускающая дубликаты.

  • set — неупорядоченная коллекция, содержащая только уникальные элементы.

  • dict — неупорядоченная (в Python < 3.7) коллекция пар «ключ-значение» с уникальными ключами.

Развернутый ответ (сценарии использования):

  • list (список): Используется, когда важен порядок элементов или когда допустимы дубликаты.

    • Пример: Последовательность шагов в алгоритме, история транзакций, список комментариев к посту.

  • set (множество): Используется, когда нужна уникальность элементов и порядок не важен. Также он оптимизирован для операций проверки на вхождение и математических операций с множествами (объединение, пересечение).

    • Пример: Хранение уникальных ID пользователей, определение общих интересов у двух пользователей, удаление дубликатов из списка.

  • dict (словарь): Используется для хранения связанных данных в виде пар «ключ-значение» для быстрого доступа к значению по ключу.

    • Пример: Представление объекта (e.g., {'id': 1, 'name': 'John Doe'}), кэширование результатов вычислений, сопоставление данных из разных источников.

Что проверяет этот вопрос:
Способность кандидата выбрать адекватную и эффективную структуру данных для решения конкретной задачи. Неправильный выбор может привести к усложнению кода и снижению производительности.

Вопрос 3: Проанализируйте следующий код. Что он выведет и почему?

def add_item(item, some_list=[]):
    some_list.append(item)
    return some_list

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

Краткий ответ:
Код выведет [1] и затем [1, 2]. Проблема заключается в том, что изменяемый объект (list) используется в качестве значения по умолчанию.

Развернутый ответ:
Аргументы по умолчанию в Python вычисляются и создаются один раз — в момент определения (интерпретации) функции, а не при каждом её вызове.

  1. Когда def add_item(...) исполняется, в памяти создается один объект-список [], и ссылка на него сохраняется как значение по умолчанию для параметра some_list.

  2. Первый вызов add_item(1) использует этот единственный экземпляр списка, добавляет в него 1 и возвращает [1].

  3. Второй вызов add_item(2) использует тот же самый экземпляр списка, который уже содержит 1, добавляет в него 2 и возвращает [1, 2].

Идиоматически правильное решение — использовать None в качестве "сигнального" значения по умолчанию:

def add_item(item, some_list=None):
    if some_list is None:
        some_list = []
    some_list.append(item)
    return some_list

Что проверяет этот вопрос:
Это один из важнейших "проверочных" вопросов. Он тестирует глубокое понимание жизненного цикла объектов в Python и разницы между временем определения функции и временем её вызова. Непонимание этого механизма — частый источник трудноуловимых багов.

Вопрос 4: Что такое *args и **kwargs и для чего они используются?

Краткий ответ:
Это специальные синтаксические конструкции для передачи в функцию произвольного числа аргументов. *args собирает все неименованные (позиционные) аргументы в кортеж (tuple). **kwargs собирает все именованные (ключевые) аргументы в словарь (dict).

Развернутый ответ:
Порядок их использования в сигнатуре функции строго определен: сначала идут обычные аргументы, затем *args, затем **kwargs.

  • *args (arguments): Позволяет передать переменное количество позиционных аргументов.

  • **kwargs (keyword arguments): Позволяет передать переменное количество именованных аргументов.

Пример:

def process_data(required_arg, *args, **kwargs):
    print(f"Обязательный аргумент: {required_arg}")
    print(f"Позиционные аргументы (*args): {args}")
    print(f"Именованные аргументы (**kwargs): {kwargs}")

process_data(
    "START",
    10, 20, "go",
    status="active",
    user_id=123
)

Вывод:

Обязательный аргумент: START
Позиционные аргументы (*args): (10, 20, 'go')
Именованные аргументы (**kwargs): {'status': 'active', 'user_id': 123}

Что проверяет этот вопрос:
Знание синтаксиса определения функций и понимание, как создавать гибкие и расширяемые API. Это особенно важно при работе с декораторами, наследованием и передачей аргументов между функциями.

Вопрос 5: Что такое comprehensions? Приведите пример для списка.

Краткий ответ:
Comprehensions (включения) — это лаконичный и читаемый синтаксис для создания коллекций (list, dict, set) на основе итерируемых объектов. Они позволяют заменить громоздкие циклы for одной выразительной строкой кода.

Развернутый ответ:
Наиболее распространенный пример — list comprehension (списковое включение). Оно объединяет цикл for, опциональное условие if и выражение для создания нового элемента в одну конструкцию.

Сравнение:

  • Традиционный подход с циклом for:

    squares_of_evens = []
    for i in range(10):
        if i % 2 == 0:
            squares_of_evens.append(i * i)
    
  • Идиоматичный подход с list comprehension:

    squares_of_evens = [i * i for i in range(10) if i % 2 == 0]
    

Оба фрагмента кода дают одинаковый результат ([0, 4, 16, 36, 64]), но второй вариант считается более "питоничным", так как он декларативен (описывает что нужно получить, а не как) и часто работает быстрее.

Что проверяет этот вопрос:
Знакомство кандидата с идиомами языка Python. Умение использовать comprehensions — это маркер того, что разработчик пишет не просто работающий, а чистый, лаконичный и эффективный код.

Вопрос 6: Чем f-строки отличаются от метода .format() и почему они предпочтительнее?

Краткий ответ:
F-строки (форматированные строковые литералы) — это современный, более быстрый и удобный способ форматирования строк, представленный в Python 3.6.

Развернутый ответ:
Основные преимущества f-строк перед .format():

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

    • .format(): "User {name} has ID {user_id}." .format(name=user_name, user_id=uid)

    • f-string: f"User {user_name} has ID {uid}."

  2. Производительность: F-строки, как правило, работают быстрее, поскольку они парсятся и вычисляются на этапе выполнения, а не через вызов метода.

  3. Возможность встраивания выражений: Внутри фигурных скобок f-строки можно размещать любые валидные Python-выражения, что невозможно в .format() без предварительного вычисления.

    • Пример: f"The final price is {price * (1 + tax_rate):.2f}."

Что проверяет этот вопрос:
Знание современных возможностей языка. Использование f-строк вместо устаревших методов (.format() или %) показывает, что кандидат следит за развитием Python и применяет актуальные лучшие практики.

Вопрос 7: Что такое __init__ и self в контексте классов Python?

Краткий ответ:
__init__ — это специальный метод-конструктор, который вызывается для инициализации нового экземпляра класса. self — это общепринятое имя для первого параметра метода, который представляет собой ссылку на сам экземпляр класса.

Развернутый ответ:

  • __init__(self, ...): Это один из "магических" методов Python. Он автоматически вызывается сразу после создания объекта в памяти. Его основная задача — установить начальное состояние объекта, то есть присвоить значения его атрибутам (полям).

  • self: Это не ключевое слово, а соглашение об именовании. Через self методы экземпляра получают доступ к его атрибутам и другим методам. Когда вы вызываете my_object.my_method(arg1), Python автоматически преобразует этот вызов в MyClass.my_method(my_object, arg1), передавая сам объект первым аргументом.

Пример:

class Dog:
    def __init__(self, name):
        # self.name создает атрибут 'name' для этого конкретного экземпляра
        self.name = name

    def bark(self):
        # self.name используется для доступа к атрибуту экземпляра
        print(f"{self.name} says woof!")

my_dog = Dog("Rex")  # Вызов __init__ с name="Rex"
my_dog.bark()       # Python передает my_dog в качестве 'self'

Что проверяет этот вопрос:
Понимание самых основ объектно-ориентированного программирования (ООП) в Python. Без четкого осознания роли конструктора и ссылки на экземпляр невозможно корректно создавать и использовать классы.

Вопрос 8: Что такое генератор? В чем его ключевое преимущество перед функцией, возвращающей список?

Краткий ответ:
Генератор — это итератор, который порождает значения "на лету" с помощью ключевого слова yield, не сохраняя их все в памяти. Его главное преимущество — эффективность по памяти.

Развернутый ответ:
Когда обычная функция с return возвращает список, она сначала должна полностью сформировать этот список в памяти. Если данных много, это может привести к высокому потреблению RAM или даже к MemoryError.

Функция-генератор использует yield. Когда yield вызывается, функция "приостанавливает" свое выполнение и отдает значение. При следующем запросе значения выполнение возобновляется с того же места. Таким образом, в памяти одновременно находится только одно значение.

Сценарий использования:
Обработка больших файлов, работа с бесконечными последовательностями, сложные конвейеры обработки данных.

Пример:

# Функция, возвращающая список (неэффективно для больших n)
def list_first_n_squares(n):
    result = []
    for i in range(n):
        result.append(i*i)
    return result

# Функция-генератор (эффективно по памяти)
def gen_first_n_squares(n):
    for i in range(n):
        yield i*i

# Использование:
# sum(list_first_n_squares(100_000_000)) # Потенциально вызовет MemoryError
# sum(gen_first_n_squares(100_000_000)) # Работает без проблем

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

Вопрос 9: Что такое декоратор в Python?

Краткий ответ:
Декоратор — это функция, которая принимает другую функцию в качестве аргумента, расширяет её поведение, не изменяя её исходный код, и возвращает новую, "обернутую" функцию.

Развернутый ответ:
Декораторы являются реализацией концепции функций высшего порядка и активно используют замыкания. Синтаксис с символом @ (@my_decorator) — это "синтаксический сахар" для более явного вызова my_function = my_decorator(my_function).

Типичные сценарии использования:

  • Логирование вызовов функций и времени их выполнения.

  • Кэширование результатов (мемоизация).

  • Проверка прав доступа или аутентификации.

  • Валидация аргументов функции.

Пример (декоратор для замера времени выполнения):

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Функция {func.__name__} выполнялась {end_time - start_time:.4f} секунд")
        return result
    return wrapper

@timer_decorator
def heavy_calculation(n):
    """Что-то долго вычисляет."""
    sum(i*i for i in range(n))

heavy_calculation(10_000_000)

Что проверяет этот вопрос:
Понимание более сложных концепций языка: функции как объекты первого класса, замыкания, функции высшего порядка. Умение читать и понимать декораторы — необходимый навык для работы с большинством современных Python-фреймворков (Flask, Django, FastAPI и др.).

Вопрос 10: Что такое GIL и как он влияет на многопоточность в Python?

Краткий ответ:
GIL (Global Interpreter Lock) — это глобальный замок интерпретатора, мьютекс, который в реализации CPython позволяет только одному потоку исполнять Python-байт-код в любой момент времени внутри одного процесса.

Развернутый ответ:
Основная задача GIL — упростить управление памятью в CPython, защищая доступ к Python-объектам и предотвращая состояние гонки на уровне C-реализации. Однако у этого есть важное следствие для параллельных вычислений:

  • Для CPU-bound задач (интенсивные вычисления): Модуль threading не обеспечивает реального параллелизма на многоядерных процессорах. Из-за GIL потоки, выполняющие вычисления, будут исполняться поочередно, а не одновременно, и производительность не увеличится, а может даже незначительно упасть из-за накладных расходов на переключение контекста.

  • Для I/O-bound задач (ожидание сети, диска): threading остается эффективным инструментом. Когда поток выполняет блокирующую операцию ввода-вывода (например, делает сетевой запрос), он освобождает GIL, позволяя другому потоку выполняться в это время. Это создает псевдо-параллелизм, который значительно повышает производительность приложений, работающих с сетью или файловой системой.

Для достижения истинного параллелизма при решении CPU-bound задач в Python следует использовать модуль multiprocessing, который обходит GIL путем создания отдельных процессов, каждый со своим собственным интерпретатором Python и пространством памяти.

Что проверяет этот вопрос:
Это продвинутый вопрос для Junior-позиции, и знание о GIL — серьезный плюс. Он проверяет, понимает ли кандидат ограничения стандартной модели многопоточности в Python и осознает ли разницу между конкурентностью и параллелизмом. Ответ на этот вопрос показывает, что разработчик задумывается о производительности и может выбрать правильный инструмент (threading или multiprocessing) в зависимости от типа задачи.

Заключение

Помните, что техническое собеседование — это диалог, а не экзамен. Важнее не зазубрить ответы, а продемонстрировать ход мысли и понимание базовых принципов, стоящих за кодом. Умение рассуждать и объяснять свой выбор ценится гораздо выше, чем мгновенный, но поверхностный ответ.

Безусловно, некоторые из представленных ответов могут быть спорными, а подходы к решению — различаться в зависимости от проекта или команды. Если вы с чем-то не согласны, заметили неточность или хотите предложить свой, более элегантный вариант ответа, я всегда открыт к обсуждению. Для таких технических дискуссий у меня есть группа в Telegram

Удачи на собеседованиях!

Комментарии (6)


  1. KonstantinTokar
    23.10.2025 17:45

    Этот же набор вопросов можно (частично) использовать для обоснования того, что питон был плохо спроектирован.


    1. GBR-613
      23.10.2025 17:45

      Ну как плохо... Изначально он предполагался не для того, чтобы делать на нем что-то большое и сложное, а для того, чтобы делать что-то маленькое и очень быстро. И для этого он был спроектирован лучше некуда.

      С Javascript то же самое.


    1. avkritsky
      23.10.2025 17:45

      А какие именно вопросы? Я, максимум, мог бы только про третий вопрос так сказать.


      1. KonstantinTokar
        23.10.2025 17:45

        ответил ниже, промахнулся


  1. KonstantinTokar
    23.10.2025 17:45

    3,5,7,8,9,10

    Некоторые пункты спорные, но если бы их не было было бы лучше.


  1. nagadit
    23.10.2025 17:45

    1. Хешируемость (Hashability): tuple является хешируемым объектом, поэтому его можно использовать в качестве ключа в словаре (dict) или как элемент множества (set). list не является хешируемым.

    Доказывать на собеседовании, что tuple хешируемый - главная ошибка.

    Просто запусти вот это и все станет понятно:

    a = tuple([[1,2,3],1])
    d = {a:5}

    Хешируемость и изменяемость — это совершенно разные понятия никак не связанные друг с другом.

    У tuple (как и у большинства контейнеров в python), хеш берется не от самого контейнера, а от элементов внутри него.

    Хешируемость определяется банальными магическими методами eq и hash. Ты можешь даже list сделать хешируемым))

    Изменяемость же определятся логикой работы этих объектов в CPython.

    p.s. Доп вопрос на собес: если ты создашь свой класс, например:

    class A:
      pass

    А потом создашь его экземпляр и добавишь как ключ в словарь, то все сработает:

    a = A()
    
    d = {a:1}

    Но сразу после этого вы можете добавить полей в этот класс:

    a.lol = 10

    и при этом его хеш не сломается:

    print(d[a]) -> 1

    Как же так? - велком в коменты