Функции в Python

Функции как объекты, аргументы, области видимости и замыкания, функциональный стиль, декораторы и модуль functools

Вячеслав Федоров

Лекция 5

Разработка и применение ПО

в физических исследованиях

def

Содержание

От объявления функции до декораторов

01

Функция как объект

02

Аргументы

03

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

04

Функциональный стиль

05

Декораторы

06

Модуль functools

Цель лекции

Функция как единица расчёта: повторное использование, проверка, воспроизводимость

Функция является первым инструментом борьбы со сложностью: фрагмент логики, скрытый за именем, в дальнейшем не требуется удерживать в памяти. Расчёт, разбитый на функции с явными параметрами, проверяется по частям и воспроизводится с теми же входными данными.

Функция как объект

Объявление, документация, атрибуты, аннотации типов

01

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Объявление

функции

def kinetic_energy(m, v):
    """Кинетическая энергия тела, Дж.
 
    m — масса, кг; v — скорость, м/с.
    """
    return m * v ** 2 / 2
 
kinetic_energy(2.0, 3.0)    # 9.0

ЭлементНазначение
defсоздание объекта функции и связывание имени
returnвозврат значения; без return — None
строка документациистроковый литерал в начале тела: __doc__, help()
имябуквы, цифры и _, не с цифры; snake_case (PEP 8)

Пояснение

def выполняется во время работы программы, как присваивание: функцию можно определить в условии или внутри другой функции.

Функция —

объект первого класса

01

Связывание с именами

circle = area — второе имя того же объекта

02

Хранение в коллекциях

ops = {'area': area, 'abs': abs}

03

Передача в аргументах

sorted(data, key=f), quad(f, 0, 1)

04

Возврат из функций

фабрики функций и замыкания

Для физика

Подынтегральное выражение для quad и целевая функция для minimize передаются как аргументы: интегратору безразлично, как функция устроена внутри.

АтрибутСодержимое
__name__, __qualname__имя и полное имя
__doc__строка документации
__defaults__, __kwdefaults__значения по умолчанию
__code__объект кода: байт-код, имена
__annotations__аннотации параметров
__closure__ячейки замыкания
__globals__пространство имён модуля

Аннотации

типов

def mean(xs: list[float], w: list[float] | None = None) -> float:
    ...
 
mean.__annotations__
# {'xs': list[float], 'w': list[float] | None, 'return': <class 'float'>}

ИсточникВозможность
PEP 3107, 3.0синтаксис аннотаций параметров и результата
PEP 484, 3.5модуль typing; на нём работают mypy и pyright
PEP 585, 3.9list[float], dict[str, int] без typing
PEP 604, 3.10объединение типов: float | None
PEP 649/749, 3.14аннотации вычисляются отложенно

Важно

Аннотации не проверяются при выполнении: аргумент другого типа принимается, и TypeError, если возникнет, произойдёт в теле функции. Проверку выполняют анализаторы mypy и pyright.

Демонстрация:

функция как объект

01

Функция с аннотациями и документацией

02

Имя и строка документации

03

Аннотации в __annotations__

04

Второе имя для той же функции

05

Функции как значения словаря

06

Функция без return

07

Непроверяемая аннотация: area('2')

Что наблюдается

Функция — объект класса function с атрибутами; аннотации хранятся, но не проверяются, и ошибка возникает внутри тела.

root@lab — запись со стенда

Документация

и doctest

def kinetic_energy(m, v):
    """Кинетическая энергия тела.
 
    Parameters
    ----------
    m : float
        Масса, кг.
    v : float
        Скорость, м/с.
 
    Returns
    -------
    float
        Энергия, Дж.
 
    Examples
    --------
    >>> kinetic_energy(2.0, 3.0)
    9.0
    """

РазделСодержимое
Parametersимя : тип, смысл, единицы
Returnsтип и смысл результата
Raisesисключения и их условия
Examplesвызовы с ожидаемым выводом

Пояснение

python3 -m doctest phys.py выполняет примеры из документации и сравнивает вывод: документация проверяется как тест.

Для физика

