Вложенные функции Python: назначение, синтаксис и практические сценарии

Раздел: Прочее -> Функции Python

Локальные функции в Python: базовый подход и варианты

Локальная функция Python определяется внутри другой функции. Она доступна только в теле внешней функции, если не возвращается наружу как объект. Такой прием помогает группировать вспомогательную логику, создавать замыкания, декораторы и фабрики функций.

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

def outer(x):
    def inner(y):
        return x + y
    return inner(5)

print(outer(10))

локальная функция python (локальные функции (вложенные) в python)

15

Внешняя функция outer принимает x. Внутренняя inner использует x из enclosing scope и y из своей области. Вызов inner(5) происходит внутри outer, поэтому результат возвращается наружу.

Как вернуть вложенную функцию и сохранить доступ к внешней переменной?

Если inner возвращается без вызова, получается замыкание. Оно запоминает значение x из момента создания.

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
print(add5(3))
print(add5(10))
8
15

Функция make_adder создает новую функцию add для каждого n. Переменная n хранится в ячейке замыкания.

Как изменить переменную внешней функции из вложенной?

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

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c = counter()
print(c())
print(c())
1
2

Без nonlocal возникла бы UnboundLocalError, потому что count += 1 создает локальную переменную count до ее чтения.

Как создать несколько независимых счетчиков через фабрику функций?

Каждый вызов внешней функции создает новое окружение и новую ячейку count.

def make_counter(start=0):
    value = start
    def step():
        nonlocal value
        value += 1
        return value
    return step

c1 = make_counter(10)
c2 = make_counter(100)
print(c1())
print(c2())
print(c1())
11
101
12

Как вложенные функции применяются в декораторах?

Декоратор обычно содержит функцию wrapper, которая вызывает исходную функцию.

def trace(func):
    def wrapper(*args, **kwargs):
        print('Call', func.__name__)
        result = func(*args, **kwargs)
        print('Result', result)
        return result
    return wrapper

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

print(add(2, 3))
Call add
Result 5
5

wrapper замыкает func. Декоратор возвращает wrapper вместо исходной функции.

Как избежать позднего связывания в замыканиях внутри цикла?

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

funcs = []
for i in range(3):
    funcs.append(lambda: i)
print([f() for f in funcs])

funcs2 = []
for i in range(3):
    funcs2.append(lambda i=i: i)
print([f() for f in funcs2])
[2, 2, 2]
[0, 1, 2]

Аргумент i=i сохраняет текущее значение в момент создания lambda. Это классическая ошибка позднего связывания.

Как организовать рекурсию через вложенную функцию?

Вложенная функция может вызывать себя по имени или через параметр. Для самовызова имя ищется в enclosing scope.

def factorial(n):
    def fact(k):
        if k <= 1:
            return 1
        return k * fact(k - 1)
    return fact(n)

print(factorial(5))
120

Внешняя функция служит оберткой, а вся рекурсивная логика скрыта внутри.

Как использовать вложенную функцию для мемоизации?

Кэш можно хранить во внешней функции, а вложенная функция будет обновлять его через nonlocal или через изменяемый объект.

def memo_fib():
    cache = {}
    def fib(n):
        if n in cache:
            return cache[n]
        if n < 2:
            cache[n] = n
        else:
            cache[n] = fib(n - 1) + fib(n - 2)
        return cache[n]
    return fib

fib = memo_fib()
print(fib(10))
print(fib(20))
55
6765

Как проверить область видимости вложенной функции?

Атрибут __closure__ показывает ячейки, связанные с замыканием. Атрибут __code__.co_freevars перечисляет свободные переменные.

def outer(a):
    b = 2
    def inner(c):
        return a + b + c
    return inner

f = outer(1)
print(f.__code__.co_freevars)
print(f.__closure__)
print(f(3))
('a', 'b')
(, )
6

Значения ячеек можно получить через cell_contents, но для прикладного кода обычно достаточно понимания freevars.

Типичная ошибка: UnboundLocalError при попытке изменить внешнюю переменную без nonlocal.

def outer():
    x = 1
    def inner():
        x += 1
        return x
    return inner()

# print(outer())  # UnboundLocalError

Решение: объявить nonlocal x внутри inner. Если переменная находится в глобальной области, применяется global.

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

Ограничение pickle: локальные функции и lambda нельзя сериализовать стандартным pickle, если они не доступны по глобальному имени. Для передачи между процессами часто применяются функции верхнего уровня.

