Функции и Декораторы

Функции и Декораторы #

1. Что такое функции высшего порядка и какие их основные примеры? #

Это функции, которые могут принимать другие функции в качестве аргументов или возвращать их в качестве результата. Этот подход делает код более гибким и позволяет строить сложные операции на основе более простых.

В Python функции map, filter, и reduce являются классическими примерами функций высшего порядка, так как они принимают другие функции в качестве аргументов.


2. Чем стали заменять функции map(), reduce(), filter()? #

map(), filter() и reduce() не исчезли, но в современном Python их часто заменяют более читаемыми конструкциями:

map()     → list comprehension / generator expression
filter()  → list comprehension / generator expression с if
reduce()  → sum(), any(), all(), min(), max(), join(), циклы

map() #

map() применяет функцию к каждому элементу:

numbers = [1, 2, 3, 4]

result = list(map(lambda x: x * 2, numbers))
print(result)

Современная замена:

numbers = [1, 2, 3, 4]

result = [x * 2 for x in numbers]
print(result)

То есть:

map(func, iterable)

часто заменяют на:

[func(x) for x in iterable]

Или, если не нужен сразу список:

(func(x) for x in iterable)

filter() #

filter() оставляет только элементы, подходящие под условие:

numbers = [1, 2, 3, 4, 5, 6]

result = list(filter(lambda x: x % 2 == 0, numbers))
print(result)

Современная замена:

numbers = [1, 2, 3, 4, 5, 6]

result = [x for x in numbers if x % 2 == 0]
print(result)

То есть:

filter(condition, iterable)

часто заменяют на:

[x for x in iterable if condition(x)]

reduce() #

reduce() постепенно сворачивает коллекцию в одно значение. Например, сумма:

from functools import reduce

numbers = [1, 2, 3, 4]

result = reduce(lambda acc, x: acc + x, numbers)
print(result)

Лучше заменить на:

numbers = [1, 2, 3, 4]

result = sum(numbers)
print(result)

Типичные замены reduce() #

Сумма #

sum(numbers)

Вместо:

reduce(lambda acc, x: acc + x, numbers)

Произведение #

import math

math.prod(numbers)

Вместо:

reduce(lambda acc, x: acc * x, numbers)

Проверить, что все элементы подходят #

all(x > 0 for x in numbers)

Вместо:

reduce(lambda acc, x: acc and x > 0, numbers, True)

Проверить, что хотя бы один подходит #

any(x > 10 for x in numbers)

Вместо:

reduce(lambda acc, x: acc or x > 10, numbers, False)

Максимум / минимум #

max(numbers)
min(numbers)

Вместо:

reduce(lambda acc, x: acc if acc > x else x, numbers)

Склеивание строк #

", ".join(words)

Вместо:

reduce(lambda acc, x: acc + ", " + x, words)

Почему стали заменять #

Главная причина — читаемость. Сравни:

result = list(map(lambda x: x.upper(), names))

И:

result = [name.upper() for name in names]

Второй вариант обычно читается проще: сразу видно, что мы создаём новый список из names.

То же самое с filter():

result = list(filter(lambda user: user.is_active, users))

Лучше:

result = [user for user in users if user.is_active]

Важное уточнение #

map() и filter() не запрещены и не устарели. Они всё ещё нормальны, особенно когда уже есть готовая функция:

names = ["alex", "john", "bob"]

result = list(map(str.upper, names))

Это вполне читаемо.

Но если внутри появляется lambda, чаще лучше использовать comprehension:

result = [name.upper() for name in names]

reduce() используют реже #

reduce() в Python убрали из встроенных функций и перенесли в модуль functools:

from functools import reduce

Это намёк на то, что его не считают повседневным инструментом.

Чаще вместо него лучше использовать:

sum()
all()
any()
min()
max()
math.prod()
"".join()
обычный for-цикл

Итог #

map()    → [func(x) for x in items]
filter() → [x for x in items if condition(x)]
reduce() → sum(), all(), any(), min(), max(), join(), math.prod(), for

На практике:

# map
[x * 2 for x in numbers]

# filter
[x for x in numbers if x % 2 == 0]

# reduce
sum(numbers)

Главная идея: в Python чаще выбирают не самый функциональный стиль, а самый читаемый.


3. Как реализована передача в функцию произвольного количества позиционных и именованных аргументов с помощью *args и **kwargs? #

Суть #

*args и **kwargs — это механизм, который позволяет функции принять больше аргументов, чем явно перечислено в сигнатуре.

def func(*args, **kwargs):
    print(args)
    print(kwargs)

Внутри функции:

args   → tuple с позиционными аргументами
kwargs → dict с именованными аргументами

Официальная документация Python описывает *args как var-positional параметр, который принимает произвольную последовательность позиционных аргументов, а **kwargs как var-keyword параметр, который принимает произвольное количество именованных аргументов.


*args #

*args собирает лишние позиционные аргументы в кортеж.

def func(a, b, *args):
    print("a:", a)
    print("b:", b)
    print("args:", args)


func(1, 2, 3, 4, 5)

Результат:

a: 1
b: 2
args: (3, 4, 5)

Как это работает:

1 → a
2 → b
3, 4, 5 → args

То есть Python сначала заполняет обычные параметры, а все оставшиеся позиционные аргументы складывает в args.

Важно: args — это просто имя по соглашению. Можно написать и так:

def func(*values):
    print(values)

Но в реальном коде почти всегда используют именно args.


**kwargs #

**kwargs собирает лишние именованные аргументы в словарь.

def func(name, **kwargs):
    print("name:", name)
    print("kwargs:", kwargs)


func(name="Alex", age=25, city="Baku")

Результат:

name: Alex
kwargs: {'age': 25, 'city': 'Baku'}

Как это работает:

name="Alex" → name
age=25      → kwargs["age"]
city="Baku" → kwargs["city"]

То есть Python сначала пытается сопоставить именованные аргументы с обычными параметрами функции. Всё, что не подошло, попадает в kwargs. В документации это описано как механизм принятия произвольного количества keyword-аргументов.


Вместе: *args и **kwargs #

Обычно пишут так:

def func(*args, **kwargs):
    print(args)
    print(kwargs)


func(1, 2, 3, name="Alex", age=25)

Результат:

(1, 2, 3)
{'name': 'Alex', 'age': 25}

Разбор:

1, 2, 3           → args
name="Alex"      → kwargs
age=25           → kwargs

Порядок параметров #

Правильный порядок такой:

def func(positional, default=10, *args, keyword_only, **kwargs):
    ...

Упрощённо:

обычные позиционные параметры
параметры со значением по умолчанию
*args
keyword-only параметры
**kwargs

Пример:

def func(a, b=10, *args, c, **kwargs):
    print(a)
    print(b)
    print(args)
    print(c)
    print(kwargs)


func(1, 2, 3, 4, c=5, d=6, e=7)

Результат:

1
2
(3, 4)
5
{'d': 6, 'e': 7}

Разбор:

a = 1
b = 2
args = (3, 4)
c = 5
kwargs = {"d": 6, "e": 7}

Важное отличие: сбор и распаковка #

В определении функции *args и **kwargs собирают аргументы:

def func(*args, **kwargs):
    ...

А при вызове функции * и ** наоборот распаковывают значения:

args = (1, 2, 3)
kwargs = {"name": "Alex", "age": 25}

func(*args, **kwargs)

Это то же самое, что:

func(1, 2, 3, name="Alex", age=25)

Официальный FAQ Python прямо показывает этот паттерн: функция может собрать аргументы через *args и **kwargs, а затем передать их дальше через *args и **kwargs.


Пример с проксированием вызова #

Частый кейс — функция-обёртка:

def log_call(func, *args, **kwargs):
    print("Calling:", func.__name__)
    return func(*args, **kwargs)


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


result = log_call(add, 10, 20)
print(result)

Результат:

Calling: add
30

Здесь:

log_call(add, 10, 20)
func = add
args = (10, 20)
kwargs = {}
func(*args, **kwargs)
add(10, 20)

Ошибки при передаче аргументов #

Дублирование аргумента #

def func(a):
    print(a)


func(1, a=2)

Ошибка:

TypeError: func() got multiple values for argument 'a'

Потому что a получил значение дважды:

1   → a
a=2 → a

Неожиданный именованный аргумент #

def func(a):
    print(a)


func(a=1, b=2)

Ошибка:

TypeError: func() got an unexpected keyword argument 'b'

Но если добавить **kwargs, ошибка исчезнет:

def func(a, **kwargs):
    print(a)
    print(kwargs)


func(a=1, b=2)

Результат:

1
{'b': 2}

Итог #

def func(*args, **kwargs):
    ...

означает:

*args    → принять любое количество позиционных аргументов
**kwargs → принять любое количество именованных аргументов

А при вызове:

func(*args, **kwargs)

означает:

*args    → распаковать iterable как позиционные аргументы
**kwargs → распаковать mapping как именованные аргументы

Главная формула:

def wrapper(*args, **kwargs):
    return target(*args, **kwargs)

Это основа декораторов, обёрток, middleware, логирования, проксирования вызовов и гибких API.


4. Допускает ли Python определение функции внутри другой функции и каковы особенности таких вложенных функций? #

Да, Python допускает определение функции внутри другой функции.

Такая функция называется вложенной функцией. Она создаётся во время выполнения внешней функции и имеет доступ к её локальной области видимости. В документации Python прямо описаны nested scopes и nonlocal для функций, вложенных в другие функции.

def outer():
    def inner():
        print("Внутренняя функция")

    inner()


outer()

Результат:

Внутренняя функция

1. Вложенная функция существует только внутри внешней #

def outer():
    def inner():
        return "Hello"

    return inner()


print(outer())

Результат:

Hello

Но напрямую вызвать inner() снаружи нельзя:

inner()

Будет ошибка:

NameError: name 'inner' is not defined

Потому что имя inner находится в локальной области видимости функции outer.


2. Вложенная функция может читать переменные внешней функции #

def outer():
    message = "Hello"

    def inner():
        print(message)

    inner()


outer()

Результат:

Hello

Разбор:

outer()
├── message = "Hello"
└── inner()
    └── читает message из outer()

Такая переменная называется free variable / closure variable: она определена во внешней области, но используется во вложенной функции. В глоссарии Python это описано как переменная из nested scope, находящаяся во внешней области видимости.


3. Но изменять внешнюю переменную нельзя без nonlocal #

Читать можно:

def outer():
    counter = 0

    def inner():
        print(counter)

    inner()


outer()

Но если попытаться изменить:

def outer():
    counter = 0

    def inner():
        counter += 1
        print(counter)

    inner()


outer()

Будет ошибка:

UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

Причина: если внутри функции есть присваивание имени, Python считает это имя локальной переменной этой функции. Это правило описано в Python FAQ: присваивание внутри функции делает переменную локальной, если явно не указано другое.


4. Для изменения внешней переменной используется nonlocal #

def outer():
    counter = 0

    def inner():
        nonlocal counter
        counter += 1
        print(counter)

    inner()
    inner()


outer()

Результат:

1
2

Разбор:

nonlocal counter
counter берётся не из inner()
а из ближайшей внешней функции outer()

nonlocal говорит Python, что имя нужно искать во внешней, но не глобальной области видимости. Документация уточняет: если имя есть в нескольких внешних областях, используется ближайшее.


5. Вложенную функцию можно вернуть наружу #

def make_multiplier(n):
    def multiplier(x):
        return x * n

    return multiplier


double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))
print(triple(10))

Результат:

20
30

Здесь multiplier продолжает помнить значение n, даже когда make_multiplier() уже завершилась.

make_multiplier(2)
n = 2
возвращает multiplier
double(10) использует сохранённое n = 2

Это называется замыкание — closure.

В Python FAQ показан аналогичный пример: внешняя функция linear(a, b) возвращает вложенную функцию, которая использует a и b из внешней области.


6. Вложенные функции часто используют для декораторов #

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("До вызова функции")
        result = func(*args, **kwargs)
        print("После вызова функции")
        return result

    return wrapper


@my_decorator
def say_hello():
    print("Hello")


say_hello()

Результат:

До вызова функции
Hello
После вызова функции

Разбор:

my_decorator получает функцию say_hello
создаёт wrapper
wrapper вызывает исходную say_hello
наружу возвращается wrapper

То есть декораторы часто строятся именно на вложенных функциях и замыканиях.


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

def outer():
    def inner():
        return 10

    return inner


f1 = outer()
f2 = outer()

print(f1 is f2)

Результат:

False

Почему:

outer() → создал одну inner-функцию
outer() → создал другую inner-функцию

Они имеют одинаковый код, но это разные объекты функции.


8. Когда стоит использовать вложенные функции #

Вложенные функции полезны, когда функция нужна только внутри другой функции. Типичные случаи:

1. Вспомогательная логика, которая не нужна снаружи
2. Замыкания
3. Декораторы
4. Фабрики функций
5. Сокрытие деталей реализации

Пример вспомогательной функции:

def process_user(user):
    def normalize_name(name):
        return name.strip().title()

    user["name"] = normalize_name(user["name"])
    return user


user = {"name": "  alex  "}
print(process_user(user))

Результат:

{'name': 'Alex'}

normalize_name() не нужна всему модулю, поэтому её можно спрятать внутри process_user().


9. Когда лучше не использовать #

Не стоит делать вложенную функцию, если она:

1. Нужна в нескольких местах модуля
2. Слишком большая
3. Усложняет чтение внешней функции
4. Требует отдельного тестирования

В таком случае лучше вынести её на уровень модуля:

def normalize_name(name):
    return name.strip().title()


def process_user(user):
    user["name"] = normalize_name(user["name"])
    return user

Так проще читать, тестировать и переиспользовать.


Итог #

Python разрешает такое:

def outer():
    def inner():
        ...

Основные особенности:

inner() видна только внутри outer()
inner() может читать переменные outer()
для изменения переменных outer() нужен nonlocal
inner() можно вернуть наружу
при возврате наружу появляется замыкание
каждый вызов outer() создаёт новый объект inner

Главная идея:

def outer(value):
    def inner():
        return value

    return inner

Вложенная функция — это обычная функция, но созданная внутри другой функции и работающая с её областью видимости.


5. Доступна ли вложенная (локальная) функция для вызова из внешнего кода и как определяется область её видимости? #

Обычная вложенная функция не доступна для прямого вызова из внешнего кода.

def outer():
    def inner():
        return "Hello"

    return inner()


outer()
inner()  # NameError

inner существует как имя только внутри локальной области видимости outer. Область видимости в Python определяется тем, где имя привязано в коде: если имя создаётся внутри блока функции, оно является локальным для этого блока. Python документация формулирует это так: если имя привязано в блоке, оно становится локальной переменной этого блока, если не объявлено через global или nonlocal.


Почему inner() не видна снаружи #

Когда Python выполняет def inner():, он создаёт объект функции и привязывает его к имени inner.

Но это происходит внутри вызова outer():

def outer():
    def inner():
        return "Hello"

    print(inner())

Логически:

глобальная область
└── outer

локальная область outer()
└── inner

Снаружи имя inner не существует:

def outer():
    def inner():
        return "Hello"

outer()

print(inner())

Результат:

NameError: name 'inner' is not defined

Причина не в том, что функция “особенная”, а в том, что её имя находится в локальном namespace внешней функции. В документации Python namespace описан как отображение имён на объекты, а локальный namespace функции создаётся при вызове функции и удаляется после её завершения.


Как определяется область видимости #

Для имён в Python обычно используют правило LEGB:

L — Local
    локальная область текущей функции

E — Enclosing
    области внешних функций

G — Global
    глобальная область модуля

B — Built-in
    встроенные имена Python

Пример:

x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)

    inner()


outer()

Результат:

local

Python ищет имя x так:

inner local
outer enclosing
module global
builtins

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


Можно ли всё-таки вызвать вложенную функцию снаружи #

Да, но только если наружу передать сам объект функции. Например, вернуть её:

def outer():
    def inner():
        return "Hello"

    return inner


func = outer()

print(func())

Результат:

Hello

Здесь снаружи вызывается не имя inner, а переменная func, которая ссылается на тот же объект функции:

outer()
создал inner
вернул объект функции inner
func = inner
func()

То есть сама функция может жить дольше, чем вызов outer(), если на неё осталась ссылка.


Важный пример с замыканием #

def make_counter():
    count = 0

    def inc():
        nonlocal count
        count += 1
        return count

    return inc


counter = make_counter()

print(counter())
print(counter())
print(counter())

Результат:

1
2
3

Хотя make_counter() уже завершилась, inc() продолжает помнить переменную count.

Это работает потому, что вложенная функция замкнула переменную из внешней области. Для изменения такой переменной используется nonlocal. В документации Python указано, что nonlocal заставляет имя ссылаться на уже существующую переменную из ближайшей внешней функции.


Важное различие #

Недоступна напрямую #

def outer():
    def inner():
        return 10

outer()

inner()  # NameError

Почему:

имя inner было локальным внутри outer()

Доступна, если вернуть наружу #

def outer():
    def inner():
        return 10

    return inner


f = outer()

print(f())

Почему:

f теперь ссылается на объект функции inner

Итог #

Вложенная функция:

def outer():
    def inner():
        ...

по умолчанию:

не доступна для прямого вызова из внешнего кода
видна только внутри внешней функции
создаётся при выполнении outer()
получает имя в локальной области outer()
может читать переменные outer()
может быть возвращена наружу как объект
может образовать замыкание

Главное правило:

область видимости имени функции определяется не тем,
что это функция, а тем, где выполнен def

Если def inner() находится внутри outer(), то имя inner локально для outer().


6. Что такое декоратор в Python и для каких целей он используется? #

