Шпора.tech

Блог

Вопросы на собеседовании Python-разработчика: 25 вопросов с разбором

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

Списков «100 вопросов по Python» в интернете достаточно, и толку от них мало: заученное определение декоратора слышно сразу. Интервьюер почти всегда задаёт следующий вопрос — «а зачем нужен functools.wraps?» — и на нём заготовка кончается.

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

Типы данных и модель памяти

Какие типы изменяемые, какие нет?

Вопрос кажется школьным, но за ним тянется половина остальных.

Почему список меняется, а строка нет Неизменяемые int, str, tuple, frozenset, bytes Операция создаёт новый объект, исходный остаётся прежним Изменяемые list, dict, set, bytearray Меняются на месте — вызывающий увидит изменения у себя Отсюда классическая ловушка — изменяемый аргумент по умолчанию: def f(x, acc=[]): # список создаётся ОДИН раз при определении функции acc.append(x) # и копится между вызовами Правильно: acc=None, а внутри — if acc is None: acc = []
Изменяемые и неизменяемые типы в Python: что происходит при передаче в функцию

Неизменяемые: int, float, str, tuple, frozenset, bytes. Изменяемые: list, dict, set, bytearray.

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

Что не так с этим кодом?

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

Классическая ловушка. Список создаётся один раз — в момент определения функции, а не при каждом вызове. Поэтому второй вызов add_item("b") вернёт ["a", "b"], чего никто не ждал.

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

Если после этого спросят, где такое поведение полезно, — ответ есть: кеширование через изменяемый аргумент по умолчанию иногда пишут намеренно, хотя functools.lru_cache честнее.

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

== сравнивает значения, is — тождественность объектов в памяти.

Уточняющий вопрос почти всегда про мелкие целые: a = 256; b = 256; a is b даёт True, а с 257False. Python кеширует небольшие числа от −5 до 256. Правильная реакция на это — не «надо запомнить», а «поэтому на is сравнивают только с None, True и False».

Как работает копирование?

Присваивание копию не делает — это второе имя для того же объекта. copy.copy() делает поверхностную копию: новый контейнер, но те же вложенные объекты. copy.deepcopy() копирует рекурсивно.

a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)  # [[1, 2, 99], [3, 4]] — вложенный список общий

Функции и декораторы

Что такое декоратор?

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

import functools, time

def timed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            print(f"{fn.__name__}: {time.perf_counter() - start:.3f}s")
    return wrapper

Здесь две детали, которые и проверяют. functools.wraps переносит на обёртку имя и докстроку оригинала — без него fn.__name__ станет "wrapper", и это ломает всё, что полагается на интроспекцию, от логов до Sphinx. А try/finally гарантирует, что время замерится даже при исключении.

Что такое замыкание?

Функция, которая помнит переменные из объемлющей области видимости после того, как та завершилась.

def counter():
    n = 0
    def inc():
        nonlocal n   # без nonlocal будет UnboundLocalError
        n += 1
        return n
    return inc

nonlocal здесь — то, ради чего вопрос задают.

Чем *args отличается от **kwargs?

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

Генераторы и итераторы

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

Генератор вычисляет элементы по одному и не хранит их все в памяти. Списковое включение на миллион элементов займёт память под миллион, генераторное — под один.

sum(x * x for x in range(10_000_000))  # память постоянная
sum([x * x for x in range(10_000_000)])  # ~400 МБ

Цена — по генератору нельзя пройти дважды и нельзя взять len().

Что такое протокол итератора?

Объект итерируем, если у него есть __iter__, возвращающий итератор. Итератор — объект с __next__, который отдаёт следующий элемент и бросает StopIteration, когда элементы кончились. Цикл for — синтаксический сахар над этим.

Что делает yield?

Превращает функцию в генератор: при вызове тело не выполняется, а возвращается объект-генератор. Каждый next() исполняет тело до следующего yield и замораживает состояние.

GIL и параллелизм

Что такое GIL?

