Замыкания. Карринг. Декораторы.

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

  • Мы можем определить функцию внутри другой функции

def outer(a, b):
    def inner(c, d):
        return c + d
    return inner(a, b)
outer(4, 7)
# 11
  • Внутренние функции могут быть полезны при выполнении некоторых сложных задач более одного раза внутри другой функции. Это позволит избежать дублирования кода

  • Рассмотрим пример работы со строкой, когда внутренняя функция добавляет текст в свой аргумент:

def hello(saying):
    def inner(name):
        return f"Hello, {name}!"
    return inner(saying)

hello("Igor")
# 'Hello, Igor!'

Практика:

  • Напишите функцию, которая принимает аргумент — число. В теле функции определите внутреннюю функцию, которая принимает строку и число. Значение строки по умолчанию — "Python". Число — это количество, сколько раз должна быть продублирована строка. Верните результат работы внутренней функции во внешней функции. Протестируйте программу.

Замыкания

  • Внутренняя функция может действовать как замыкание, если возвращает функцию

  • Замыкание — это функция, которая находится внутри другой функции и ссылается на переменные, объявленные в теле внешней функции

def talk(n, name):
    def hello():
        return f'Привет {name}.'
    def goodbye():
        return f'Пока {name}.'
    if n > 0:
        return hello
    else:
        return goodbye

talk(1, 'Андрей')()
# 'Привет Андрей.'
def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

closure = outer_function(10)
print(closure(5))  # => 15
# Замыкание (closure) позволяет сохранить значение между вызовами и может быть использовано внутри inner_function даже после того, как outer_function уже завершила свою работу
def add_number(n):
    def inner(x):
        return x + n
    return inner

add_five = add_number(5) # add_five хранит внутреннюю функцию c сохраненной переменной n == 5
add_ten = add_number(10) # а add_ten сохранила n == 10

print(add_five(3))  # 8
print(add_ten(3))   # 13
  • Применение замыканий позволяет создавать функции с доступом к переменным, находящимся вне их области видимости и запоминать значения переменных

def password_protected(password):
    def inner():
        if password == 'secret':
            print("Access granted")
        else:
            print("Access denied")
    return inner

login = password_protected('secret')
login()  # Access granted
# таким образом функция login "запомнила" значение переменной password
  • Внутри внешней функции мы можем создавать переменные и изменять их во внутренних функциях

  • Для этого используется ключевое слово nonlocal

  • Пример использования замыкания и ключевого слова nonlocal для генерации ID:

def id_generator():
    unique_id = 0

    def generate_id():
        nonlocal unique_id
        unique_id += 1
        return f"ID{unique_id}"

    return generate_id

get_id = id_generator()
print(get_id())  # ID1
print(get_id())  # ID2

Практика:

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

Пример работы программы:

love_python = func("любит Python")
love_python("Игорь") # Игорь любит Python
love_python("Татьяна") # Татьяна любит Python
  1. Создайте функцию для генерации паролей. Функция должна принимать символы, из которых будет генерироваться пароль (установите для этого значение по умолчанию), и длину пароля. При каждом вызове она должна возвращать новый случайный пароль. Используйте замыкание и модули random и string

Пример работы программы:

generate_8_char_password = password_generator(8)
print(generate_8_char_password())  # Например: "rT3uF9pW"
print(generate_8_char_password())  # Еще один случайный пароль

Карринг

  • Карринг (currying) — это техника в функциональном программировании, при которой функция, принимающая несколько аргументов, преобразуется в последовательность функций, каждая из которых принимает по одному аргументу.

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

# функция сложения двух чисел
def add_nums(x, y):
    return x + y


# применяем карринг
def add(x):
    def add_x(y):
        return x + y
    return add_x

# Создаем функцию для сложения на 5
add_5 = add(5)

# Теперь можем использовать add_5 как отдельную функцию
result = add_5(3)  # Результат: 8
  • Ещё один пример:

def greet(greeting):
    def greet_name(name):
        return f'{greeting}, {name}!'
    return greet_name

say_hi = greet('Привет')
say_hello = greet('Здравствуй')

print(say_hi('Анна'))  # Результат: "Привет, Анна!"
print(say_hello('Петр'))  # Результат: "Здравствуй, Петр!"
  • Карринг удобен, когда вы хотите создавать множество функций, используя один и тот же "шаблон" и частично применять аргументы.

Практика:

  • Создайте функцию умножения двух чисел. Используйте карринг

Декораторы

  • Иногда нам нужно модифицировать существующую функцию, не меняя при этом исходный код

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

def func_decorator(func):
    def wrapper(): 
        print("---------что-то делаем перед вызовом функции ------")
        res = func() 
        print("---------что-то делаем после вызова функции-------")
        return res 
    return wrapper

def some_func():
    print("Работает функция some_func!")
    
func_decorator(some_func)()
# ---------что-то делаем перед вызовом функции ------
# Работает функция some_func!
# ---------что-то делаем после вызова функции-------
  • Добавим аргументы и создадим дополнительную переменную для задекорированной функции:

def func_decorator(func):
    def wrapper(num1, num2): 
        print("---------что-то делаем перед вызовом функции ------")
        res = func(num1, num2) 
        print("---------что-то делаем после вызова функции-------")
        return res 
    return wrapper

def some_func(num1, num2):
    print(f"Работает функция some_func! Кстати числа равны {num1} и {num2}")
    
upgrade_some_func = func_decorator(some_func)
upgrade_some_func(5, 10)
  • Для того чтобы сделать декоратор универсальным, можно сразу указать все возможные аргументы (позиционные и ключевые) таким образом:

def func_decorator(func):
    def wrapper(*args, **kwargs): 
        print("---------что-то делаем перед вызовом функции ------")
        res = func(*args, **kwargs) 
        print("---------что-то делаем после вызова функции-------")
        return res 
    return wrapper
    
def some_func(num1, num2):
    print(f"Работает функция some_func! Кстати числа равны {num1} и {num2}")
    
upgrade_some_func = func_decorator(some_func)
upgrade_some_func(3, 5)
  • В Python есть "синтаксический сахар" — более удобная форма записи для декораторов:

@func_decorator # декорируем функцию some_func
def some_func(num1, num2):
    print(f"Работает функция some_func! Кстати числа равны {num1} и {num2}")

some_func(3, 5) # т.к. мы декорировали функцию перед определением, она сработает вместе с декоратором

Пример — декоратор, измеряющий время работы функций:

import time

def test_time(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        end = time.time()
        delta = end - start
        print(f"Время работы: {delta} сек")
        return res

    return wrapper

Практика:

  1. Напишите функцию конкатенации двух строк. Измерьте время её работы при помощи декоратора test_time

  2. Определите декоратор test, который выводит строку start при вызове функции и строку end, когда функция завершает свою работу. Дополнительно: добавьте вывод имени запускаемой функции (используйте метод func.__name__)

  3. Напишите декоратор, который возводит результат работы функции в квадрат. Примените его к любой функции, возвращающей число.

  4. Напишите декоратор, который будет автоматически повторять выполнение функции, если она выбрасывает исключение.

Last updated