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

Раздел: Структуры данных -> вложенность в Python

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

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

Наиболее эффективное решение: замыкания для сохранения состояния

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

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

counter1 = make_counter()
print(counter1())  # 1
print(counter1())  # 2
counter2 = make_counter()
print(counter2())  # 1

Python функции внутри функции (вложенные функции в python)

1
2
1

Каждый вызов make_counter создаёт новую переменную count, и возвращённая функция increment работает именно с ней. Ключевое слово nonlocal необходимо для изменения внешней переменной, иначе будет создана новая локальная.

Типичные ошибки:

  • Забыть nonlocal - попытка присвоения создаст локальную переменную и сломает замыкание.
  • Позднее связывание в циклах - если создать несколько замыканий внутри цикла, все они будут ссылаться на последнее значение переменной цикла. Решение - использование аргумента по умолчанию или functools.partial.

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

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

def calculate_total(items):
    def apply_tax(price):
        return price * 1.2
    total = sum(apply_tax(item) for item in items)
    return total

print(calculate_total([100, 200]))
360.0

apply_tax не доступна снаружи, что предотвращает её случайное использование.

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

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

Замыкание позволяет зафиксировать параметр.

def prefixer(prefix):
    def add_prefix(text):
        return f"{prefix} {text}"
    return add_prefix

hello_prefix = prefixer("Привет")
print(hello_prefix("Мир"))
print(hello_prefix("Питон"))
Привет Мир
Привет Питон

Каждый вызов prefixer запоминает свой prefix.

Переменная prefix не изменяется внутри замыкания - если потребуется её изменить, снова понадобится nonlocal.

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

Фабрика принимает параметр и возвращает новую функцию, поведение которой зависит от этого параметра.

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

square = power_factory(2)
cube = power_factory(3)
print(square(5))  # 25
print(cube(5))    # 125
25
125

Цель - параметризованное создание функций без дублирования кода.

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

Как добавить логирование к функции без изменения её кода?

Декоратор - это функция, принимающая другую функцию и возвращающая обёртку, обычно реализованную через вложенную функцию.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} c {args} {kwargs}")
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

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

add(3, 5)
Вызов add c (3,) {}
Результат: 8

Декораторы позволяют отделить сквозную функциональность (логирование, тайминг, проверку прав) от основной логики.

Без functools.wraps обёртка теряет имя и документацию исходной функции. Использование @functools.wraps(func) решает эту проблему.

Как кешировать результаты рекурсивной функции?

Мемоизация с помощью вложенного словаря позволяет сохранить уже вычисленные значения.

def fibonacci_memo():
    cache = {0: 0, 1: 1}
    def fib(n):
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
    return fib

fib = fibonacci_memo()
print(fib(10))  # 55
print(fib(50))  # 12586269025
55
12586269025

Замкнутый словарь cache существует всё время жизни fib.

При больших n кеш может потреблять много памяти. Ограничение размера кеша или использование functools.lru_cache - более надёжное решение.

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

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

def create_account(initial_balance=0):
    balance = initial_balance
    def get_balance():
        return balance
    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance
    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            raise ValueError("Недостаточно средств")
        balance -= amount
        return balance
    return {"get": get_balance, "deposit": deposit, "withdraw": withdraw}

acc = create_account(100)
print(acc["get"]())
acc["deposit"](50)
print(acc["get"]())
100
150

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

Отсутствие типизации и сложность расширения - для серьёзных проектов лучше использовать классы.

Расширенные примеры и нестандартные приёмы

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

Создание декоратора, который принимает параметр (например, количество повторений).

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

@repeat(3)
def say(text):
    print(text)
    return len(text)

say("Привет")
Привет
Привет
Привет

Вложенность трёх функций обеспечивает гибкость.

Композиция функций

Функция compose, объединяющая две функции в одну.

Пример
def compose(f, g):
    def composed(x):
        return f(g(x))
    return composed

def double(x):
    return x * 2

def add_one(x):
    return x + 1

double_then_add_one = compose(add_one, double)  # apply double first, then add_one
print(double_then_add_one(5))  # 5*2+1 = 11
11

Композиция полезна в функциональном стиле.

Каррирование

Преобразование функции от многих аргументов в цепочку вложенных функций.

Пример
def curry(func):
    def curried(*args):
        if len(args) >= func.__code__.co_argcount:
            return func(*args)
        def more(*more_args):
            return curried(*(args + more_args))
        return more
    return curried

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

print(add(1)(2)(3))      # 6
print(add(1, 2)(3))     # 6
print(add(1, 2, 3))     # 6
6
6
6

Каррирование упрощает частичное применение.

Кэширование с TTL через замыкание

Хранение времени последнего обновления кеша.

Пример
import time

def ttl_cache(seconds):
    cache = {}
    def decorator(func):
        def wrapper(*args):
            now = time.time()
            if args in cache:
                value, timestamp = cache[args]
                if now - timestamp < seconds:
                    return value
            result = func(*args)
            cache[args] = (result, now)
            return result
        return wrapper
    return decorator

@ttl_cache(2)
def slow_square(x):
    time.sleep(1)
    return x * x

print(slow_square(4))  # через 1 сек
print(slow_square(4))  # мгновенно из кеша
time.sleep(2)
print(slow_square(4))  # снова через 1 сек (кеш устарел)
16
16
16

Обратите внимание: время жизни кеша ограничено.

Просмотр переменных замыкания через __closure__

Пример
def outer(x):
    def inner(y):
        return x + y
    return inner

f = outer(10)
print(f.__closure__)
print(f.__closure__[0].cell_contents)  # 10
(<cell at 0x...: int object at 0x...>,)
10

Атрибут __closure__ содержит ячейки со значениями внешних переменных.

Контекстный менеджер на вложенных функциях (через генератор)

Пример
from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"Открытие ресурса {name}")
    try:
        yield name
    finally:
        print(f"Закрытие ресурса {name}")

with managed_resource("файл.txt") as res:
    print(f"Работа с {res}")
Открытие ресурса файл.txt
Работа с файл.txt
Закрытие ресурса файл.txt

Хотя здесь используется декоратор, сама функция managed_resource является вложенной (генератор).

вложенные функции в Python - comments

En
Python функции внутри функции (python)