Декоратор в Python — это callable-объект, чаще всего функция, который принимает другую функцию/класс, добавляет или меняет её поведение и возвращает новый объект.

Обычно декоратор используют через синтаксис @decorator. В официальном glossary Python декоратор описан как функция, возвращающая другую функцию, обычно применяемая через @wrapper-синтаксис. Там же указано, что @decorator — это синтаксический сахар.


Простая идея #

Допустим, есть функция:

def say_hello():
    print("Hello")

Мы хотим добавить поведение до и после её вызова, но не менять саму функцию.

Можно сделать так:

def my_decorator(func):
    def wrapper():
        print("До вызова функции")
        func()
        print("После вызова функции")

    return wrapper


@my_decorator
def say_hello():
    print("Hello")


say_hello()

Результат:

До вызова функции
Hello
После вызова функции

Фактически это означает:

def say_hello():
    print("Hello")


say_hello = my_decorator(say_hello)

То есть имя say_hello теперь указывает не на исходную функцию, а на результат работы декоратора.


Как работает @decorator #

Запись:

@my_decorator
def func():
    ...

эквивалентна такой записи:

def func():
    ...

func = my_decorator(func)

Это ключевой момент: декоратор вызывается не при каждом вызове функции, а в момент определения функции. При последующих вызовах вызывается уже обёрнутая функция. В документации Python указано, что определение функции — это выполняемый оператор, который создаёт объект функции и связывает его с именем в текущей области видимости.


Зачем нужны декораторы #

Декораторы используют, когда нужно добавить сквозное поведение, не засоряя основную бизнес-логику.

Типичные задачи:

логирование
проверка прав доступа
измерение времени выполнения
кэширование
валидация аргументов
повторные попытки
транзакции
регистрация обработчиков
middleware-логика