Формат NumPy принят в NumPy, SciPy и Astropy; Sphinx с расширением numpydoc собирает из него справочник.

Демонстрация:

doctest

01

Примеры в документации функции

02

doctest: обнаружение ошибки в формуле

03

Исправление формулы

04

Повторный запуск с -v без ошибок

Что наблюдается

Пример ожидал 9.0, а функция без деления на 2 вернула 18.0; после исправления оба примера проходят.

root@lab — запись со стенда

Аргументы

Виды параметров, значения по умолчанию, *args и **kwargs, распаковка

02

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Виды

параметров

def f(a, b, /, c, *args, d=1, **kw):

a, b, /

только позиционные (3.8)

c

позиционный или именованный

*args

лишние позиционные → кортеж

d=1

только именованный

**kw

лишние именованные → словарь

ВызовСвязывание параметров
f(1, 2, 3)a=1, b=2, c=3, args=(), d=1, kw={}
f(1, 2, 3, 4, 5, d=6, e=7)args=(4, 5), d=6, kw={'e': 7}
f(1, 2, c=3)c передан по имени
f(*(1, 2), **{'c': 3, 'd': 4})распаковка: a=1, b=2, c=3, d=4

Значения

по умолчанию

def integrate(f, a, b, *, n=1000, tol=1e-8):
    ...
 
integrate(g, 0.0, 1.0, n=10_000)   # явно
integrate(g, 0.0, 1.0, 10_000)     # TypeError
 
def unique(xs, seen=None):
    seen = set() if seen is None else set(seen)
    ...

Важно

Значение по умолчанию вычисляется один раз, при выполнении def: изменяемый объект становится общим для всех вызовов. Вместо него указывается None.

Для физика

Настройки расчёта — число узлов, метод, допуск — объявляются только именованными: по вызову integrate(g, 0, 1, 1e-6, 100) не понять, где допуск, а где число узлов.

*args

и **kwargs

def min_of(first, *rest):
    res = first
    for x in rest:
        if x < res:
            res = x
    return res
 
min_of(-5, 12, 13)        # -5
min_of(*{-5, 12, 13})     # распаковка

def run(cmd, **options):
    if options.get('verbose', True):
        print('Logging enabled')
 
opts = {'verbose': False}
run('mysqld', **opts)

Пояснение

Звёздочка в объявлении собирает аргументы, в вызове — раскладывает коллекцию по параметрам. Обёртка def w(*args, **kwargs): return f(*args, **kwargs) передаёт любые аргументы без изменений.

Демонстрация:

аргументы

01

Функция со всеми видами параметров

02

Вызов с минимумом аргументов

03

Лишние позиционные и именованные

04

Сигнатура через inspect.signature

05

Ошибки: лишний позиционный и позиционный по имени

06

Распаковка кортежа и словаря в вызове

Что наблюдается

Лишние позиционные собираются в кортеж, лишние именованные — в словарь; нарушение вида параметра обнаруживается при вызове.

root@lab — запись со стенда

Распаковка

при присваивании

first, *rest = range(1, 5)        # 1, [2, 3, 4]
first, *mid, last = 'физика'      # 'ф', [...], 'а'
(x1, y1), (x2, y2) = (0, 0), (4, 3)
for name, *values in rows:
    ...
 
points = [*old, *new]             # PEP 448
config = {**defaults, **user}

Пояснение

Имя со звёздочкой получает список, возможно пустой; обычным именам значений должно хватить: a, *b, c = [42] вызывает ValueError.

Для физика

Строка файла вида «время, канал, отсчёты…» разбирается одним присваиванием: t, ch, *counts = line.split().

Демонстрация:

распаковка

01

Первый элемент и остаток

02

Первый, середина и последний

03

Вложенная распаковка координат

04

Недостаток значений: ValueError

05

Распаковка в литералах списка и словаря

Что наблюдается

Звёздочка собирает остаток в список; если значений не хватает обычным именам, возникает ValueError.

root@lab — запись со стенда

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

LEGB, global и nonlocal, замыкания, позднее связывание

03

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Правило

LEGB

Built-in: len, print, min, sum

Global: имена модуля

Enclosing: объемлющая функция

Local: сама функция

поиск идёт изнутри наружу до первого совпадения

min = 42               # global
 
def f(*args):
    min = 2            # enclosing
    def g():
        min = 4        # local
        print(min)
    g()

Важно

Переменная с именем встроенной функции закрывает её: после len = 5 вызов len('abc') завершается ошибкой.

Присваивание

и области видимости

Действие в функцииРезультат
чтение именипоиск по LEGB
присваивание именилокальная переменная на всё тело функции
global xприсваивание переменной модуля
nonlocal xприсваивание переменной объемлющей функции

count = 0
 
def bump():
    count += 1      # UnboundLocalError

Пояснение

Решение «локальная или нет» принимается при компиляции функции: присваивание где угодно в теле делает имя локальным во всём теле.

Для физика

Глобальное состояние делает результат функции зависимым от истории вызовов. Параметры и результаты передаются явно, счётчики и накопители оформляются замыканием или объектом.

Демонстрация:

области видимости

01

Чтение имени объемлющей функции

02

Присваивание в функции: UnboundLocalError

03

Встроенная функция, закрытая глобальным именем

04

Встроенная len после del

Что наблюдается

count += 1 читает ещё не получившую значения локальную переменную; после del len поиск снова доходит до встроенной области.

root@lab — запись со стенда

Замыкания

и nonlocal

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter
 
c = make_counter()
c(), c(), c()         # (1, 2, 3)

c

объект функции counter

__closure__

кортеж ячеек

ячейка count

cell_contents = 3

make_counter завершилась, а count продолжает существовать в ячейке

Для физика

Фабрика make_gaussian(mu, sigma) возвращает функцию одной переменной x с закреплёнными параметрами, пригодную для интегрирования и построения графика.

Позднее

связывание

funcs = [lambda: i for i in range(3)]
[f() for f in funcs]          # [2, 2, 2]
 
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]          # [0, 1, 2]
 
funcs = [partial(pow, 2, i) for i in range(3)]
[f() for f in funcs]          # [1, 2, 4]

Важно

Функция, созданная в цикле, захватывает переменную, а не её значение, и после цикла видит последнее значение.

Для физика

Список моделей или обработчиков для перебора параметров, созданный в цикле лямбдами, вычисляет всё с последним параметром без сообщения об ошибке.

Демонстрация:

замыкания

01

Фабрика счётчиков с nonlocal

02

Три вызова одного счётчика

03

Значение в ячейке замыкания

04

Лямбды в цикле: позднее связывание

05

Фиксация значения параметром по умолчанию

Что наблюдается

Счётчик хранит состояние в ячейке замыкания; лямбды из цикла видят последнее значение i, пока его не закрепит параметр по умолчанию.

root@lab — запись со стенда

Функциональный стиль

lambda, функции высшего порядка, включения, чистые функции, параметры для SciPy, стоимость вызова

04

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Анонимные

функции

lambda x: x ** 2
lambda a, *args, b=None, **kw: 42
 
sorted(events, key=lambda e: e.energy)
max(data, key=lambda p: p[0])

Пояснение

PEP 8 рекомендует def вместо присваивания лямбды имени: f = lambda x: ... ничем не лучше def f(x): ..., но в трассировке ошибки теряет имя.

lambdadef
телоодно выражениелюбые инструкции
имя в трассировке<lambda>имя функции
документациянетесть
применениекороткий ключ или аргументвсё остальное

Функции

высшего порядка

ФункцияНазначение
map(f, xs)применение f к каждому элементу, лениво
filter(pred, xs)отбор элементов, лениво; filter(None, xs) — истинные
zip(xs, ys)параллельный перебор до конца самой короткой
sorted, min, max (key=f)порядок и экстремумы по ключу
any, allпроверка условия с досрочным выходом
operator.itemgetter, attrgetterготовые ключи без lambda

Важно

map, filter и zip возвращают итераторы: значения вычисляются по требованию и проходятся один раз — повторный list от того же map пуст.

Для физика

