Урок по треку Python 7 мин

Функции в Python: аргументы, LEGB и замыкания

Аргументы, *args и **kwargs, изменяемые значения по умолчанию, LEGB, замыкания, lambda.

Функция в 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.
  • Что такое замыкание и что выведет список лямбд из цикла.
  • Передаются ли аргументы по ссылке или по значению.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.