Например, логирование:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов функции: {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


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


print(add(2, 3))

Результат:

Вызов функции: add
5

Почему внутри wrapper используют *args и **kwargs #

Обычно декоратор должен уметь оборачивать функции с разными аргументами:

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper

Разбор:

*args    → принимает любые позиционные аргументы
**kwargs → принимает любые именованные аргументы
func(*args, **kwargs) → передаёт их в исходную функцию

Пример:

def log_call(func):
    def wrapper(*args, **kwargs):
        print("args:", args)
        print("kwargs:", kwargs)
        return func(*args, **kwargs)

    return wrapper


@log_call
def create_user(username, age=None):
    return {"username": username, "age": age}


print(create_user("alex", age=25))

Результат:

args: ('alex',)
kwargs: {'age': 25}
{'username': 'alex', 'age': 25}

Проблема без functools.wraps #

Обычный декоратор заменяет исходную функцию на wrapper.

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@decorator
def hello():
    """Документация функции hello."""
    print("Hello")


print(hello.__name__)
print(hello.__doc__)

Результат:

wrapper
None

Проблема: Python теперь видит не исходную hello, а внутреннюю функцию wrapper.

Поэтому обычно используют functools.wraps:

from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@decorator
def hello():
    """Документация функции hello."""
    print("Hello")


print(hello.__name__)
print(hello.__doc__)

Результат:

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

functools.wraps нужен, чтобы сохранить метаданные исходной функции: имя, документацию и другую служебную информацию. В документации functools он описан как удобная обёртка над update_wrapper.


Декоратор с параметрами #

Иногда нужно передать настройки самому декоратору.

Например:

from functools import wraps


def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None

            for _ in range(times):
                result = func(*args, **kwargs)

            return result

        return wrapper

    return decorator


@repeat(3)
def say_hello():
    print("Hello")


say_hello()

Результат:

Hello
Hello
Hello

Здесь три уровня:

repeat(3)
возвращает decorator
decorator получает say_hello
возвращает wrapper

То есть декоратор с параметрами — это функция, которая возвращает настоящий декоратор.


Несколько декораторов #

Можно навесить несколько декораторов:

@decorator_a
@decorator_b
def func():
    ...

Это эквивалентно:

def func():
    ...

func = decorator_a(decorator_b(func))

Порядок важен:

сначала применяется decorator_b
потом decorator_a

А при вызове функции внешняя обёртка вызывается первой.


Пример из реального Python #

Встроенные @staticmethod и @classmethod — это тоже декораторы. Официальная документация приводит их как типичные примеры декораторов.

class User:
    @staticmethod
    def validate_name(name):
        return len(name) >= 3

    @classmethod
    def create_guest(cls):
        return cls()

Также часто встречается @property:

class User:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

После этого name вызывается как атрибут:

user = User("Alex")

print(user.name)

А не как метод:

user.name()

Главное отличие от обычной функции #

Обычная функция:

def func():
    ...

Декоратор:

def decorator(func):
    def wrapper(*args, **kwargs):
        ...
        return func(*args, **kwargs)

    return wrapper

Применение:

@decorator
def func():
    ...

Смысл:

исходная функция
передаётся в декоратор
декоратор возвращает новую функцию
имя функции начинает ссылаться на эту новую функцию

Итог #

Декоратор в Python — это способ обернуть функцию или класс дополнительной логикой без изменения их исходного кода.

Минимальная схема:

from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # логика до вызова
        result = func(*args, **kwargs)
        # логика после вызова
        return result

    return wrapper

Главная идея:

@decorator
def func():
    ...

равно:

func = decorator(func)

На практике декораторы особенно полезны для логирования, авторизации, кэширования, middleware, измерения времени выполнения и повторного использования общей логики.


7. Как работает декоратор с параметрами и почему его реализация обычно включает несколько уровней вложенных функций? #

Декоратор с параметрами — это не сам декоратор напрямую, а функция, которая сначала принимает настройки, затем возвращает настоящий декоратор.

@repeat(3)
def say_hello():
    print("Hello")

Фактически Python воспринимает это примерно так:

def say_hello():
    print("Hello")

say_hello = repeat(3)(say_hello)

Обычный декоратор принимает функцию и возвращает новую функцию. Синтаксис @decorator является синтаксическим сахаром над присваиванием вида func = decorator(func). Это описано в Python glossary и PEP 318.


Обычный декоратор без параметров #

from functools import wraps


def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Вызов функции")
        return func(*args, **kwargs)

    return wrapper


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

Это равно:

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

add = log_call(add)

Здесь log_call сразу получает функцию add.

log_call
└── получает func
    └── возвращает wrapper

Декоратор с параметрами #

Теперь нужен параметр, например количество повторов:

from functools import wraps


def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None

            for _ in range(times):
                result = func(*args, **kwargs)

            return result

        return wrapper

    return decorator


@repeat(3)
def say_hello():
    print("Hello")


say_hello()

Результат:

Hello
Hello
Hello

Почему здесь три уровня функций #

Потому что есть три разные задачи.

def repeat(times):              # 1 уровень
    def decorator(func):        # 2 уровень
        def wrapper(*args, **kwargs):  # 3 уровень
            ...
        return wrapper
    return decorator

Разбор:

repeat(times)
│ принимает параметры декоратора
│ например: times = 3
└── decorator(func)
    │ принимает декорируемую функцию
    │ например: say_hello
    └── wrapper(*args, **kwargs)
        │ вызывается вместо исходной функции
        │ принимает аргументы реального вызова

Что происходит при определении функции #

Когда Python видит:

@repeat(3)
def say_hello():
    print("Hello")

он делает примерно следующее:

decorator = repeat(3)

def say_hello():
    print("Hello")

say_hello = decorator(say_hello)

Пошагово:

1. Вызывается repeat(3)
2. repeat возвращает decorator
3. Python передаёт say_hello в decorator
4. decorator возвращает wrapper
5. имя say_hello теперь указывает на wrapper

Важно: repeat(3) вызывается при определении функции, а не при каждом вызове say_hello().


Что происходит при вызове функции #

После декорирования имя say_hello уже ссылается на wrapper.

say_hello()

Фактически вызывает:

wrapper()

А внутри wrapper уже вызывается исходная функция:

for _ in range(times):
    result = func(*args, **kwargs)

Где:

times = 3
func = исходная say_hello

times и func сохраняются во вложенных функциях через замыкание. Это обычное поведение вложенных функций: внутренняя функция может использовать имена из внешней области видимости. В документации Python это относится к механизму nested scopes/free variables.


Почему нельзя сделать только два уровня #

Допустим, написать так:

def repeat(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper

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

@repeat
def func():
    ...

Но нельзя нормально использовать так:

@repeat(3)
def func():
    ...

Потому что при записи @repeat(3) Python сначала вызывает repeat(3). Значит, repeat должен принять 3 и вернуть настоящий декоратор.

То есть нужна такая схема:

repeat(3)        → возвращает decorator
decorator(func)  → возвращает wrapper
wrapper()        → выполняет дополнительную логику

Зачем нужен functools.wraps #

Обычно внутри декоратора используют @wraps(func):

from functools import wraps


def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None

            for _ in range(times):
                result = func(*args, **kwargs)

            return result

        return wrapper

    return decorator

Без wraps у функции могут потеряться метаданные:

__name__
__doc__
__module__
__annotations__

functools.wraps используется, чтобы wrapper выглядел как исходная функция с точки зрения имени, документации и других атрибутов. В документации functools.wraps описан как удобная обёртка над update_wrapper. ( Python documentation)


Пример с параметром проверки прав #

from functools import wraps


def require_role(required_role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if user["role"] != required_role:
                raise PermissionError("Недостаточно прав")

            return func(user, *args, **kwargs)

        return wrapper

    return decorator


@require_role("admin")
def delete_user(user, user_id):
    print(f"Удаляем пользователя {user_id}")


admin = {"role": "admin"}

delete_user(admin, 42)

Разбор:

require_role("admin")
сохраняет required_role = "admin"
decorator получает delete_user
wrapper проверяет роль при каждом вызове delete_user()

Главное правило #

Декоратор с параметрами имеет несколько уровней, потому что каждый уровень работает в разный момент времени:

1 уровень: принимает настройки декоратора
2 уровень: принимает декорируемую функцию
3 уровень: принимает аргументы вызова функции

Минимальный шаблон:

from functools import wraps


def decorator_with_params(param):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # логика с использованием param
            return func(*args, **kwargs)

        return wrapper

    return decorator

Смысл:

@decorator_with_params(param)
def func():
    ...

равен:

func = decorator_with_params(param)(func)


8. Что происходит с аргументами при передаче в функцию, и как они связываются с параметрами? #

При вызове функции Python берёт переданные аргументы и связывает их с параметрами функции.

def func(a, b):
    print(a, b)


func(10, 20)

Разбор:

10 → a
20 → b

В документации Python аргумент описывается как значение, переданное функции при вызове, а параметры функции получают значения из позиционных аргументов, именованных аргументов или значений по умолчанию.


Аргумент vs параметр #

Параметры — это имена в определении функции:

def create_user(username, age):
    ...

Здесь параметры:

username
age

Аргументы — это значения при вызове:

create_user("alex", 25)

Здесь аргументы:

"alex"
25

То есть:

параметр → имя внутри функции
аргумент → значение, которое передали в это имя

Связывание позиционных аргументов #

Позиционные аргументы связываются по порядку:

def func(a, b, c):
    print(a, b, c)


func(10, 20, 30)

Разбор:

a = 10
b = 20
c = 30

Python сначала размещает позиционные аргументы в первые свободные слоты формальных параметров. Это правило описано в разделе Calls официальной документации.


Связывание именованных аргументов #

Именованные аргументы связываются по имени параметра:

def func(a, b, c):
    print(a, b, c)


func(c=30, a=10, b=20)

Результат:

10 20 30

Разбор:

a = 10
b = 20
c = 30

Порядок именованных аргументов здесь не важен, потому что Python сопоставляет их по имени.


Смешанный вызов #

def func(a, b, c):
    print(a, b, c)


func(10, c=30, b=20)

Разбор:

10   → a
c=30 → c
b=20 → b

Результат:

10 20 30

Но позиционные аргументы должны идти до обычных именованных:

func(a=10, 20, 30)

Так нельзя:

SyntaxError: positional argument follows keyword argument

Значения по умолчанию #

Если параметр имеет значение по умолчанию, аргумент можно не передавать:

def connect(host, port=5432):
    print(host, port)


connect("localhost")

Разбор:

host = "localhost"
port = 5432

Если аргумент передан, он заменяет значение по умолчанию:

connect("localhost", 5433)

Разбор:

host = "localhost"
port = 5433

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


Ошибка: не хватает аргументов #

def func(a, b):
    print(a, b)


func(10)

Ошибка:

TypeError: func() missing 1 required positional argument: 'b'

Потому что:

a = 10
b = ?

Для b нет ни переданного аргумента, ни значения по умолчанию.


Ошибка: слишком много аргументов #

def func(a, b):
    print(a, b)


func(10, 20, 30)

Ошибка:

TypeError: func() takes 2 positional arguments but 3 were given

Потому что функция ожидает два параметра:

a
b

А передано три позиционных значения:

10
20
30

Ошибка: аргумент передан дважды #

def func(a, b):
    print(a, b)


func(10, a=20)

Ошибка:

TypeError: func() got multiple values for argument 'a'

Разбор:

10   → a
a=20 → a

Параметр a получил значение два раза. Документация указывает, что если слот параметра уже заполнен, повторное заполнение через keyword-аргумент приводит к TypeError.


*args #

Если в сигнатуре есть *args, лишние позиционные аргументы собираются в кортеж:

def func(a, *args):
    print("a:", a)
    print("args:", args)


func(1, 2, 3, 4)

Результат:

a: 1
args: (2, 3, 4)

Разбор:

1       → a
2, 3, 4 → args

По документации параметр вида *identifier получает лишние позиционные аргументы, а если их нет — пустой кортеж.


**kwargs #

Если в сигнатуре есть **kwargs, лишние именованные аргументы собираются в словарь:

def func(a, **kwargs):
    print("a:", a)
    print("kwargs:", kwargs)


func(a=1, name="alex", age=25)

Результат:

a: 1
kwargs: {'name': 'alex', 'age': 25}

Разбор:

a=1         → a
name="alex" → kwargs["name"]
age=25      → kwargs["age"]

По документации параметр вида **identifier получает лишние keyword-аргументы, а если их нет — пустое отображение.


Keyword-only параметры #

Параметры после * или *args можно передавать только по имени:

def func(a, *, limit, offset=0):
    print(a, limit, offset)


func(10, limit=100)

Разбор:

a = 10
limit = 100
offset = 0

А так нельзя:

func(10, 100)

Ошибка:

TypeError: func() takes 1 positional argument but 2 were given

Смысл * здесь:

всё после * должно передаваться только по имени

Документация Python прямо указывает, что параметры после * или *identifier являются keyword-only.


Positional-only параметры #

Параметры до / можно передавать только позиционно:

def func(a, b, /, c):
    print(a, b, c)


func(1, 2, c=3)

Разбор:

a = 1
b = 2
c = 3

А так нельзя:

func(a=1, b=2, c=3)

Ошибка:

TypeError: func() got some positional-only arguments passed as keyword arguments

Смысл /:

всё до / должно передаваться только позиционно

Документация Python указывает, что параметры перед / являются positional-only и могут передаваться только позиционными аргументами.


Полная схема связывания #

Пример:

def func(a, b, /, c, d=4, *args, e, f=6, **kwargs):
    print(a, b, c, d, args, e, f, kwargs)


func(1, 2, 3, 40, 50, 60, e=70, x=80, y=90)

Результат:

1 2 3 40 (50, 60) 70 6 {'x': 80, 'y': 90}

Разбор:

1        → a
2        → b
3        → c
40       → d
50, 60   → args
e=70     → e
f        → 6 по умолчанию
x=80     → kwargs["x"]
y=90     → kwargs["y"]

Общая логика:

1. Позиционные аргументы заполняют параметры слева направо
2. Именованные аргументы заполняют параметры по имени
3. Параметры без значения получают default, если он есть
4. Лишние позиционные уходят в *args
5. Лишние именованные уходят в **kwargs
6. Если параметр остался без значения — TypeError
7. Если параметр получил значение дважды — TypeError

Важно: передаётся не копия объекта #

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

def change_list(items):
    items.append(3)


numbers = [1, 2]
change_list(numbers)

print(numbers)

Результат:

[1, 2, 3]

Разбор:

numbers ─┐
         ├──> [1, 2]
items   ─┘

numbers и items — разные имена, но они ссылаются на один и тот же список.


Но переприсваивание параметра не меняет внешнюю переменную #

def reassign(items):
    items = [100, 200]


numbers = [1, 2]
reassign(numbers)

print(numbers)

Результат:

[1, 2]

Разбор:

до присваивания:

numbers ─┐
         ├──> [1, 2]
items   ─┘

после items = [100, 200]:

numbers ───> [1, 2]
items   ───> [100, 200]

То есть изменение объекта видно снаружи, если объект изменяемый. Но перепривязка локального имени items не меняет внешнее имя numbers.


Итог #

Аргументы при вызове функции связываются с параметрами функции по правилам сигнатуры:

def func(a, b=10, *args, c, **kwargs):
    ...
a, b      → обычные параметры
b=10      → значение по умолчанию
*args     → лишние позиционные аргументы
c         → keyword-only параметр
**kwargs  → лишние именованные аргументы

Главная формула:

аргумент — это переданное значение
параметр — это локальное имя внутри функции
связывание — это присваивание переданных значений этим локальным именам


9. Какой метод должен быть реализован в классе, чтобы его экземпляр можно было использовать как декоратор функций? #

Чтобы экземпляр класса можно было использовать как декоратор функции, в классе должен быть реализован метод:

__call__

Потому что декоратор должен быть вызываемым объектом — callable. В Python объект становится вызываемым, если его класс реализует __call__. Официальная документация описывает object.__call__ как метод, который вызывается при вызове экземпляра как функции.


Пример #

from functools import wraps


class Logger:
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Вызов функции: {func.__name__}")
            return func(*args, **kwargs)

        return wrapper


logger = Logger()


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


print(add(2, 3))

Результат:

Вызов функции: add
5

Что здесь происходит #

Запись:

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

эквивалентна:

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

add = logger(add)

А так как logger — это экземпляр класса, выражение:

logger(add)

работает только потому, что у класса Logger есть метод:

def __call__(self, func):
    ...

То есть Python фактически вызывает:

Logger.__call__(logger, add)

Схема #

Logger()
создаётся объект logger

@logger
Python вызывает logger(add)

logger(add)
срабатывает logger.__call__(add)

__call__ возвращает wrapper
add теперь указывает на wrapper

Декоратор-класс с параметрами #

Часто класс-декоратор используют, когда декоратору нужны настройки.

from functools import wraps


class Repeat:
    def __init__(self, times):
        self.times = times

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None

            for _ in range(self.times):
                result = func(*args, **kwargs)

            return result

        return wrapper


@Repeat(3)
def say_hello():
    print("Hello")


say_hello()

Результат:

Hello
Hello
Hello

Как это раскрывается #

@Repeat(3)
def say_hello():
    print("Hello")

примерно равно:

def say_hello():
    print("Hello")

decorator = Repeat(3)
say_hello = decorator(say_hello)

Разбор:

Repeat(3)
вызывается __init__
создаётся объект decorator с self.times = 3

decorator(say_hello)
вызывается __call__
возвращается wrapper

Главное #

Минимальный шаблон класса-декоратора:

from functools import wraps


class MyDecorator:
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)

        return wrapper

Использование:

decorator = MyDecorator()


@decorator
def func():
    ...

Итог:

экземпляр класса может быть декоратором,
если он является callable-объектом

для этого в классе нужен метод __call__


10. Как реализована встроенная функция вычисления факториала в Python на уровне стандартной библиотеки и языка? #

В Python факториал вызывается так:

import math

math.factorial(5)  # 120

Технически это не функция из builtins, а функция модуля math стандартной библиотеки. Документация описывает math.factorial(n) как функцию, возвращающую факториал неотрицательного целого числа n; начиная с Python 3.10, float со значением целого числа, например 5.0, больше не принимается.


На уровне Python API #

import math

print(math.factorial(0))  # 1
print(math.factorial(1))  # 1
print(math.factorial(5))  # 120

Математически:

5! = 1 * 2 * 3 * 4 * 5 = 120

Но внутри CPython это не реализовано как простой Python-код вида:

def factorial(n):
    result = 1

    for i in range(2, n + 1):
        result *= i

    return result

Такой код показывает идею, но не реальную реализацию стандартной библиотеки.


Где реализован factorial #

В CPython реализация находится на C-уровне.

В современных версиях CPython код факториала находится в исходниках модуля math / math.integer, а не в .py-файле. В исходниках CPython прямо указано, что используется divide-and-conquer factorial algorithm, то есть алгоритм «разделяй и властвуй», а не простое последовательное умножение 1 * 2 * ... * n.


Общая схема работы #

Упрощённо:

math.factorial(n)
проверка и преобразование аргумента
проверка, что n >= 0
если n маленькое → взять ответ из таблицы
если n большое → вычислить через оптимизированный C-алгоритм
вернуть Python int

Проверка аргумента #

В CPython аргумент приводится к C-типу long через C API. Для этого используется механизм, который работает с Python int и объектами, имеющими __index__(). Документация C API указывает, что PyLong_AsLongAndOverflow() сначала вызывает __index__(), если объект не является PyLongObject, а с Python 3.10 больше не использует __int__().

Пример объекта, который может быть принят:

import math


class Number:
    def __index__(self):
        return 5


print(math.factorial(Number()))

Результат:

120

А вот float больше не принимается:

import math

math.factorial(5.0)

Будет ошибка:

TypeError

Проверка на отрицательное значение #

Факториал определён только для неотрицательных целых чисел:

import math

math.factorial(-1)

В CPython для отрицательного значения выставляется ValueError с сообщением, что factorial() не определён для отрицательных значений. Это видно в реализации функции, где после преобразования аргумента проверяется x < 0. ( GitHub)


Оптимизация для маленьких значений #

Для маленьких n CPython не считает факториал каждый раз заново.

В исходниках есть таблица заранее посчитанных значений:

0!  = 1
1!  = 1
2!  = 2
3!  = 6
4!  = 24
5!  = 120
...

В коде CPython эта таблица называется SmallFactorials. Если n попадает в диапазон таблицы, результат возвращается сразу.

Упрощённо:

SMALL_FACTORIALS = [
    1,
    1,
    2,
    6,
    24,
    120,
    720,
    5040,
    40320,
]

Идея:

if n < len(SMALL_FACTORIALS):
    return SMALL_FACTORIALS[n]

Алгоритм для больших значений #

Для больших n CPython считает факториал не как линейное умножение:

1 * 2 * 3 * 4 * ... * n

А представляет факториал в виде:

n! = odd_part * 2**k

Где:

odd_part → нечётная часть факториала
2**k     → степень двойки, содержащаяся в факториале

В комментариях исходного кода CPython прямо указано, что факториал записывается как 2**k * m, где m — нечётная часть, а k и m вычисляются отдельно. Затем результат собирается через битовый сдвиг.


Почему отдельно считают степень двойки #

В произведении 1 * 2 * 3 * ... * n очень много множителей 2.

Например:

10! = 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10

Чётные множители содержат степени двойки:

2  = 2 * 1
4  = 2**2
6  = 2 * 3
8  = 2**3
10 = 2 * 5

CPython выносит все двойки отдельно:

n! = нечётная_часть * 2**k

А умножение на 2**k для большого целого числа можно сделать сдвигом влево:

odd_part << k

В исходниках CPython two_valuation вычисляется как:

n - count_set_bits(n)

где count_set_bits(n) — количество единичных битов в двоичной записи числа. Это соответствует формуле n//2 + n//4 + n//8 + ....


Divide-and-conquer умножение #

Нечётная часть факториала считается через рекурсивное деление произведения на части.

В исходниках есть функция factorial_partial_product, которая считает произведение нечётных чисел в диапазоне и рекурсивно делит диапазон на две части. В комментариях прямо указано, что она работает через divide and conquer и разбивает диапазон на примерно равные куски, пока поддиапазоны не станут достаточно маленькими для быстрого C-умножения. ( GitHub)

Упрощённая идея:

def product_range(start, stop):
    if stop - start маленький:
        return обычное_умножение(start, stop)

    middle = (start + stop) // 2

    return product_range(start, middle) * product_range(middle, stop)

Это лучше, чем длинная цепочка:

result = 1

for i in range(1, n + 1):
    result *= i

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


Почему это быстрее простого цикла #

Простой цикл делает примерно так:

1
1 * 2
2 * 3
6 * 4
24 * 5
120 * 6
...

То есть большое накопленное число постоянно умножается на маленькое.

Divide-and-conquer делает более сбалансированное дерево умножений:

(1 * 3 * 5 * 7) * (9 * 11 * 13 * 15)

А не:

((((1 * 3) * 5) * 7) * 9) * 11 ...

Для больших n сбалансированное умножение эффективнее.


Упрощённая модель реализации #

Очень грубо реальную идею CPython можно представить так:

def factorial(n):
    n = operator.index(n)

    if n < 0:
        raise ValueError("factorial() not defined for negative values")

    if n in small_factorials:
        return small_factorials[n]

    odd_part = compute_odd_part_with_divide_and_conquer(n)
    power_of_two = n - count_set_bits(n)

    return odd_part << power_of_two

Это не реальный код CPython, а приближённая Python-модель.


На уровне языка #

На уровне самого языка Python нет отдельного оператора факториала.

То есть такого синтаксиса нет:

5!

Правильный способ:

import math

math.factorial(5)

math.factorial() возвращает обычный Python int, а Python int поддерживает целые числа произвольной точности, поэтому результат может быть очень большим:

import math

print(math.factorial(100))

Итог #

math.factorial() в CPython реализован не как простой Python-цикл, а как C-функция стандартной библиотеки.

Главные детали:

math.factorial(n)
это функция модуля math, не builtins
принимает неотрицательное целое число
float вроде 5.0 не принимается с Python 3.10
для маленьких n используется таблица готовых значений
для больших n используется divide-and-conquer алгоритм
n! представляется как odd_part * 2**k
умножение на 2**k делается битовым сдвигом
результат возвращается как Python int

Ключевая мысль: стандартный math.factorial() — это не учебная реализация через for, а оптимизированная реализация CPython на C с отдельной обработкой маленьких значений, нечётной части факториала и степени двойки.


11. Функция map() #

map() — встроенная функция Python, которая применяет указанную функцию к каждому элементу итерируемого объекта и возвращает итератор. В официальной документации сигнатура указана так: map(function, iterable, /, *iterables, strict=False).

numbers = [1, 2, 3, 4]

result = map(lambda x: x * 2, numbers)

print(result)
print(list(result))

Результат:

<map object at ...>
[2, 4, 6, 8]

Как работает map() #

Общая форма:

map(function, iterable)

Например:

numbers = [1, 2, 3, 4]

def square(x):
    return x ** 2

result = map(square, numbers)

print(list(result))

Результат:

[1, 4, 9, 16]

Схема:

numbers = [1, 2, 3, 4]

square(1) → 1
square(2) → 4
square(3) → 9
square(4) → 16

То есть map() не фильтрует элементы, а преобразует каждый элемент.


map() возвращает итератор #

В Python 3 map() возвращает не список, а map object, то есть ленивый итератор.

numbers = [1, 2, 3]

result = map(lambda x: x * 10, numbers)

print(result)

Результат:

<map object at ...>

Чтобы получить список:

print(list(result))

Результат:

[10, 20, 30]

Важно: итератор можно пройти только один раз.

numbers = [1, 2, 3]

result = map(lambda x: x * 2, numbers)

print(list(result))
print(list(result))

Результат:

[2, 4, 6]
[]

После первого list(result) итератор уже исчерпан.


map() с lambda #

Часто map() используют с lambda:

numbers = [1, 2, 3, 4]

result = list(map(lambda x: x ** 2, numbers))

print(result)

Результат:

[1, 4, 9, 16]

Но в современном Python такой вариант обычно заменяют list comprehension:

numbers = [1, 2, 3, 4]

result = [x ** 2 for x in numbers]

print(result)

Результат тот же:

[1, 4, 9, 16]

В документации Python прямо показан пример, где list(map(lambda x: x**2, range(10))) эквивалентен [x**2 for x in range(10)], при этом list comprehension назван более кратким и читаемым вариантом.


map() с готовой функцией #

map() хорошо смотрится, когда уже есть готовая функция:

words = ["hello", "python", "map"]

result = list(map(len, words))

print(result)

Результат:

[5, 6, 3]

Или:

strings = ["1", "2", "3", "4"]

numbers = list(map(int, strings))

print(numbers)

Результат:

[1, 2, 3, 4]

Вот здесь map(int, strings) читается нормально, потому что int уже готовая функция.


map() с несколькими iterable #

map() может принимать несколько итерируемых объектов.

a = [1, 2, 3]
b = [10, 20, 30]

result = list(map(lambda x, y: x + y, a, b))

print(result)

Результат:

[11, 22, 33]

Схема:

1 + 10 → 11
2 + 20 → 22
3 + 30 → 33

Если передано несколько iterable, функция должна принимать столько аргументов, сколько iterable передано. Документация указывает, что map() применяет функцию к элементам из нескольких iterable параллельно.


Что будет, если длины разные #

a = [1, 2, 3]
b = [10, 20]

result = list(map(lambda x, y: x + y, a, b))

print(result)

Результат:

[11, 22]

Почему:

1 + 10 → 11
2 + 20 → 22
3      → пары нет, поэтому остановка

По умолчанию map() останавливается, когда заканчивается самый короткий iterable.


Параметр strict #

В Python 3.14 у map() добавили параметр strict.

a = [1, 2, 3]
b = [10, 20]

result = list(map(lambda x, y: x + y, a, b, strict=True))

При разных длинах будет ошибка:

ValueError

Смысл:

strict=False → остановиться по самому короткому iterable
strict=True  → выбросить ошибку, если длины отличаются

Документация Python 3.14 указывает, что strict=True вызывает ValueError, если один iterable закончился раньше другого.


Чем заменить map() #

Чаще всего map() заменяют list comprehension:

result = list(map(lambda x: x * 2, numbers))

Лучше:

result = [x * 2 for x in numbers]

Если нужен не список, а ленивое вычисление:

result = (x * 2 for x in numbers)

Сравнение:

map(lambda x: x * 2, numbers)
(x * 2 for x in numbers)

list(map(lambda x: x * 2, numbers))
[x * 2 for x in numbers]

Когда map() уместен #

map() стоит использовать, когда преобразование выражается готовой функцией:

list(map(int, strings))
list(map(str.upper, names))
list(map(len, words))

А если нужна lambda, часто читаемее comprehension:

# хуже читается
list(map(lambda x: x.price * 1.2, products))

# обычно лучше
[product.price * 1.2 for product in products]

Итог #

map():

map(function, iterable)

означает:

возьми каждый элемент iterable
примени к нему function
верни ленивый итератор с результатами

Главное:

map() не возвращает список напрямую
map() применяет функцию к каждому элементу
map() останавливается по самому короткому iterable
map() удобно использовать с готовыми функциями
с lambda часто лучше заменить на list comprehension

Минимальная формула:

result = map(func, items)

эквивалентна идее:

result = (func(item) for item in items)


12. Функция reduce() #

reduce() — функция из модуля functools, которая последовательно сворачивает iterable в одно итоговое значение.

from functools import reduce

Общий вид:

reduce(function, iterable, initial)

Идея:

берём первые два значения
применяем к ним функцию
полученный результат используем как accumulator
идём дальше по iterable
получаем одно итоговое значение

В документации Python functools.reduce() описан как функция, которая применяет функцию двух аргументов к элементам iterable слева направо, чтобы свести их к одному значению.

Простой пример #

from functools import reduce

numbers = [1, 2, 3, 4, 5]

result = reduce(lambda acc, x: acc + x, numbers)

print(result)

Результат:

15

Как это считается:

1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15

То есть:

reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5])

