Замыкания в Python

Курс по Python: https://stepik.org/course/100707

Смотреть материал на YouTube | RuTube

На этом занятии затронем новую тему – замыкания. Это один из любимых вопросов на собеседовании в области программирования – рассказать, что такое замыкания. И сейчас вы узнаете подробный ответ на него.

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

def say_name(name):
    def say_goodbye():
        print("Don't say me goodbye, " + name + "!")
 
    say_goodbye()

А, затем вызвать:

say_name("Sergey")

Мы здесь сначала вызываем внешнюю функцию, затем, в ней формируется вложенная функция say_goodbye() и вызывается. В результате, в консоли видим сообщение «Don't say me goodbye, Sergey!».

Я, думаю, здесь вам все должно быть понятно. А теперь сделаем, следующее. Вместо вызова внутренней функции возвратим ссылку на нее с помощью оператора return:

def say_name(name):
    def say_goodbye():
        print("Don't say me goodbye, " + name + "!")
 
    return say_goodbye

Конечно, после запуска программы мы не увидим никакого сообщения, так как внутренняя функция нигде не вызывается. Исправим это. Сохраним ссылку на функцию say_goodbye() в переменной f:

f = say_name("Sergey")

А, затем, вызовем ее:

f()

Мы снова видим то же самое сообщение. Вам не кажется здесь ничего странным? Например, откуда функция say_goodbye() берет значение переменной name? Ведь внешняя функция say_name() выполнилась и завершилась, а значит, все ее локальные переменные вроде как тоже должны были бы исчезнуть? Но нет, мы обращаемся к переменной name и успешно получаем ее значение! Почему? Давайте разберемся.

Дело в том, что когда у нас имеется глобальная ссылка f на внутреннее, локальное окружение функции say_goodbye(), то это окружение продолжает существовать, оно не удаляется автоматически сборщиком мусора, именно из-за этой глобальной ссылки на него. А вместе с ним, продолжают существовать и все внешние локальные окружения, в данном случае – окружение функции say_name(), потому что также существует неявная, скрытая ссылка на него из внутреннего окружения. Такие ссылки формируются автоматически и позволяют, в частности, обращаться к переменным, объявленным в этих внешних окружениях. Именно поэтому функция print() в say_goodbye() имеет доступ к переменной name и эта переменная продолжает существовать, пока существует окружение say_goodbye, а значит и окружение say_name.

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

f = say_name("Sergey")
f2 = say_name("Python")
f()
f2()

Где может пригодиться такой функционал? Например, можно создать функцию-счетчик, которая бы увеличивала значение локальной переменной на единицу при каждом запуске:

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

Обратите внимание, мы здесь используем ключевое слово nonlocal, чтобы переменная start изменялась во внешней локальной области, а не создавалась бы в текущей, локальной. Без этой строчки возникнет ошибка, из-за неопределенности: мы берем текущее значение start извне, а потом создавали бы переменную с тем же именем внутри области step. Так делать нельзя. И строчка nonlocal start четко указывает брать переменную start из внешней локальной области, а не создавать в текущей.

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

c1 = counter(10)
c2 = counter()
print(c1(), c2())
print(c1(), c2())
print(c1(), c2())

У нас, действительно, оба счетчика отработают независимо друг от друга.

И приведу еще один пример с замыканиями. Предположим, мы хотим сделать функцию, которая бы удаляла ненужные символы в начале и конце строки. Через замыкание это можно реализовать, следующим образом:

def strip_string(strip_chars=" "):
    def do_strip(string):
        return string.strip(strip_chars)
 
    return do_strip

Обратите внимание, вложенная функция тоже имеет параметр – строку, у которой будут удаляться ненужные символы. Далее, создадим два объекта с разным списком удаляемых символов:

strip1 = strip_string()
strip2 = strip_string(" !?,.;")

И вызовем вложенную функцию с одним аргументом:

print(strip1(" hello python!.. "))
print(strip2(" hello python!.. "))

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

Вот, что из себя представляют замыкания и вот так они работают. Для закрепления этого материала пройдите практические задания, а затем, на покорение нового материала – к новым урокам!

Курс по Python: https://stepik.org/course/100707

Видео по теме