max(events, key=attrgetter('energy')) находит событие с наибольшей энергией за один проход без промежуточного списка.

Включения

и map, filter

[x ** 2 for x in range(10) if x % 2]
list(map(lambda x: x ** 2,
         filter(lambda x: x % 2, range(10))))
 
[x for xs in nested for x in xs]      # вложенные
{x % 7 for x in data}                 # множество
{k: v for k, v in date.items() if v}  # словарь

values = list(map(float, fields))
total = sum(x * x for x in data)

Пояснение

Включение читается проще комбинации map и filter с лямбдами; с готовой функцией короче запись через map: map(float, fields). Выражение-генератор в круглых скобках не строит список.

Демонстрация:

функциональный стиль

01

Сортировка записей: по первому элементу и по ключу

02

Максимум по ключу itemgetter

03

map — одноразовый итератор

04

filter(None, ...): только истинные

05

zip: обрезка по короткой последовательности

06

any и all по условию

Что наблюдается

Второй list от того же map пуст: итератор уже пройден; zip останавливается по самой короткой последовательности.

root@lab — запись со стенда

Чистые

функции

01

Зависимость только от аргументов

одинаковые входы дают одинаковый выход

02

Нет побочных эффектов

аргументы и глобальное состояние не изменяются

03

Простота проверки и кеширования

тест пишется без подготовки окружения; результат можно запомнить

04

Параллельное выполнение

без блокировок и гонок

total = 0.0
def add_sample(x):        # нечистая
    global total
    total += x
 
def mean(xs):             # чистая
    return sum(xs) / len(xs)

Для физика

Генератор случайных чисел передаётся в функцию моделирования аргументом: с тем же зерном default_rng(42) результат воспроизводится, хотя вызов меняет состояние генератора.

Параметры

для интеграторов

def gauss(x, mu, sigma):
    return math.exp(-(x - mu) ** 2 / (2 * sigma ** 2))
 
quad(gauss, -5, 5, args=(0.0, 1.0))                # 1: args=
quad(lambda x: gauss(x, 0.0, 1.0), -5, 5)          # 2: лямбда
quad(partial(gauss, mu=0.0, sigma=1.0), -5, 5)     # 3: partial

СпособКогда уместен
args=(...)параметр args у quad, minimize, solve_ivp
lambdaодиночный вызов с закреплёнными значениями
partialпередача в процессы (функция — из импортируемого модуля)
замыкание, фабрикамодель с логикой и состоянием; pickle не сериализует

Важно

Лямбды, собранные в цикле и вызванные после него, получат последнее значение параметра; quad внутри той же итерации вычисляется верно.

Стоимость

вызова функции

Операция над 10⁵ чиселВремя (Python 3.12)
[x * x for x in xs]1,64 мс
[sq(x) for x in xs]2,66 мс
list(map(sq, xs))3,10 мс
цикл for с t += x1,17 мс
sum(xs)0,25 мс

Пояснение

Вызов функции Python добавил около 10 нс на элемент: 2,66 мс против 1,64 мс на 10⁵ чисел.

Для физика

Цикл по миллионам значений переносят во встроенные функции и NumPy: они выполняют цикл в скомпилированном коде. Подробно — в главе «Оптимизация средствами самого Python».

Демонстрация:

стоимость вызова

01

Подготовка: список и функция sq

02

Включение с выражением

03

Включение с вызовом функции

04

map с лямбдой

05

Явный цикл накопления

06

Встроенная sum

Что наблюдается

Вызов функции добавляет около 10 нс на элемент; встроенная sum в четыре-пять раз быстрее явного цикла.

root@lab — запись со стенда

Декораторы

Синтаксис, functools.wraps, аргументы, цепочки, практические декораторы

05

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Декоратор

как функция

@decorator
def foo(x):
    return 42

≡

def foo(x):
    return 42
 
foo = decorator(foo)

Пояснение

Декоратор — функция, принимающая функцию и возвращающая новую функцию или другой объект. Синтаксис @ появился в Python 2.4 (PEP 318), с 3.9 после @ допускается любое выражение (PEP 614).

Для физика