эквивалентен:

((((1 + 2) + 3) + 4) + 5)

Что такое acc и x #

В функции для reduce() обычно два аргумента:

lambda acc, x: acc + x

Где:

acc → accumulator, накопленное значение
x   → текущий элемент iterable

Пример:

from functools import reduce

numbers = [2, 3, 4]

result = reduce(lambda acc, x: acc * x, numbers)

print(result)

Результат:

24

Разбор:

acc = 2, x = 3 → 2 * 3 = 6
acc = 6, x = 4 → 6 * 4 = 24

initial #

Третий аргумент initial задаёт начальное значение аккумулятора.

from functools import reduce

numbers = [1, 2, 3]

result = reduce(lambda acc, x: acc + x, numbers, 10)

print(result)

Результат:

16

Разбор:

initial = 10

10 + 1 = 11
11 + 2 = 13
13 + 3 = 16

То есть initial как бы ставится перед элементами iterable. Документация Python указывает, что initial используется как начальное значение и как значение по умолчанию, если iterable пустой.

Поведение с пустым iterable #

Без initial пустой iterable даст ошибку:

from functools import reduce

reduce(lambda acc, x: acc + x, [])

Результат:

TypeError

Потому что Python не знает, с какого значения начинать.

С initial всё нормально:

from functools import reduce

