Функции
В Python функция — это не описание кода, которое компилятор куда-то подставляет, а объект, создаваемый в момент выполнения инструкции def и связываемый с именем. Отсюда выводится почти вся тема — и то, почему функцию можно положить в dict и передать в sorted, и то, почему def f(x=[]) копит состояние между вызовами, и то, почему три замыкания из цикла возвращают одно число. Ловушки здесь — не «странности языка», а следствия двух моделей: модели объектов и модели связывания имён.
Питон-специфичных механизмов три, и каждый спрашивают отдельно. Первый — передача аргументов: не по значению и не по ссылке, а «call by sharing»; параметр связывается с тем же объектом, что и у вызывающего. Второй — значения по умолчанию: они вычисляются один раз, когда исполняется def, и лежат на объекте функции в __defaults__, поэтому изменяемый дефолт общий для всех вызовов. Третий — захват переменной, а не значения: вложенная функция держит cell-объект и читает его в момент вызова, откуда и берётся позднее связывание. Разберите каждый механизм в слоях ниже.
Карта темы
- Функции первого класса — функция это объект типа
function, её можно присвоить, передать, вернуть и положить в контейнер. - Функция как объект — что реально лежит на объекте функции —
__name__,__defaults__,__closure__,__code__и пользовательские атрибуты. - Передача аргументов — call by sharing — переприсваивание параметра невидимо вызывающему, изменение общего объекта видно.
- Переменное число аргументов —
*argsсобирает позиционные вtuple,**kwargsименованные вdict, а маркеры/и*задают способ передачи. - Изменяемый аргумент по умолчанию — дефолт вычисляется один раз при
defи живёт на функции; сентинелNone— единственное решение. - Вложенные функции и области видимости — правило LEGB,
UnboundLocalErrorиglobalпротивnonlocal. - Замыкания и ячейки — cell-объекты, захват переменной вместо значения и ловушка позднего связывания в цикле.
- lambda и её границы — тело из одного выражения, отсюда
SyntaxErrorнаraise, и правило выбора междуlambdaиdef.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что Python копирует аргументы «по значению» | Функция, вызвавшая lst.append, молча испортит список вызывающего — копии не было |
| Ждать, что переприсваивание параметра дойдёт до вызывающего | lst = [1] меняет только локальное имя; чтобы изменение стало видно, нужен lst[:] = [1] или lst.append |
Писать def f(x=[]) или def f(d={}) | Объект создаётся один раз при def и переиспользуется — состояние протекает между вызовами |
Чинить изменяемый дефолт через x = x or [] | Переданные пустой список, 0 и "" тихо подменяются новым объектом — правильный сентинел это if x is None |
| Думать, что замыкание копирует значение переменной | Все функции, созданные в цикле, вернут последнее значение счётчика — классический [2, 2, 2] |
Считать *args списком, а отсутствующие *args/**kwargs — None | args это tuple без append, а при отсутствии аргументов — пустой tuple/dict, и проверка is None не сработает |
| Присвоить в теле имени, которое выше читается | Имя становится локальным на всю функцию, и чтение до присваивания падает с UnboundLocalError |
Ставить raise, return или присваивание в тело lambda | SyntaxError при компиляции модуля — тело lambda обязано быть выражением |
Значение для собеседований
Тема идёт в самом начале собеседования и работает как фильтр. Первый вопрос почти всегда один — как в Python передаются аргументы. Ответы «по значению» и «по ссылке» одинаково неверны и сразу переводят разговор в разбор ошибок; правильный звучит как «по присваиванию, call by sharing» и обязан сопровождаться парой примеров — переприсваивание параметра невидимо снаружи, append виден. Дальше дают два кодовых вопроса и просят предсказать вывод: def add(item, bucket=[]) с двумя вызовами и [lambda: i for i in range(3)]. Оба проверяют понимание времени вычисления — дефолт вычисляется при def, замыкание читает ячейку при вызове.
Вторая половина — про сигнатуры и области видимости. Про *args/**kwargs спрашивают на джуниорском уровне, и ловушка тут в типах: tuple и dict, а не list и не None. На middle-уровне добавляют маркеры / и *. По областям видимости дают код с чтением и последующим присваиванием одного имени и ждут UnboundLocalError — решение принимается при компиляции функции, а не в точке использования; типичная ошибка — назвать NameError или сослаться на порядок строк. Глубже спрашивают про nonlocal против global, про содержимое __closure__ и про то, почему lambda не быстрее def. Все три закрываются одним ответом — функция это объект, и всё её состояние видно руками.