Расширенные примеры вложенных функций Python

Фабрика валидаторов с сообщением об ошибке

Внешняя функция создает валидатор для конкретного поля. Внутренняя функция замыкает min_len и field.

Пример
def make_length_validator(field, min_len):
    def validate(value):
        if len(value) < min_len:
            return f'{field}: минимум {min_len} символов'
        return None
    return validate

validate_name = make_length_validator('name', 3)
validate_code = make_length_validator('code', 5)
print(validate_name('ab'))
print(validate_code('abc'))
print(validate_name('alice'))
name: минимум 3 символов
code: минимум 5 символов
None

Каждый валидатор хранит собственные field и min_len. Такой прием полезен в формах и API.

Декоратор с параметрами через вложенные функции

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

Пример
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print('Hello', name)
    return name

print(greet('Anna'))
Hello Anna
Hello Anna
Hello Anna
Anna

Три уровня вложенности типичны для декораторов с аргументами.

Аккумулятор с несколькими действиями и nonlocal

Внутренние функции изменяют общее состояние через nonlocal.

Пример
def make_account(balance=0):
    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance
    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            return 'Недостаточно средств'
        balance -= amount
        return balance
    def get_balance():
        return balance
    return deposit, withdraw, get_balance

dep, wd, get = make_account(100)
print(dep(50))
print(wd(30))
print(get())
150
120
120

Все три функции замыкают одну ячейку balance.

Вложенная функция как ключ сортировки

Локальная функция может ссылаться на внешние параметры сортировки.

Пример
def sort_by_weight(items, weights):
    def key(item):
        return weights.get(item, 0)
    return sorted(items, key=key)

items = ['b', 'a', 'c']
weights = {'a': 10, 'b': 5, 'c': 1}
print(sort_by_weight(items, weights))
['c', 'b', 'a']

key замыкает weights и применяется только внутри sort_by_weight.

Рекурсивный поиск во вложенных словарях

Внутренняя функция рекурсивно обходит структуру и накапливает путь.

Пример
def find_key(data, target):
    def walk(node, path):
        if isinstance(node, dict):
            for k, v in node.items():
                new_path = path + [k]
                if k == target:
                    return new_path
                found = walk(v, new_path)
                if found:
                    return found
        elif isinstance(node, list):
            for index, item in enumerate(node):
                found = walk(item, path + [index])
                if found:
                    return found
        return None
    return walk(data, [])

data = {'a': {'b': [{'c': 1}]}}
print(find_key(data, 'c'))
['a', 'b', 0, 'c']

Внешняя функция задает начальный путь, а walk скрывает детали обхода.

Генератор, создаваемый вложенной функцией

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

Пример
def make_range_generator(start, stop):
    def gen():
        current = start
        while current < stop:
            yield current
            current += 1
    return gen

gen = make_range_generator(2, 5)
print(list(gen()))
[2, 3, 4]

start и stop остаются доступными во время итерации.

Замыкание для обработки событий

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

Пример
def make_handlers(event_ids):
    handlers = []
    for event_id in event_ids:
        def handler(event_id=event_id):
            return f'Обработчик {event_id}'
        handlers.append(handler)
    return handlers

for h in make_handlers([10, 20, 30]):
    print(h())
Обработчик 10
Обработчик 20
Обработчик 30

Аргумент по умолчанию фиксирует текущее значение event_id.

Кэширование с ручным замыканием и подсчетом вызовов

Внешняя функция хранит cache и calls. Внутренняя функция обновляет их.

Пример
def memo_square():
    cache = {}
    calls = 0
    def square(n):
        nonlocal calls
        calls += 1
        if n not in cache:
            cache[n] = n * n
        return cache[n]
    def stats():
        return calls, len(cache)
    return square, stats

square, stats = memo_square()
print(square(4))
print(square(4))
print(square(5))
print(stats())
16
16
25
(3, 2)

Функция stats показывает, что вычисление square выполнялось три раза, а уникальных значений два.

Вложенная функция в методе класса

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

Пример
class Report:
    def __init__(self, rows):
        self.rows = rows
    def total(self):
        def to_number(value):
            return float(value.replace(',', '.'))
        return sum(to_number(row['amount']) for row in self.rows)

report = Report([{'amount': '1,5'}, {'amount': '2,25'}])
print(report.total())
3.75

to_number нужна только внутри total и не засоряет пространство имен класса.

Локальные функции (вложенные) в Python - comments

En
локальная функция python (python)