Урок 02 из 12 · 2 минуты чтения

Функции — аргументы, области видимости, замыкания и декораторы

Как передаются аргументы, *args и **kwargs, LEGB, ловушка изменяемого значения по умолчанию, замыкания и позднее связывание, декораторы и functools.

Как передаются аргументы

Ни «по значению», ни «по ссылке» в классическом смысле: функция получает ссылку на тот же объект (call by sharing).

def add(items):
    items.append(1)    # меняет объект — видно снаружи
    items = [99]       # переприсваивает имя — снаружи не видно

Если менять аргумент не нужно — копируйте или возвращайте новый объект.

*args и **kwargs

*args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. При вызове звёздочки распаковывают: f(*values, **options).

def connect(host, /, port=5432, *, timeout=5): ...

До / — только позиционные, после * — только именованные.

Области видимости — LEGB

Имя ищется по цепочке: Local → Enclosing → Global → Built-in. Присваивание внутри функции делает имя локальным для всей функции — отсюда UnboundLocalError, если прочитать его до присваивания. Писать во внешнюю переменную — через global или nonlocal. Циклы и if своей области не создают.

Изменяемое значение по умолчанию

Значение по умолчанию вычисляется один раз — при определении функции:

def add_item(item, items=[]):     # общий список на все вызовы
    items.append(item)
    return items

def add_item(item, items=None):   # правильно
    if items is None:
        items = []
    items.append(item)
    return items

Замыкания

Вложенная функция помнит переменные внешней после её завершения:

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]
fs = [lambda i=i: i for i in range(3)]   # [0, 1, 2]

Декораторы

Декоратор — функция, которая принимает функцию и возвращает новую; @dec — сахар для f = dec(f).

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(func.__name__, time.perf_counter() - start)
    return wrapper

functools.wraps копирует в обёртку имя, docstring и ставит __wrapped__ — без него логи, документация и pytest видят «wrapper».

Декоратор с параметрами — фабрика декораторов, три уровня вложенности: @retry(times=3) равно f = retry(times=3)(f). Для async-функций обёртка тоже должна быть async.

functools

ЧтоЗачем
lru_cache(maxsize), cacheмемоизация; аргументы должны быть хешируемыми
partialзафиксировать часть аргументов
singledispatchперегрузка по типу первого аргумента
cached_propertyвычислить атрибут один раз

lru_cache на методе держит ссылку на self — объекты не освобождаются.

Что запомнить

  • Функция получает ссылку на объект: изменить можно, переприсвоить снаружи — нет.
  • Изменяемое значение по умолчанию — None и создание внутри.
  • Замыкание видит переменную в момент вызова.
  • Обёртку декоратора всегда помечают functools.wraps.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

Читайте также

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования