Функция в Python это объект первого класса: её можно положить в переменную, передать аргументом, вернуть из другой функции. На собесе проверяют три вещи: как передаются аргументы, как ищутся имена и что происходит со значениями по умолчанию.
Виды параметров
def f(a, b=2, /, c=3, *args, d, e=5, **kwargs):
...
a, bдо/только позиционные (3.8+). Удобно, когда имя параметра не часть API.cобычный: можно и позиционно, и по имени.*argsсобирает лишние позиционные аргументы в кортеж.d, eпосле*args(или голой*) только именованные.**kwargsсобирает лишние именованные в словарь.
При вызове * и ** наоборот распаковывают: f(*items, **options).
Как передаются аргументы
Ни «по значению», ни «по ссылке» в смысле C++. Функция получает ссылку на тот же объект (иногда это называют call by sharing). Изменить переданный список можно, а перепривязать переменную вызывающего нельзя:
def add(lst):
lst.append(1) # видно снаружи
lst = [99] # снаружи не видно: перепривязали локальное имя
Ловушка изменяемого значения по умолчанию
Значения по умолчанию вычисляются один раз, при выполнении def, и хранятся в f.__defaults__.
def append_to(x, bucket=[]):
bucket.append(x)
return bucket
append_to(1) # [1]
append_to(2) # [1, 2] тот же список!
Правильно: bucket=None, а внутри if bucket is None: bucket = []. Так же ведёт себя def f(now=datetime.now()): время зафиксируется при импорте модуля.
LEGB: где ищется имя
Поиск идёт по четырём уровням: Local (функция), Enclosing (внешние функции), Global (модуль), Built-in (len, print).
Важное правило: если в функции есть присваивание имени, это имя локальное во всей функции, даже до строки присваивания:
count = 0
def inc():
count += 1 # UnboundLocalError
Чтобы менять глобальную переменную, нужно global count, а переменную внешней функции nonlocal count.
Замыкания
Вложенная функция запоминает переменные внешней функции (они лежат в ячейках, видно через f.__closure__).
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
Замыкание связывает переменную, а не значение. Отсюда классическая ловушка позднего связывания:
fs = [lambda: i for i in range(3)]
[f() for f in fs] # [2, 2, 2]
Все лямбды видят одну i, которая к моменту вызова равна 2. Лечится значением по умолчанию lambda i=i: i или functools.partial.
lambda и функции высшего порядка
lambda это короткая анонимная функция из одного выражения. Встроенные map, filter, sorted(key=...), min/max(key=...) принимают функции. В современном коде вместо map/filter с лямбдой чаще пишут comprehension: он читается легче.
Частые ошибки
- Изменяемое значение по умолчанию.
UnboundLocalErrorиз-за присваивания глобальной переменной безglobal.- Позднее связывание в цикле с лямбдами.
- Перекрытие встроенных имён:
list = [...], после этогоlist()сломан в этой области. - Порядок параметров: значение по умолчанию не может стоять перед обязательным позиционным.
Что спросят на собесе
- Что выведет функция с
def f(x, acc=[])при нескольких вызовах и как исправить. - Что такое
*argsи**kwargs, что означают/и*в сигнатуре. - Правило LEGB,
globalиnonlocal, откуда берётсяUnboundLocalError. - Что такое замыкание и что выведет список лямбд из цикла.
- Передаются ли аргументы по ссылке или по значению.