Global Interpreter Lock — блокировка, из-за которой в один момент времени байт-код CPython исполняет только один поток. Многопоточность не ускоряет вычисления на нескольких ядрах.

Дальше почти всегда спрашивают, зачем тогда потоки нужны. Ответ: GIL освобождается на операциях ввода-вывода. Пока поток ждёт ответа сети или диска, остальные работают — поэтому для запросов к API потоки помогают, а для перемножения матриц нет.

Для вычислений — multiprocessing или библиотеки, отпускающие GIL в сишном коде (NumPy). Для ввода-вывода — потоки или asyncio. В Python 3.13 появилась экспериментальная сборка без GIL, но в проде это пока редкость — упомянуть стоит, опираться в ответе нет.

Чем asyncio отличается от потоков?

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

Классы

Что такое MRO?

Method Resolution Order — порядок, в котором Python ищет метод при множественном наследовании. Считается алгоритмом C3, посмотреть можно через ClassName.__mro__. Отсюда же требование вызывать super().__init__(), а не Base.__init__(self): второе ломает цепочку при ромбовидном наследовании.

Чем @staticmethod отличается от @classmethod?

@classmethod первым аргументом получает класс и потому корректно работает с наследованием — типичное применение — альтернативные конструкторы. @staticmethod не получает ничего и по сути просто лежит в пространстве имён класса.

class User:
    @classmethod
    def from_json(cls, raw):
        return cls(**json.loads(raw))   # у наследника вернёт наследника

Что такое __slots__?

Объявление фиксированного набора атрибутов. Экземпляр перестаёт иметь __dict__, что экономит память — заметно, когда объектов миллионы. Цена: нельзя добавить атрибут на лету, и есть тонкости с наследованием.

Как работает менеджер контекста?

Объект с __enter__ и __exit__. Второй вызывается всегда — в том числе при исключении, и если он вернёт истинное значение, исключение будет подавлено.

from contextlib import contextmanager

@contextmanager
def transaction(conn):
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise

Практические вопросы

Как найти узкое место в медленном коде?

Вопрос про подход, а не про инструмент. Хороший ответ: сначала измерить (cProfile, line_profiler), потом смотреть на алгоритмическую сложность, и только потом на микрооптимизации. Ответ «переписал бы на C» без замеров — плохой знак.

Чем словарь лучше списка для поиска?

Поиск в словаре — в среднем константа, в списке — линейный проход. На тысяче элементов разницы не заметишь, на миллионе она решает всё. Стоит упомянуть, что в худшем случае (коллизии) словарь деградирует до линейного, хотя на практике это редкость.

Как устроен set и когда он нужен?

Хеш-таблица без значений. Проверка вхождения за константу, автоматическая уникальность. Классическое применение на собеседовании — убрать дубликаты или найти пересечение двух коллекций за линейное время вместо квадратичного.

Как отвечать, если вопрос незнаком

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

Рабочий вариант — сказать, что не сталкивались, и рассуждать вслух от того, что знаете.

Как это звучит

«С __slots__ в проде не работал. Из названия и из того, что у объектов обычно есть __dict__, предположу, что это способ зафиксировать набор атрибутов и сэкономить память. Так?»

Это сильный ответ: видно, что человек умеет выводить из первых принципов, а не только вспоминать.

Коротко

  • За каждым базовым вопросом стоит уточняющий — готовьте не определение, а следствие.
  • Изменяемый аргумент по умолчанию, functools.wraps, nonlocal, MRO — вопросы, где видно опыт.
  • GIL: не мешает вводу-выводу, мешает вычислениям.
  • На вопросы о производительности отвечайте «сначала измерить».
  • Незнакомый вопрос — рассуждайте вслух от известного, не выдумывайте.

Шпора — ИИ-помощник, который слышит вопрос интервьюера и за секунду выводит опору для ответа. Работает на вашем компьютере с любой видеосвязью.

Попробовать бесплатно
Читать дальше

Вопросы по SQL на собеседовании: 20 задач с разбором решений