// python interview / functions

Вопросы на собеседовании: Функции, замыкания и декораторы

Функции как объекты, LEGB, *args/**kwargs, декораторы и functools — любимая территория интервьюеров. Здесь — топ-15 по частоте на реальных собесах: у первых вопросов открыт полный разбор, у остальных — устный эталон. Весь банк темы (28 вопросов) с разборами — в приложении.

Открыть тему в приложении каждый день бесплатно: 3 эталона и 3 проверки арбитром

junior: база, с которой начинают

Что означает, что в Python функции — объекты первого класса? Как это использовать на практике?

def apply(func, value):
    return func(value)

def square(x):
    return x * x

operations = {"square": square, "negate": lambda x: -x}
print(apply(operations["square"], 5))

как ответить Функции в Python — обычные объекты: их можно присвоить переменной, передать как аргумент, вернуть из другой функции, хранить в списках/словарях. У функции есть свои атрибуты (__name__, __doc__), она хранится в памяти как любое значение. Отсюда растут callback'и, декораторы, диспетчеризация по словарю {имя: функция} и сортировка с key=func.

разбор

Ключевое отличие от языков, где функция — не значение: в Python def просто создаёт объект-функцию и привязывает к нему имя в текущей области видимости, как обычное присваивание. Поэтому f = print работает — f получает ссылку на тот же объект, что и print, а не копию. И если потом переприсвоить имя print в текущей области, сам объект-функция не пострадает — f продолжит вызываться как ни в чём не бывало.

Типичные применения, которые стоит назвать:

  • sorted(items, key=len) — функция как аргумент;
  • фабрики функций (make_adder(n) возвращает функцию);
  • словарь-диспетчер вместо цепочки if/elif по строковому имени команды;
  • декораторы — функция, принимающая и возвращающая функцию.

Частая ловушка на практике — перепутать f и f(): передать вызов вместо ссылки на функцию (button.on_click(handler()) вместо handler), тогда код сработает не при клике, а сразу при регистрации. Ещё стоит знать, что методы класса — тоже функции, просто при обращении через экземпляр становятся bound method с автоматически подставленным self.

чтобы прозвучать сильнее Для мидла добавь, что это база функциональных паттернов — каррирование, dependency injection через передачу функций, частичное применение (functools.partial) — и что bound method хранит и функцию, и __self__ как отдельные атрибуты (__func__ и __self__).

python-functions-001 · junior · high

Что такое замыкание в Python и когда оно реально пригождается?

как ответить Замыкание — это вложенная функция, которая запоминает переменные из объемлющей области видимости даже после того, как внешняя функция уже завершила выполнение. Python хранит эти переменные в ячейках (cell-объектах), доступных через атрибут __closure__ вложенной функции, — значение живёт, пока на него есть ссылка. Используется для фабрик функций, декораторов и инкапсуляции состояния без написания класса.

разбор

Классический пример — счётчик без класса:

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

increment продолжает видеть и менять count даже после того, как make_counter вернула управление — потому что count живёт в общей с increment ячейке, а не копируется.

Важно проговорить: замыкание захватывает саму переменную (по ссылке на ячейку), а не значение на момент создания — если исходное имя изменится позже, замыкание увидит новое значение. Это источник классической ловушки с циклами (лямбда в цикле захватывает переменную цикла, а не её значение на итерации) — частый follow-up вопрос. Практическое применение — декораторы с состоянием (счётчик вызовов, кэш) и функции-фабрики стратегий, где не хочется городить отдельный класс ради одного метода.

чтобы прозвучать сильнее Для мидла упомяни cell-объекты в __closure__, разницу между удержанием ссылки и удержанием значения, и что это основа для декораторов с состоянием и мемоизации без глобальных переменных.

python-functions-002 · junior · high

Как Python ищет значение переменной внутри вложенных функций? Расскажи про правило LEGB.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        print(x)
    inner()

outer()

как ответить LEGB — порядок поиска имени: Local (тело текущей функции) → Enclosing (объемлющие функции) → Global (модуль) → Built-in (встроенные имена вроде len). Python идёт по этим областям изнутри наружу и останавливается на первом совпадении; если нигде не нашёл — NameError. Присваивание переменной внутри функции по умолчанию делает её локальной для всей этой функции, даже если снаружи есть переменная с тем же именем.

полный разбор и проверка ответа арбитром — в приложении python-functions-003 · junior · high

Чем позиционные аргументы отличаются от именованных при вызове функции и когда лучше передавать аргумент по имени?

как ответить Позиционные аргументы связываются с параметрами по порядку слева направо, именованные передаются как имя=значение и порядок для них не важен, зато на месте вызова сразу видно, что именно передаётся. Именованные стоит использовать, когда параметров много или они однотипны — например несколько bool-флагов, где create_user(True, False, True) нечитаемо без заглядывания в сигнатуру.

полный разбор и проверка ответа арбитром — в приложении python-functions-004 · junior · high

Расскажи, что такое *args и **kwargs в сигнатуре функции — как они работают и в каких случаях ты их применяешь?

как ответить *args собирает все лишние позиционные аргументы в кортеж, **kwargs — все лишние именованные в словарь; имена args/kwargs — просто соглашение, работает именно звёздочка. Использую их, когда число аргументов заранее не известно или функция должна прозрачно прокидывать всё дальше в другую функцию — классический случай декораторов и обёрток над чужим API: def wrapper(*args, **kwargs): return func(*args, **kwargs).

полный разбор и проверка ответа арбитром — в приложении python-functions-005 · junior · high

Объясни на пальцах, как работает декоратор в Python — что происходит, когда ты пишешь @my_decorator над функцией?

как ответить Декоратор — обычная функция, которая принимает другую функцию и возвращает новую, чаще всего wrapper, добавляющую поведение до или после вызова оригинала. Синтаксис @decorator — это сахар для func = decorator(func), и выполняется он один раз, в момент определения функции, а не при каждом её вызове. Обёртка держит ссылку на исходную функцию через замыкание и обычно принимает *args, **kwargs, чтобы работать с любой сигнатурой.

полный разбор и проверка ответа арбитром — в приложении python-functions-006 · junior · high

Что делает functools.wraps и зачем его вешать на wrapper внутри декоратора? Что конкретно сломается, если про него забыть?

как ответить functools.wraps(func) — это декоратор для wrapper-функции, который копирует на неё __name__, __doc__, __module__, __qualname__ и сохраняет ссылку на оригинал в __wrapped__. Без него задекорированная функция в трейсбеках, help() и интроспекции выглядит как generic 'wrapper', а не как исходная функция — это ломает читаемость логов, документацию и инструменты, которые матчат функции по имени.

полный разбор и проверка ответа арбитром — в приложении python-functions-007 · junior · high

Чем lambda отличается от обычной функции, объявленной через def? Какие у неё ограничения и когда ты вообще возьмёшь lambda, а не def?

как ответить lambda — одно выражение без имени: тело обязано быть expression, а не suite, поэтому туда физически нельзя вставить присваивание, цикл, try/except — только выражение, значение которого возвращается неявно. Использую lambda только для одноразовых функций, которые сразу передаются аргументом: key= в sorted, простой callback. Всё, что не помещается в одну строку или нуждается в docstring и переиспользовании — обычная def.

полный разбор и проверка ответа арбитром — в приложении python-functions-008 · junior · high

Объясни, как работает functools.lru_cache, чем functools.cache от него отличается — и что выведет этот код?

from functools import lru_cache

@lru_cache
def total(numbers):
    return sum(numbers)

print(total([1, 2, 3]))

как ответить lru_cache мемоизирует результат вызова по хэшу позиционных и именованных аргументов и держит не больше maxsize последних результатов, вытесняя старые по LRU. cache — это просто lru_cache(maxsize=None), то есть кэш без ограничения размера и без вытеснения. Здесь код упадёт с TypeError: unhashable type: 'list', потому что list нельзя захэшировать и использовать как ключ кэша — под этими декораторами можно вызывать функцию только с хэшируемыми (обычно иммутабельными) аргументами.

полный разбор и проверка ответа арбитром — в приложении python-functions-009 · junior · high

middle: где отделяют уверенных

Чем отличаются nonlocal и global? Когда без них не обойтись?

x = 10

def outer():
    x = 20
    def inner():
        nonlocal x
        x = 30
    inner()
    print(x)

outer()
print(x)

как ответить global говорит интерпретатору, что присваивание внутри функции должно менять переменную модуля, а не создавать локальную. nonlocal делает то же самое, но для ближайшей объемлющей не-глобальной функции — то есть нужен, чтобы менять переменную, захваченную замыканием. Без этих ключевых слов присваивание всегда создаёт новую локальную переменную, даже если имя совпадает с внешним, — на чтение это не влияет, они нужны только когда переменной присваивают новое значение.

полный разбор и проверка ответа арбитром — в приложении python-functions-013 · middle · high

Что делают * и / в сигнатуре функции — как объявить keyword-only и positional-only параметры и зачем это вообще нужно?

как ответить Одиночная * в сигнатуре означает, что всё после неё можно передать только по имени — keyword-only. Одиночная / — наоборот: всё до неё можно передать только позиционно, без имени — positional-only, например def f(a, b, /, c, *, d). Это способ зафиксировать контракт вызова: не дать вызывающему коду завязаться на конкретное имя параметра, которое ты, возможно, захочешь переименовать в будущем без breaking change.

полный разбор и проверка ответа арбитром — в приложении python-functions-014 · middle · high

Если у функции несколько декораторов в столбик, в каком порядке они выполняются? Разбери, что выведет этот код.

def bold(func):
    def wrapper(*args, **kwargs):
        print("bold before")
        result = func(*args, **kwargs)
        print("bold after")
        return result
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        print("italic before")
        result = func(*args, **kwargs)
        print("italic after")
        return result
    return wrapper

@bold
@italic
def greet():
    print("hello")

greet()

как ответить Декораторы в столбик применяются снизу вверх на этапе определения — @bold @italic def f() эквивалентно f = bold(italic(f)) — но выполняются снаружи внутрь на этапе вызова. Для этого кода вывод: bold before, italic before, hello, italic after, bold after — верхний декоратор оборачивает всё остальное, поэтому его код срабатывает первым и последним.

полный разбор и проверка ответа арбитром — в приложении python-functions-015 · middle · high

Как работает key= в sorted/min/max и почему в Python 3 это единственный способ задать порядок сравнения? Как отсортировать по нескольким полям сразу?

как ответить key= принимает функцию от одного аргумента, которая вычисляется один раз на элемент заранее (decorate-sort-undecorate), а не на каждое сравнение — поэтому сложность остаётся O(n log n) по сравнениям и O(n) по вызовам key, в отличие от старого cmp=, которого в Python 3 больше нет вообще. Сортировка по нескольким полям — это key=lambda x: (x.a, x.b) с кортежем, а для смешанного направления по числовым полям — унарный минус: key=lambda x: (x.a, -x.b).

полный разбор и проверка ответа арбитром — в приложении python-functions-016 · middle · high

Что выведет этот код и почему? Как поправить, чтобы получить [0, 1, 2]?

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])

как ответить Выведет [2, 2, 2]. lambda в цикле не захватывает значение i на момент создания — она захватывает саму переменную i, замыкание ссылается на общую ячейку (cell) в объемлющей области видимости, а for переиспользует одну и ту же i на каждой итерации. К моменту вызова любой lambda цикл уже закончился и i равна 2, поэтому все три вызова возвращают одно и то же последнее значение. Чинится default-аргументом lambda i=i: i — так значение копируется в момент создания функции, а не читается позже.

полный разбор и проверка ответа арбитром — в приложении python-functions-017 · middle · high

Что будет, если повесить @lru_cache на метод класса, и почему это иногда проблема?

как ответить lru_cache оборачивает функцию метода один раз на уровне класса, а self входит в ключ как обычный хешируемый аргумент. Кэш удерживает сильные ссылки на self, остальные аргументы и результат, пока запись не вытеснена или не вызван cache_clear(). Поэтому экземпляр может жить дольше ожидаемого, но стандартный @lru_cache ограничен maxsize=128 — это не безусловная утечка до конца жизни класса. Неограниченный рост возможен при maxsize=None; кэш общий для всех экземпляров, а cache_clear() очищает его целиком.

полный разбор и проверка ответа арбитром — в приложении python-functions-018 · middle · high

Соседние темы того же собеса