В научном коде декораторы встречаются постоянно: @numba.njit компилирует функцию, @functools.lru_cache запоминает результаты, @pytest.fixture готовит данные для тестов.

Устройство

простого декоратора

def trace(func):
    def inner(*args, **kwargs):
        print('вызов', func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return inner
 
@trace
def area(r):
    return 3.14159 * r ** 2

Пояснение

func доступна внутри inner после завершения trace благодаря замыканию.

area(2.0)

inner(*args, **kwargs)

print('вызов', ...)

func(*args, **kwargs) → результат

functools.wraps

и метаданные

АтрибутБез wrapsС wraps
__name__'inner''area'
__doc__Noneдокументация area
help(area)справка об innerсправка об area
__wrapped__нетисходная функция
inspect.signature(*args, **kwargs)(r)

import functools
 
def trace(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        print('вызов', func.__name__)
        return func(*args, **kwargs)
    return inner

Важно

Собственный декоратор всегда начинается с @functools.wraps(func) над внутренней функцией.

Демонстрация:

декоратор trace

01

Декоратор trace без wraps

02

Декорированная функция area

03

Вызов: печать и результат

04

Имя функции после декорирования

05

Декоратор с functools.wraps

06

Имя после wraps и ссылка __wrapped__

Что наблюдается

Без wraps у функции area имя inner; с wraps имя сохранено, а __wrapped__ ведёт к исходной функции.

root@lab — запись со стенда

Декораторы

с аргументами

def trace(handle):
    def decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            print(func.__name__, file=handle)
            return func(*args, **kwargs)
        return inner
    return decorator
 
@trace(sys.stderr)
def identity(x):
    return x

# @trace(sys.stderr) означает:
decorator = trace(sys.stderr)
identity = decorator(identity)
 
# @trace и @trace(handle=sys.stderr):
def trace(func=None, *, handle=sys.stdout):
    if func is None:
        return lambda f: trace(f, handle=handle)
    if not callable(func):
        raise TypeError('нужно trace(handle=...)')
    ...

Важно

Звёздочка не защищает от записи @trace(sys.stderr): файл попадает в параметр func, поэтому декоратор проверяет callable(func).

Цепочки

декораторов

@deco1
@deco2
def foo(): ...
 
# применение снизу вверх:
foo = deco1(deco2(foo))

Пояснение

@lru_cache располагают выше проверок, чтобы попадание в кеш не тратило время на проверку; @timethis — ниже, чтобы измерять саму функцию.

вход deco1

вход deco2

тело foo

выход deco2

выход deco1

Демонстрация:

порядок декораторов

01

Декоратор tag с аргументом

02

Два tag на одной функции

03

Вызов: порядок входа и выхода

Что наблюдается

Обёртка внешнего декоратора получает управление первой и возвращает его последней: обёртки вложены, как deco1(deco2(foo)).

root@lab — запись со стенда

Практические

декораторы

ДекораторЗадачаУстройство
@timethis(n_iter=100)лучшее время из n запусковцикл с perf_counter, минимум
@onceоднократное выполнениефлаг и результат в атрибутах обёртки
@memoizedкеш результатовсловарь в замыкании, ключ — аргументы
@deprecatedпредупреждение об устареванииwarnings.warn(stacklevel=2)
@pre, @postпроверка условий (контракты)assert до и после вызова

Для физика

@timethis берёт минимум из нескольких запусков: среднее искажается случайными помехами системы, а минимум показывает, на что способен код в их отсутствие.

@deprecated

и предупреждения

def deprecated(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        warnings.warn(f'{func.__name__} устарела',
                      DeprecationWarning, stacklevel=2)
        return func(*args, **kwargs)
    return inner

01

При каждом вызове

warnings.warn внутри inner, а не в теле декоратора

02

stacklevel=2

строка вызывающего кода в сообщении

03

Видимость

по умолчанию — только для кода __main__

04

В тестах

-W error::DeprecationWarning: предупреждение как ошибка

Пояснение

С Python 3.13 то же делает готовый декоратор warnings.deprecated (PEP 702).

Демонстрация:

устаревшая функция

01

Декоратор deprecated в модуле legacy.py

02

Вызов из сценария: предупреждение со строкой вызова

03

Предупреждение как ошибка: -W error

Что наблюдается

stacklevel=2 указывает строку run.py:3, где вызвана функция; с -W error то же предупреждение прерывает выполнение.

root@lab — запись со стенда

Контракты

через декораторы

def pre(cond, message):
    def decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            assert cond(*args, **kwargs), message
            return func(*args, **kwargs)
        return inner
    return decorator
 
@pre(lambda x: 0 < x < 1, 'аргумент должен лежать в (0, 1)')
def log_fraction(x):
    return math.log(x)

Важно

Ключ -O удаляет assert из байт-кода: внешние данные проверяются явным raise ValueError.

Для физика

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

Демонстрация:

контракт и -O

01

Функция с предусловием

02

Обычный запуск: AssertionError на 2.0

03

Запуск с -O без проверки

Что наблюдается

С ключом -O assert удаляется, и функция возвращает логарифм аргумента вне допустимого диапазона без сообщения об ошибке.

root@lab — запись со стенда

Декораторы

стандартной библиотеки

ДекораторНазначение
@functools.lru_cache, @cacheмемоизация — шестая часть
@property, @classmethod, @staticmethodметоды классов — следующая лекция
@dataclasses.dataclassкласс данных — следующая лекция
@contextlib.contextmanagerменеджер контекста из генератора
@atexit.registerвызов при завершении интерпретатора
@warnings.deprecated('…') (3.13)пометка устаревших функций
@typing.overloadварианты сигнатуры для анализаторов

Пояснение

Декоратор класса получает класс и возвращает класс: @dataclass дописывает методы __init__, __repr__ и __eq__ по аннотациям полей.

Модуль functools

Рекурсия, lru_cache и cache, partial, singledispatch, reduce

06

Функция как объект

Аргументы

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

Функциональный стиль

Декораторы

Модуль functools

Рекурсия

и стек вызовов

def depth(n):
    return 0 if n == 0 else 1 + depth(n - 1)
 
depth(900)       # 900
depth(5000)      # RecursionError
 
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

01

Кадр стека на вызов

аргументы и локальные переменные каждого вызова

02

Предел глубины

sys.getrecursionlimit() = 1000, затем RecursionError

03

Хвостовая рекурсия

без оптимизации, в отличие от функциональных языков

04

Решение

глубокую рекурсию — в цикл, повторные подзадачи — в кеш

Демонстрация:

предел рекурсии

01

Предел глубины рекурсии

02

Рекурсия глубины 900

03

Глубина 5000: RecursionError

04

Тот же подсчёт циклом

Что наблюдается

Предел — 1000 кадров; цикл той же логики проходит миллион шагов без ограничения.

root@lab — запись со стенда

Мемоизация:

lru_cache и cache

from functools import lru_cache
 
@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)
 
fib(32)            # 2178309
fib.cache_info()

Для физика

Кеш окупается, когда функция многократно вызывается с одними аргументами: табличные функции, коэффициенты, дорогие интегралы. Массивы NumPy нехешируемы и ключом кеша быть не могут.

СредствоНазначение
@lru_cache(maxsize=128)кеш с вытеснением давно не использованных записей
@cache (3.9)то же без ограничения размера
cache_info()попадания, промахи, размер
cache_clear()очистка кеша
typed=True1 и 1.0 — разные ключи

Важно

Кеш с maxsize=None в долго работающей программе растёт неограниченно.

Демонстрация:

мемоизация fib

01

Рекурсивный fib без кеша: время fib(32)

02

Тот же fib с lru_cache

03

Статистика кеша

04

fib(400): однократное вычисление каждого значения

Что наблюдается

Без кеша fib(32) вычисляется за 0,15 с, с кешем — около 70 мкс: 33 промаха и 30 попаданий вместо семи миллионов вызовов.

root@lab — запись со стенда

Частичное

применение

from functools import partial
 
int2 = partial(int, base=2)
int2('1010')          # 10
 
def planck(nu, T):
    ...
 
quad(partial(planck, T=5800.0), 1e14, 1e15)

partiallambda
закрепление при созданиипозднее связывание
сериализация pickleнет сериализации
атрибуты func, args, keywordsтолько текст лямбды

Для физика

multiprocessing.Pool.map передаёт функцию в процессы через pickle: partial подходит, лямбда — нет; исходная функция — из импортируемого модуля.

Обобщённые функции:

singledispatch

from functools import singledispatch
 
@singledispatch
def describe(x):
    return 'объект'
 
@describe.register
def _(x: int):
    return 'целое'
 
@describe.register
def _(x: list):
    return f'список из {len(x)}'

Пояснение

Реализация выбирается по типу первого аргумента с учётом наследования: describe(True) даёт 'целое', поскольку bool — подкласс int.

Для физика

Функция обработки принимает число, список или массив и выбирает реализацию по типу входа без цепочки isinstance.

Другие средства

functools

СредствоНазначение
reduce(f, xs)свёртка последовательности; для произведения — math.prod (3.8)
total_orderingвсе сравнения класса по __eq__ и __lt__
cached_property (3.8)атрибут, вычисляемый при первом обращении
wraps, update_wrapperметаданные обёртки
partialmethod, singledispatchmethodварианты для методов классов
cmp_to_keyперенос старых функций сравнения

Пояснение

reduce в Python применяется редко: sum, min, max, math.prod и явный цикл читаются лучше.

Демонстрация:

functools

01

partial(int, base=2): двоичный разбор

02

Атрибуты func и keywords

03

singledispatch: реализации для int и list

04

Выбор по типу, включая bool

05

Свёртка reduce: 5!

Что наблюдается

partial хранит закреплённые аргументы; singledispatch выбирает реализацию по типу с учётом наследования.

root@lab — запись со стенда

Итоги

лекции

01

Функция — объект: её передают, хранят и возвращают; аннотации документируют типы, но не проверяются.

02

Порядок параметров: только позиционные, /, обычные, *args, только именованные, **kwargs.

03

Значение по умолчанию вычисляется один раз; изменяемые значения заменяются на None.

04

Имена ищутся по LEGB; присваивание делает имя локальным, global и nonlocal это меняют.

05

Замыкание хранит переменные объемлющей функции; функция из цикла видит последнее значение, поэтому его фиксируют параметром по умолчанию, partial или фабрикой.

06

Декоратор — функция, принимающая функцию; wraps сохраняет метаданные, functools даёт готовые решения.

Упражнения для самопроверки

01

Функция unique(iterable, seen=set()) при повторном вызове на тех же данных возвращает пустой список: объяснить причину и исправить

02

Объяснить результат [f() for f in [lambda: i for i in range(3)]] и исправить тремя способами

03

Написать @timethis(n_iter=...) с functools.wraps и проверить help() и inspect.signature обёрнутой функции

04

Сравнить самописный @memoized и functools.lru_cache на рекурсивном fib: время и cache_info()

05

Написать фабрику make_gaussian(mu, sigma) и проинтегрировать результат через scipy.integrate.quad

Главы книги: «Функции», «Декораторы и functools» · продолжается задание «Решение задач на Python и анализ сложности»

Источники

1

Книга курса: главы «Функции» и «Декораторы и functools»

phys-dev.github.io/soft-dev-book

2

Лебедев С. Функции в Python. – Computer Science Center

youtube.com/watch?v=ZRZq2u0v6KI

3

Лебедев С. Декораторы. Модуль functools. – Computer Science Center

youtube.com/watch?v=qIDBMpy38O4

4

The Python Tutorial: More on Defining Functions

docs.python.org/3/tutorial/controlflow.html

5

Модуль functools — документация Python

docs.python.org/3/library/functools.html

6

PEP 318, 570, 3102, 3104, 448, 484 — декораторы, параметры, nonlocal, распаковка, типы

peps.python.org

7

Ramalho L. Fluent Python. – 2nd ed. – O'Reilly, 2022

Спасибо за внимание

Вопросы

phys-dev.github.io/soft-dev-book

@