Использование функций внутри функций в 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()) # 1Python функции внутри функции (вложенные функции в 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)) # 12525 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)) # 1258626902555 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 = 1111
Композиция полезна в функциональном стиле.
Каррирование
Преобразование функции от многих аргументов в цепочку вложенных функций.
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)) # 66 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 является вложенной (генератор).