result = reduce(lambda acc, x: acc + x, [], 0)

print(result)

Результат:

0

Упрощённая реализация #

Документация Python показывает примерную реализацию reduce() через цикл: сначала берётся итератор, затем начальное значение, после чего каждый следующий элемент применяет функцию к накопленному значению.

Упрощённо:

def my_reduce(function, iterable, initial=None):
    iterator = iter(iterable)

    if initial is None:
        value = next(iterator)
    else:
        value = initial

    for element in iterator:
        value = function(value, element)

    return value

Но это учебная версия. В реальной реализации нужно аккуратно отличать ситуацию, когда initial не передали, от ситуации, когда передали None.

Более корректная версия:

_MISSING = object()


def my_reduce(function, iterable, initial=_MISSING):
    iterator = iter(iterable)

    if initial is _MISSING:
        value = next(iterator)
    else:
        value = initial

    for element in iterator:
        value = function(value, element)

    return value

Где reduce() полезен #

reduce() полезен, когда нужно собрать одно значение из последовательности, и операция не выражается простой встроенной функцией.

Например, собрать словарь:

from functools import reduce

pairs = [("a", 1), ("b", 2), ("c", 3)]

result = reduce(
    lambda acc, item: {**acc, item[0]: item[1]},
    pairs,
    {},
)

print(result)

Результат:

{'a': 1, 'b': 2, 'c': 3}

Но на практике такой код часто лучше написать обычным циклом:

pairs = [("a", 1), ("b", 2), ("c", 3)]

result = {}

for key, value in pairs:
    result[key] = value

print(result)

Этот вариант читается проще.

Чем обычно заменяют reduce() #

Во многих случаях reduce() лучше заменить специальной встроенной функцией:

from functools import reduce

numbers = [1, 2, 3, 4]

Сумма:

sum(numbers)

Вместо:

reduce(lambda acc, x: acc + x, numbers)

Произведение:

import math

math.prod(numbers)

Вместо:

reduce(lambda acc, x: acc * x, numbers)

Проверка, что все элементы подходят:

all(x > 0 for x in numbers)

