PythonJuniorTechnical

Что такое first-class functions в Python? Что это даёт на практике?

Функции в Python — объекты первого класса: их можно присваивать переменным, передавать аргументами, возвращать из функций и хранить в коллекциях. У них есть атрибуты (__name__, __doc__, __annotations__). Базис для callback-ов, декораторов, dependency injection.

Что это значит

«First-class» означает, что функции — обычные объекты типа function (или builtin_function_or_method). С ними можно делать всё, что с любым другим объектом:

  • Присваивать переменным: f = process — ссылка, не вызов.
  • Передавать аргументами: sorted(items, key=str.lower).
  • Возвращать из других функций (closures, decorators, фабрики).
  • Хранить в коллекциях: handlers = {"GET": handle_get, "POST": handle_post}.
  • Сравнивать по identity, передавать через очередь, сериализовать (через pickle — с ограничениями).

Каждая функция имеет атрибуты: __name__, __qualname__, __module__, __doc__, __annotations__, __code__, __defaults__, __closure__, __dict__. На них можно вешать кастомные атрибуты (используют декораторы).

Практические применения

  • Higher-order functions: map, filter, sorted(..., key=...), functools.reduce.
  • Декораторы: функция, принимающая функцию и возвращающая обёртку.
  • Callbacks: signal handlers, GUI events, ARQ/Celery tasks.
  • Strategy pattern: словарь имени → реализация вместо if/elif лестницы.
  • Dependency injection: передавайте функции вместо классов, когда не нужно состояние.
  • Partial application: functools.partial(fn, x=1) создаёт новую callable с зафиксированными аргументами.

Пример

from functools import partial, reduce


def by_length(value: str) -> int:
    return len(value)


def apply(func, value):
    return func(value)


names = ["Ada", "Guido", "Linus"]
print(sorted(names, key=by_length))   # ['Ada', 'Guido', 'Linus']
print(apply(str.upper, "python"))     # PYTHON


# Атрибуты функции
print(by_length.__name__)             # by_length
print(by_length.__annotations__)      # {'value': <class 'str'>, 'return': <class 'int'>}
by_length.cache_hits = 0              # повесить свой атрибут
print(by_length.cache_hits)


# Словарь команд — strategy pattern на функциях
def cmd_ping(args): return "pong"
def cmd_echo(args): return " ".join(args)

COMMANDS = {"ping": cmd_ping, "echo": cmd_echo}
print(COMMANDS["echo"](["hello", "world"]))  # hello world


# partial — фиксируем аргумент
log_error = partial(print, "[ERROR]")
log_error("disk full")  # [ERROR] disk full


# reduce — высокоуровневая агрегация
print(reduce(lambda a, b: a * b, [1, 2, 3, 4]))  # 24


# Декоратор как пример возврата функции
def trace(fn):
    def wrapper(*args, **kwargs):
        print(f"call {fn.__name__}({args}, {kwargs})")
        return fn(*args, **kwargs)
    return wrapper


@trace
def add(a, b): return a + b


add(1, 2)

Подводные камни

  • Путать fn (ссылка) и fn() (вызов) — передаёте результат вместо функции и удивляетесь падению.
  • Method bound vs unbound: obj.method — bound (содержит self); Class.method — unbound (нужно явно передать self).
  • Декоратор без functools.wraps теряет __name__/__doc__ исходной функции; ломает интроспекцию и pytest.
  • Lambda удобна на месте, но именованная функция лучше для тестов и stack traces.
  • Сохранение функции в БД/файл — пиклить можно только top-level функции; lambda/closure не сериализуются.
  • Mutable default argument — частая ловушка: def f(x, lst=[]): делит lst между вызовами.

Common mistakes

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

What the interviewer is testing

  • Передаёт функцию как аргумент.
  • Объясняет callbacks/decorators.
  • Понимает, что функции имеют атрибуты и identity.

Sources

Related topics