Функции
Аргументы, *args/**kwargs, замыкания, lambda и ловушка изменяемого аргумента по умолчанию.
12 вопросов
JuniorТеорияОчень частоЧто такое *args и **kwargs, и когда их применяют?
Что такое *args и **kwargs, и когда их применяют?
*args собирает лишние позиционные аргументы в tuple, а **kwargs — лишние именованные в dict. Если ничего не передали, это пустой tuple/dict, а не None. Их используют для функций с переменным числом аргументов и проброса.
Типичные ошибки
- ✗Считать, что
*args— этоlist, а неtuple - ✗Думать, что отсутствующие
*args/**kwargsравныNone, а не пустым контейнерам - ✗Полагать, что имена
args/kwargs— обязательные ключевые слова, а не соглашение
Уточняющие вопросы
- →Как пробросить
*args/**kwargsбез изменений в другой вызов функции? - →Что делает одиночная
*в сигнатуре с последующими параметрами?
JuniorКодОчень частоЧто выведет этот изменяемый аргумент по умолчанию?
Что выведет этот изменяемый аргумент по умолчанию?
[1], затем [1, 2]. Список [] по умолчанию создаётся один раз, в момент определения функции, и переиспользуется между вызовами, поэтому состояние «протекает». Решение через сентинел: def add(item, bucket=None): bucket = [] if bucket is None else bucket.
Типичные ошибки
- ✗Думать, что аргументы по умолчанию вычисляются заново при каждом вызове
- ✗Ожидать новый список на каждый вызов
- ✗Не знать решение через сентинел None
Уточняющие вопросы
- →Почему аргументы по умолчанию вычисляются в момент определения, а не вызова?
- →Как именно паттерн с сентинелом
Noneэто исправляет?
JuniorТеорияЧастоЧто значит, что функции в Python — объекты первого класса?
Что значит, что функции в Python — объекты первого класса?
Функции — это объекты типа function: их можно присваивать переменным, передавать как аргументы, возвращать, хранить в контейнерах и навешивать атрибуты. Объект создаётся один раз, когда выполняется инструкция def.
Типичные ошибки
- ✗Считать, что функции нельзя передавать или хранить в контейнерах
- ✗Думать, что новый объект функции создаётся при каждом вызове, а не на
def - ✗Полагать, что функции не имеют типа и не могут нести атрибуты
Уточняющие вопросы
- →В чём разница между обращением к
fи вызовомf()? - →Как поддержка функций первого класса делает возможными декораторы?
JuniorТеорияЧастоЧто такое lambda, и в чём её ограничения?
Что такое lambda, и в чём её ограничения?
lambda — это анонимная функция, тело которой — одно выражение, без таких инструкций, как pass, raise или присваивание. Она неявно возвращает значение этого выражения и часто передаётся в map, filter или sorted.
Типичные ошибки
- ✗Думать, что
lambdaможет содержать несколько инструкций или присваивания - ✗Считать, что
lambdaбыстрее эквивалентногоdef - ✗Ожидать явного
returnвнутри телаlambda
Уточняющие вопросы
- →Почему именованный
defможет быть предпочтительнееlambdaдля читаемости? - →Как задать
lambdaаргумент по умолчанию или захватить переменную цикла?
JuniorТеорияЧастоМожно ли определить функцию внутри другой, и где она видна?
Можно ли определить функцию внутри другой, и где она видна?
Да. Вложенная внутренняя функция видна только в локальной области внешней функции; снаружи к ней не добраться, пока её не вернут или не присвоят наружу. Новый внутренний объект создаётся при каждом запуске внешней.
Типичные ошибки
- ✗Считать, что внутренняя функция попадает в глобальное пространство имён модуля
- ✗Думать, что внутренний объект переиспользуется, а не создаётся заново при каждом вызове
- ✗Полагать, что внешний код может вызвать внутреннюю функцию без её возврата
Уточняющие вопросы
- →Как вложенная функция может читать или менять переменную внешней области?
- →Что меняется, если внешняя функция возвращает свою внутреннюю функцию?
MiddleТеорияЧастоКак аргументы передаются в функции в Python?
Как аргументы передаются в функции в Python?
Python передаёт по присваиванию, это называют «call by sharing»: параметр привязывается к тому же объекту, что передал вызывающий. Переприсваивание имени внутри не влияет на вызывающего, а изменение изменяемого объекта — влияет.
Типичные ошибки
- ✗Считать, что Python — чистая передача по значению и копирует каждый аргумент
- ✗Думать, что переприсваивание имени параметра меняет переменную вызывающего
- ✗Полагать, что правило передачи различается для изменяемых и неизменяемых типов
Уточняющие вопросы
- →Как функция может изменить
listвызывающего, но не переприсвоить его переменную? - →Почему присваивание параметру никогда не распространяется к вызывающему?
MiddleТеорияЧастоЧто такое замыкание (closure) в Python?
Что такое замыкание (closure) в Python?
Замыкание — это внутренняя функция, которая через cell-объекты захватывает ссылки на переменные внешней области и держит их живыми после возврата внешней функции. Каждый вызов внешней создаёт новое замыкание со своими привязками.
Типичные ошибки
- ✗Думать, что замыкание копирует значения, а не захватывает ссылки через cell
- ✗Считать, что все замыкания делят одну глобальную привязку вместо привязок на вызов
- ✗Полагать, что захваченные переменные исчезают при возврате внешней функции
Уточняющие вопросы
- →Почему замыкания в цикле часто захватывают одно и то же финальное значение?
- →Когда внутри замыкания нужно ключевое слово
nonlocal?
MiddleТеорияЧастоПочему изменяемый аргумент по умолчанию — ловушка, и как это исправить?
Почему изменяемый аргумент по умолчанию — ловушка, и как это исправить?
Значения по умолчанию вычисляются один раз при выполнении def и хранятся на объекте функции, поэтому изменяемый аргумент по умолчанию вроде def f(x=[]) общий и накапливается между вызовами. Решение: задать None и создавать новый объект внутри.
Типичные ошибки
- ✗Считать, что значения по умолчанию создаются при каждом вызове, а не один раз на
def - ✗Думать, что затронуты только
list, а не все изменяемые объекты - ✗Полагать, что баг про скорость, а не про общее накапливающееся состояние
Уточняющие вопросы
- →Когда изменяемое значение по умолчанию реально полезно, например для кэша?
- →Почему
if x is None: x = []предпочтительнееx = x or []?
MiddleКодИногдаЧто выведет этот список лямбд?
Что выведет этот список лямбд?
[2, 2, 2]. Замыкания захватывают переменную i, а не её значение в момент создания (позднее связывание). К моменту вызова лямбд цикл завершён и i == 2. Решение — связать на каждой итерации через аргумент по умолчанию: [lambda i=i: i for i in range(3)].
Типичные ошибки
- ✗Считать, что замыкания захватывают значение, а не переменную
- ✗Ожидать
[0, 1, 2]из семантики «значение в момент создания» - ✗Не знать решение со связыванием через аргумент по умолчанию
Уточняющие вопросы
- →Почему связывание через аргумент по умолчанию захватывает текущее значение
i? - →Чем это отличается между генераторным выражением и списковым включением?
MiddleКодИногдаЧто возбудит эта функция и почему?
Что возбудит эта функция и почему?
UnboundLocalError: local variable 'x' referenced before assignment. Поскольку x присваивается где-либо в функции (строка 2), Python считает её локальной на всю функцию, поэтому чтение на строке 1 падает. Решение: добавить global x или не затенять её. Это правило области видимости LEGB.
Типичные ошибки
- ✗Думать, что привязка имени решается в точке использования, а не для всей функции
- ✗Ожидать, что строка 1 прочитает глобальную
x - ✗Путать UnboundLocalError с обычным NameError
Уточняющие вопросы
- →Как
globalиnonlocalменяют, в какую область целится присваивание? - →Каковы четыре области в порядке поиска LEGB в Python?
SeniorТеорияИногдаПочему lambda x: raise Exception(x) — это SyntaxError?
Почему lambda x: raise Exception(x) — это SyntaxError?
Тело lambda должно быть выражением, а raise (как pass, return и присваивания) — это инструкция, которую тело лямбды содержать не может. Поэтому ошибка возникает при разборе/компиляции как SyntaxError, ещё до любого вызова.
Типичные ошибки
- ✗Думать, что это ошибка времени выполнения, а не
SyntaxErrorпри разборе - ✗Считать, что
raiseдопустим в телеlambda - ✗Полагать, что ошибка в имени исключения, а не в форме инструкции
Уточняющие вопросы
- →Как заставить
lambdaбросить исключение, используя только выражения? - →Какие ещё конструкции — инструкции, которые
lambdaне может содержать?
SeniorТеорияРедкоПочему внутри функции lst[:] = [1] доходит до вызывающего, а lst = [1] — нет?
Почему внутри функции lst[:] = [1] доходит до вызывающего, а lst = [1] — нет?
lst[:] = [1] — это присваивание срезу: оно изменяет существующий объект, на который ещё ссылается вызывающий, поэтому изменение видно. lst = [1] лишь переприсваивает локальное имя новому объекту; привязка вызывающего не затронута. Разница — в изменении общего объекта против переприсваивания локального имени.
Типичные ошибки
- ✗Путать
lst[:] = [1]сlst = [1]и ждать, что оба дойдут до вызывающего - ✗Считать, что переприсваивание параметра утекает новым объектом к вызывающему
- ✗Не понимать, что присваивание срезу изменяет существующий общий объект
Уточняющие вопросы
- →Чем
lst.clear()отличается отlst[:] = []для объекта вызывающего? - →Почему
lst += [1]доходит до вызывающего, аlst = lst + [1]— нет?