Вместо:

reduce(lambda acc, x: acc and x > 0, numbers, True)

Проверка, что хотя бы один подходит:

any(x > 10 for x in numbers)

Вместо:

reduce(lambda acc, x: acc or x > 10, numbers, False)

Максимум:

max(numbers)

Вместо:

reduce(lambda acc, x: acc if acc > x else x, numbers)

Минимум:

min(numbers)

Вместо:

reduce(lambda acc, x: acc if acc < x else x, numbers)

reduce() vs accumulate() #

reduce() возвращает только итоговое значение:

from functools import reduce

numbers = [1, 2, 3, 4]

result = reduce(lambda acc, x: acc + x, numbers)

print(result)

Результат:

10

А itertools.accumulate() возвращает промежуточные значения:

from itertools import accumulate

numbers = [1, 2, 3, 4]

result = list(accumulate(numbers))

print(result)

Результат:

[1, 3, 6, 10]

Официальная документация reduce() отдельно указывает смотреть itertools.accumulate(), когда нужны все промежуточные значения, а не только финальный результат.

Важный момент #

reduce() не является встроенной функцией уровня builtins в Python 3. Его нужно импортировать:

from functools import reduce

Это хороший сигнал: в обычном коде Python reduce() используют реже, чем map() или filter().

Итог #

reduce():

reduce(function, iterable, initial)

означает:

последовательно применить function к накопленному значению и текущему элементу,
чтобы получить одно итоговое значение

Главная схема:

[1, 2, 3, 4]

1 + 2 = 3
3 + 3 = 6
6 + 4 = 10

Минимальный пример:

from functools import reduce

result = reduce(lambda acc, x: acc + x, [1, 2, 3, 4])

print(result)  # 10

На практике reduce() стоит использовать осторожно: если есть sum(), math.prod(), any(), all(), min(), max() или обычный for, они чаще будут понятнее.


13. Что такое functools.partial? #

functools.partial — это функция, которая создаёт новый вызываемый объект с заранее зафиксированной частью аргументов.

from functools import partial

Идея:

было:
func(a, b, c)

фиксируем a:
new_func = partial(func, a)

теперь:
new_func(b, c)

Официальная документация описывает partial() как инструмент частичного применения функции: он “замораживает” часть позиционных и/или именованных аргументов и возвращает новый объект с упрощённой сигнатурой.

Простой пример #

from functools import partial


def multiply(a, b):
    return a * b


double = partial(multiply, 2)

print(double(10))
print(double(25))

Результат:

20
50

Что произошло:

double = partial(multiply, 2)

double(10)
multiply(2, 10)
20

То есть partial заранее подставил первый аргумент a = 2.

Эквивалент через обычную функцию #

Этот код:

double = partial(multiply, 2)

примерно похож на:

def double(b):
    return multiply(2, b)

Но partial делает это универсально, без ручного написания обёртки.

partial с именованными аргументами #

from functools import partial


def create_user(username, role, is_active=True):
    return {
        "username": username,
        "role": role,
        "is_active": is_active,
    }


create_admin = partial(create_user, role="admin")

print(create_admin("alex"))

Результат:

{'username': 'alex', 'role': 'admin', 'is_active': True}

Разбор:

create_admin("alex")
create_user("alex", role="admin")

То есть мы заранее зафиксировали:

role="admin"

partial сохраняет исходную функцию и аргументы #

Объект, созданный через partial, хранит три важные вещи:

.func      → исходная функция
.args      → заранее переданные позиционные аргументы
.keywords  → заранее переданные именованные аргументы

Документация Python прямо указывает, что partial-объекты являются callable-объектами и имеют read-only атрибуты func, args, keywords.

Пример:

from functools import partial


def multiply(a, b):
    return a * b


double = partial(multiply, 2)

print(double.func)
print(double.args)
print(double.keywords)

Результат будет примерно такой:

<function multiply at ...>
(2,)
{}

Как partial работает внутри #

Упрощённо partial можно представить так:

def my_partial(func, *fixed_args, **fixed_kwargs):
    def wrapper(*args, **kwargs):
        return func(
            *fixed_args,
            *args,
            **fixed_kwargs,
            **kwargs,
        )

    return wrapper

Пример:

def add(a, b, c):
    return a + b + c


add_10 = my_partial(add, 10)

print(add_10(20, 30))

Разбор:

add_10(20, 30)
add(10, 20, 30)
60

Настоящий functools.partial реализован эффективнее, но концептуально идея именно такая.

Где используется partial #

partial полезен, когда нужно получить новую функцию с частью заранее заданных параметров.

Типичные случаи:

1. Создать специализированную версию общей функции
2. Передать функцию с нужными аргументами в callback
3. Упростить вызов функции с длинной сигнатурой
4. Настроить функцию заранее
5. Использовать в map, sorted, обработчиках событий, декораторах

Пример с int #

Классический пример из документации Python:

from functools import partial

basetwo = partial(int, base=2)

print(basetwo("10010"))

Результат:

18

Разбор:

basetwo("10010")
int("10010", base=2)
18

То есть мы создали функцию, которая всегда интерпретирует строку как двоичное число. Этот пример приведён в официальной документации functools.partial.

partial vs lambda #

Часто partial можно заменить на lambda.

from functools import partial

double = partial(multiply, 2)

Примерно равно:

double = lambda x: multiply(2, x)

Но partial обычно лучше, когда нужно просто зафиксировать аргументы без дополнительной логики.

from functools import partial

parse_binary = partial(int, base=2)
parse_hex = partial(int, base=16)

print(parse_binary("1010"))
print(parse_hex("ff"))

Результат:

10
255

Здесь partial читается чище, чем несколько lambda.

Важная особенность с позиционными аргументами #

Обычный partial фиксирует позиционные аргументы слева направо.

from functools import partial


def power(base, exponent):
    return base ** exponent


square_of_2 = partial(power, 2)

print(square_of_2(10))

Результат:

1024

Разбор:

square_of_2(10)
power(2, 10)
1024

То есть зафиксирован первый аргумент base, а не второй.

Для фиксации второго аргумента удобнее использовать keyword-аргумент:

square = partial(power, exponent=2)

print(square(10))

Результат:

100

Разбор:

square(10)
power(10, exponent=2)

partial не является обычной функцией #

Объект partial вызываемый, но это не обычная функция.

Например:

from functools import partial

double = partial(multiply, 2)

print(callable(double))

Результат:

True

Но у него автоматически не создаются обычные атрибуты функции вроде __name__ и __doc__. Это отличие отдельно отмечено в документации Python.

Итог #

functools.partial нужен для частичного применения функции.

Минимальная схема:

from functools import partial


new_func = partial(func, fixed_arg, fixed_kwarg=value)

Это значит:

когда вызовут new_func(...)
Python вызовет func(fixed_arg, ..., fixed_kwarg=value)

Главная идея:

double = partial(multiply, 2)

равно по смыслу:

def double(x):
    return multiply(2, x)

partial особенно полезен, когда нужно заранее настроить функцию и передать её дальше как callback, обработчик, key-функцию или специализированную версию более общей функции.


14. Функция id() #

Функция id() #

id() — встроенная функция Python, которая возвращает уникальный идентификатор объекта в виде целого числа.

id(object)

Официально: это число уникально и постоянно для объекта только во время его жизни. После удаления объекта Python может переиспользовать этот идентификатор для другого объекта.

Пример:

a = [1, 2, 3]
b = a

print(id(a))
print(id(b))

Вывод может быть примерно таким:

2469185232768
2469185232768

Здесь a и b ссылаются на один и тот же объект.

Связь с is #

Оператор is сравнивает не значения, а идентичность объектов: являются ли две переменные ссылками на один и тот же объект. В документации Python это описывается как сравнение identity объекта.

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)      # True
print(a is b)      # False

print(id(a))
print(id(b))

Почему так:

a == b    # значения одинаковые
a is b    # объекты разные

То есть:

a is b

почти то же самое по смыслу, что:

id(a) == id(b)

Но на практике для проверки идентичности используют именно is, а не сравнение id().

В CPython id() часто выглядит как адрес в памяти #

В стандартной реализации Python — CPython — значение id(x) является адресом объекта в памяти. Но это именно деталь реализации CPython, а не общее правило языка Python. В других реализациях Python id() не обязан быть реальным адресом памяти.

x = object()

print(id(x))

Примерный вывод:

2469185273424

Это число не нужно использовать как настоящий указатель, как в C/C++.

Важный пример с изменяемыми объектами #

lst = [1, 2, 3]

print(id(lst))

lst.append(4)

print(id(lst))
print(lst)

Вывод:

2469185232768
2469185232768
[1, 2, 3, 4]

Объект списка изменился по содержимому, но остался тем же объектом.

То есть:

identity не изменилась
value изменилась

Пример с неизменяемыми объектами #

x = 10
print(id(x))

x = x + 1
print(id(x))

