Функции и аргументы: вопросы для собеседования (часть 4)
Позиционные и именованные аргументы, *args, **kwargs, значения по умолчанию, замыкания, lambda — всё это спрашивают на собеседованиях. Частая ловушка — мутабельный объект как значение по умолчанию. Умение декомпозировать код на функции и правильно работать с аргументами — признак зрелого разработчика.
Вопросы 16–20 из 20
16Какой вариант корректно избегает ловушки с изменяемым значением по умолчанию для списка?
A`def add(x, items=[]): items.append(x); return items` с пустым списком как дефолтом
B`def add(x, items=None):` с инициализацией `items = [] if items is None else items`
C`def add(x, items=()): items.append(x); return items` с пустым кортежем как дефолтом
D`def add(x, items={}): items.append(x); return items` со словарём как дефолтом списка
Ответ: Безопасный паттерн: ставить дефолт `None` и создавать новый список внутри функции при каждом вызове.
Изменяемые дефолты (список, словарь, множество) создаются один раз при определении функции и переиспользуются при каждом вызове. Чтобы избежать общих побочных эффектов между вызовами, ставят `items=None`, а внутри делают инициализацию: `items = [] if items is None else items`. Тогда при каждом вызове без аргумента создаётся новый список, и данные не «накапливаются» неожиданно между вызовами функции.
Подробный разбор → 17Вы пишете обёртку над функцией `render(template, **options)`, которая принимает много опций. Нужно пробрасывать дополнительные опции дальше, но оставить обязательный параметр явным. Какой вариант лучше?
A`def render_page(template, options): return render(template, options)` одной парой
B`def render_page(template, *options): return render(template, *options)` через кортеж
C`def render_page(*args, **kwargs): return render(*args, **kwargs)` универсальной обёртки
D`def render_page(template, **options): return render(template, **options)` с явным `template`
Ответ: В обёртках обязательные параметры делают явными, а остальное пробрасывают через `**kwargs`.
Вариант `def render_page(template, **options): return render(template, **options)` сохраняет читаемость: сразу видно обязательный `template`, при этом `**options` позволяет пробросить любые дополнительные именованные параметры дальше. Полностью «универсальный» `*args, **kwargs` иногда нужен, но ухудшает понимание интерфейса. Вариант с `*options` тоже не подойдёт: он собирает позиционные аргументы в кортеж, а у `render` опции именованные.
Подробный разбор → 18Вы проектируете функцию для аналитического трекинга: обязательны имя события, идентификатор пользователя и время, а дополнительные поля бывают редко и непредсказуемы. Какой дизайн наиболее читаемый и удобный?
A`def track_event(**kwargs): ...` со всеми полями в словаре, включая обязательные
B`def track_event(event_name, user_id, timestamp, **kwargs): ...` с явными ключевыми
C`def track_event(): ...` со сборкой полей внутри тела через глобальный контекст
D`def track_event(event_name, *args, **kwargs): ...` с обязательным только именем
Ответ: Ключевые обязательные параметры лучше делать явными, а «опциональные метаданные» собирать через `**kwargs`.
Если обязательные параметры важны для понимания кода, их стоит прописать явно в сигнатуре: так легче читать, есть автодополнение в IDE, меньше ошибок при вызове. При этом дополнительные редкие поля удобно принимать через `**kwargs` как словарь метаданных. Полностью «всё через `**kwargs`» ухудшает читаемость и усложняет контроль входных данных, а пустая сигнатура с глобальным контекстом — антипаттерн.
Подробный разбор → 19Дана функция `def add_item(item, items=[]): items.append(item); return items`. Что вернёт второй вызов, если выполнить `add_item("a")`, а затем `add_item("b")`?
A`['a', 'b']`: значение по умолчанию переиспользуется, и оба элемента остаются в одном списке
B`['b']`: остаётся только последний элемент, при втором вызове список создаётся заново
C`['a']`: остаётся только первый элемент, второй вызов получает свежую копию пустого списка
D`[]`: пустой список, параметр сбрасывается между вызовами и каждый раз приходит чистым
Ответ: Изменяемое значение по умолчанию создаётся один раз при определении функции и переиспользуется между вызовами.
Значение по умолчанию для `items` (пустой список) создаётся в момент определения функции, а не при каждом вызове. Поэтому первый вызов `add_item("a")` добавляет `"a"` в этот список, а второй `add_item("b")` добавляет `"b"` в тот же список и возвращает `["a", "b"]`. Это частая ловушка — чтобы её избежать, пишут `def add_item(item, items=None): if items is None: items = []`. Тогда новый список создаётся при каждом вызове.
Подробный разбор → 20Вы пишете функцию среднего значения, и в коде чаще всего уже есть список `values` (например, из данных). Какой интерфейс обычно более универсален и читаем?
A`def mean(*values): ...`: готовую коллекцию приходится распаковывать через `*list`
B`def mean(**values): ...`: интерфейс принимает пары ключ-значение, не последовательность
C`def mean(values): ...`: один параметр-итерируемый, который принимает list или tuple
D`def mean(items, weights=None): ...`: два параметра, второй для весовых коэффициентов
Ответ: Если вход чаще уже собран в коллекцию, лучше принимать один параметр-итерируемый, чем заставлять распаковывать в `*args`.
Сигнатура `def mean(values): ...` позволяет передавать список, кортеж или любой другой итерируемый объект напрямую (например, `mean(values)`), без распаковки. Вариант `def mean(*values): ...` удобен, когда вызывают как `mean(1, 2, 3)`, но хуже подходит для данных, которые уже собраны в коллекции — приходится писать `mean(*values)`. Двойные звёздочки `**values` принимают только пары ключ-значение и для чисел не годятся. При проектировании функций для аналитики чаще выигрывает явный параметр `values`.
Подробный разбор →