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