Здесь int — неизменяемый объект. При x = x + 1 не меняется старый объект 10, а создаётся или берётся другой объект 11, и имя x начинает ссылаться на него.

Почему иногда странно работает с маленькими числами #

a = 256
b = 256

print(a is b)  # часто True

А вот так:

a = 10000
b = 10000

print(a is b)  # может быть True или False в зависимости от ситуации

Python может кешировать некоторые маленькие числа и строки. Поэтому для чисел и строк нельзя полагаться на is для сравнения значений.

Правильно:

a == b

Неправильно для сравнения значений:

a is b

Когда id() полезна #

id() обычно используют для отладки и изучения объектной модели Python:

a = []
b = a
c = []

print(id(a))
print(id(b))
print(id(c))
a и b → один объект
c     → другой объект

Также id() помогает понять, создаётся ли новый объект или изменяется старый.

Главное #

id(obj)

возвращает идентификатор объекта.

is

проверяет, один и тот же это объект или нет.

==

проверяет равенство значений.

Кратко:

id()  → идентификатор объекта
is    → сравнение объектов по identity
==    → сравнение объектов по value

В CPython id() фактически связан с адресом объекта в памяти, но писать код с расчётом на это не стоит.


15. Как замерить время выполнения функции? #

Как замерить время выполнения функции #

Для обычного замера времени выполнения функции в Python чаще всего используют:

from time import perf_counter

perf_counter() подходит именно для измерения промежутков времени: он даёт максимально точный доступный таймер.

Простой способ #

from time import perf_counter


def my_func():
    total = 0
    for i in range(1_000_000):
        total += i
    return total


start = perf_counter()

result = my_func()

end = perf_counter()

print(f"Время выполнения: {end - start:.6f} сек.")

Что происходит:

start = perf_counter()   # время до запуска функции
my_func()                # выполняем функцию
end = perf_counter()     # время после выполнения

end - start              # длительность выполнения

Через отдельную функцию-обёртку #

from time import perf_counter


def measure_time(func, *args, **kwargs):
    start = perf_counter()

    result = func(*args, **kwargs)

    end = perf_counter()

    print(f"Время выполнения {func.__name__}: {end - start:.6f} сек.")

    return result

Пример использования:

def add_numbers(n):
    total = 0
    for i in range(n):
        total += i
    return total


result = measure_time(add_numbers, 1_000_000)

Через декоратор #

Если нужно часто замерять разные функции, удобно сделать декоратор:

from time import perf_counter
from functools import wraps


def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()

        result = func(*args, **kwargs)

        end = perf_counter()

        print(f"{func.__name__} выполнилась за {end - start:.6f} сек.")

        return result

    return wrapper

Использование:

@measure_time
def heavy_func():
    total = 0
    for i in range(10_000_000):
        total += i
    return total


heavy_func()

Вывод будет примерно такой:

heavy_func выполнилась за 0.431526 сек.

Почему perf_counter(), а не time.time() #

time.time() показывает текущее системное время.

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

Для замера длительности лучше:

perf_counter()

А не:

time()

То есть:

from time import perf_counter

лучше для бенчмарков и измерения выполнения кода.

Для точных бенчмарков — timeit #

Если нужно не просто примерно посмотреть время, а нормально сравнить производительность кода, лучше использовать timeit.

from timeit import timeit


def my_func():
    return sum(range(1_000_000))


execution_time = timeit(my_func, number=100)

print(execution_time)

Здесь функция my_func будет выполнена 100 раз, а timeit вернёт общее время.

Можно посчитать среднее:

average_time = execution_time / 100

print(f"Среднее время: {average_time:.6f} сек.")

Когда что использовать #

perf_counter()  → разовый или ручной замер
декоратор       → удобно замерять много функций
timeit          → более корректный бенчмарк

Короткий итог #

Для обычного случая:

from time import perf_counter

start = perf_counter()
func()
end = perf_counter()

print(end - start)

Для повторяемого замера:

from timeit import timeit

print(timeit(func, number=1000))


16. Почему пустой список нельзя использовать как значение аргумента по умолчанию? #

Почему пустой список нельзя использовать как аргумент по умолчанию #

Точнее: использовать можно, Python это не запрещает. Но почти всегда это ошибка.

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

То есть вот такой список:

def add_item(item, items=[]):
    items.append(item)
    return items

создаётся один раз и потом переиспользуется между вызовами функции.

Пример проблемы #

def add_item(item, items=[]):
    items.append(item)
    return items


print(add_item("apple"))
print(add_item("banana"))
print(add_item("orange"))

Результат:

['apple']
['apple', 'banana']
['apple', 'banana', 'orange']

На первый взгляд кажется, что каждый вызов должен получать новый пустой список:

1 вызов → []
2 вызов → []
3 вызов → []

Но реально происходит так:

items создаётся один раз
все вызовы используют один и тот же список

Почему так происходит #

Когда Python видит функцию:

def add_item(item, items=[]):
    ...

он создаёт объект списка [] сразу при определении функции.

Упрощённо:

создали функцию add_item
создали один список []
прикрепили этот список как default value к параметру items

После этого каждый вызов без второго аргумента использует тот же самый объект.

Проверка через id() #

def add_item(item, items=[]):
    print(id(items))
    items.append(item)
    return items


add_item("a")
add_item("b")
add_item("c")

Вывод будет примерно такой:

2534261238464
2534261238464
2534261238464

id одинаковый, значит это один и тот же список.

Правильный способ #

Вместо изменяемого объекта используют None:

def add_item(item, items=None):
    if items is None:
        items = []

    items.append(item)
    return items

Теперь:

print(add_item("apple"))
print(add_item("banana"))
print(add_item("orange"))

Результат:

['apple']
['banana']
['orange']

Каждый вызов без items создаёт новый список.

Почему именно None #

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

def func(items=None):
    if items is None:
        items = []

Важно проверять именно так:

if items is None:

А не так:

if not items:

Потому что пустой список тоже даст False.

Пример проблемы:

def add_item(item, items=None):
    if not items:
        items = []

    items.append(item)
    return items

Если пользователь специально передаст пустой список:

my_list = []

add_item("apple", my_list)

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

Какие значения опасны по умолчанию #

Опасны изменяемые объекты:

def func(items=[]):        # плохо
    ...

def func(data={}):         # плохо
    ...

def func(values=set()):    # плохо
    ...

Потому что list, dict, set можно изменить на месте.

Какие значения безопасны #

Безопасны неизменяемые объекты:

def func(limit=10):
    ...

def func(name="unknown"):
    ...

def func(enabled=True):
    ...

def func(values=()):
    ...

int, str, bool, tuple, None обычно безопасны как значения по умолчанию.

Но иногда это используют специально #

Иногда изменяемый аргумент по умолчанию используют намеренно, например для простого кеша:

def cached(value, cache={}):
    if value not in cache:
        cache[value] = value * 2

    return cache[value]

Но это редкий случай. В обычном коде так лучше не писать, потому что поведение неочевидное.

Коротко #

Плохо:

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

Главная причина:

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


17. Может ли параметр функции иметь значение по умолчанию в Python? #

Может ли параметр функции иметь значение по умолчанию #

Да, в Python параметр функции может иметь значение по умолчанию.

Это значит: если при вызове функции аргумент не передали, Python подставит заранее заданное значение.

def greet(name="Гость"):
    print(f"Привет, {name}!")

Вызов без аргумента:

greet()

Результат:

Привет, Гость!

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

greet("Али")

Результат:

Привет, Али!

Пример с несколькими параметрами #

def create_user(username, is_active=True, role="user"):
    return {
        "username": username,
        "is_active": is_active,
        "role": role,
    }

Можно вызвать так:

create_user("admin")

Результат:

{
    "username": "admin",
    "is_active": True,
    "role": "user",
}

А можно переопределить значения по умолчанию:

create_user("admin", is_active=False, role="moderator")

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

Параметры без значения по умолчанию должны идти раньше параметров со значением по умолчанию.

Правильно:

def func(a, b=10):
    pass

Неправильно:

def func(a=10, b):
    pass

Такой код вызовет ошибку:

SyntaxError: non-default argument follows default argument

Почему:

a=10  # параметр со значением по умолчанию
b     # обязательный параметр после него

Python не сможет однозначно понять, как сопоставлять переданные аргументы.

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

Это важный момент.

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 func(limit=10):
    pass


def func(name="unknown"):
    pass


def func(enabled=True):
    pass


def func(data=None):
    pass

Опаснее использовать изменяемые объекты:

def func(items=[]):     # плохо
    pass


def func(data={}):      # плохо
    pass


def func(values=set()): # плохо
    pass

Коротко #

Да, параметр функции может иметь значение по умолчанию:

def func(param="default"):
    pass

Главные правила:

1. Если аргумент не передан — используется значение по умолчанию.
2. Обязательные параметры идут до параметров со значением по умолчанию.
3. Не стоит использовать list, dict, set как значения по умолчанию.
4. Значения по умолчанию создаются один раз — при определении функции.