- Python closures
- Python closures
- Python simple closure example
- Python closure with nonlocal keyword
- Python closures vs classes
- Author
- Избегайте рекурсии в Python: вспомните о замыкании
- Что такое замыкание в Python?
- Доступ к внешним переменным из внутренней функции
- Фибоначчи с помощью замыкания
- Сравниваем производительность
- Как ещё применять замыкание?
- Что в итоге?
Python closures
Python closures tutorial shows how to use closure functions in Python.
Python functions are first-class citizens. This means that functions have equal status with other objects in Python. Functions can be assigned to variables, stored in collections, created and deleted dynamically, or passed as arguments.
A nested function, also called an inner function, is a function defined inside another function.
#!/usr/bin/python def main(): def build_message(name): msg = f'Hello ' return msg name = input("Enter your name: ") msg = build_message(name) print(msg) if __name__ == "__main__": main()
The build_message is a nested function. It is defined and invoked inside its outer main function.
Python closures
- it is a nested function
- it has access to a free variable in outer scope
- it is returned from the enclosing function
A free variable is a variable that is not bound in the local scope. In order for closures to work with immutable variables such as numbers and strings, we have to use the nonlocal keyword.
Python closures help avoiding the usage of global values and provide some form of data hiding. They are used in Python decorators.
Python simple closure example
The following is a simple example of a Python closure.
#!/usr/bin/python def make_printer(msg): msg = "hi there" def printer(): print(msg) return printer myprinter = make_printer("Hello there") myprinter() myprinter() myprinter()
In the example, we have a make_printer function, which creates and returns a function. The nested printer function is the closure.
myprinter = make_printer("Hello there")
The make_printer function returns a printer function and assigns it to the myprinter variable. At this moment, it has finished its execution. However, the printer closure still has access to the msg variable.
$ ./simple_closure.py hi there hi there hi there
Python closure with nonlocal keyword
The nonlocal keyword allows us to modify a variable with immutable type in the outer function scope.
#!/usr/bin/python def make_counter(): count = 0 def inner(): nonlocal count count += 1 return count return inner counter = make_counter() c = counter() print(c) c = counter() print(c) c = counter() print(c)
The example creates a counter function.
def make_counter(): count = 0 def inner(): nonlocal count count += 1 return count return inner
By using the nonlocal keyword, the count variable becomes a free variable. Now we can modify it.
Python closures vs classes
Python closures can be an alternate solution to small classes.
#!/usr/bin/python class Summer(): def __init__(self): self.data = [] def __call__(self, val): self.data.append(val) _sum = sum(self.data) return _sum summer = Summer() s = summer(1) print(s) s = summer(2) print(s) s = summer(3) print(s) s = summer(4) print(s)
We have a Summer class, which sums values passed to the object.
def __init__(self): self.data = []
The data is kept in the object attribute and is created in the constructor.
def __call__(self, val): self.data.append(val) _sum = sum(self.data) return _sum
Each time the instance is called, the value is appended and the sum is calculated and returned.
The following is an alternate solution with Python closure.
#!/usr/bin/python def make_summer(): data = [] def summer(val): data.append(val) _sum = sum(data) return _sum return summer summer = make_summer() s = summer(1) print(s) s = summer(2) print(s) s = summer(3) print(s) s = summer(4) print(s)
We have the same functionality with a Python closure.
def make_summer(): data = [] def summer(val): data.append(val) _sum = sum(data) return _sum return summer
Because the data is a list which is mutable, we do not have to use the nonlocal keyword.
In this tutorial we have worked with Python closures.
Author
My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.
Избегайте рекурсии в Python: вспомните о замыкании
Раньше я был программистом, которому очень нравились рекурсивные функции, просто потому, что это очень круто, с их помощью можно продемонстрировать свои навыки программирования и интеллект. Однако в большинстве случаев рекурсивные функции имеют высокую сложность, поэтому нам следует избегать их использования.
Одно из решений намного лучше – по возможности задействовать динамическое планирование: вероятно, оно – лучший способ решать задачи, которые можно разделить на подзадачи. Одна из моих предыдущих статей демонстрирует мощь динамического планирования.
Здесь я представлю ещё одну технику Python, которую можно использовать как альтернативу рекурсивной функции. Эта техника не превосходит динамическое планирование, но она гораздо проще, с точки зрения мышления. Другими словами, иногда сложно заставить динамическое планирование работать из-за абстракции идей, тогда как проще воспользоваться замыканием.
Что такое замыкание в Python?
Прежде всего позвольте мне на простом примере продемонстрировать, что такое замыкание в Python. Посмотрите на функцию ниже:
def outer(): x = 1 def inner(): print(f'x in outer function: ') return inner
Функция outer определяется с функцией inner внутри, а функция outer возвращает функцию inner ; именно она – возвращаемое значение outer .
Здесь вложенная функция – это и есть замыкание. Если проверить возвращаемое значение внешней функции, окажется, что оно является функцией.
Что делает замыкание? Поскольку оно вернуло функцию, мы, конечно, можем запустить её.
Видно, что внутренняя функция может обращаться к переменным, определённым во внешней функции. Обычно замыкание не применяется так, как показано выше, потому что это некрасиво. Мы обычно хотим определить другую функцию, чтобы удерживать функцию, которая возвращается замыканием.
Следовательно, мы также можем сказать, что в замыкании Python мы определили функцию, которая определяет функции.
Доступ к внешним переменным из внутренней функции
Как тогда мы можем использовать замыкание, чтобы заменить им рекурсию? Не торопитесь. Давайте посмотрим на другую проблему, связанную с доступом к внешним переменным из внутренней функции.
def outer(): x = 1 def inner(): print(f'x in outer function (before modifying): ') x += 1 print(f'x in outer function (after modifying): ') return inner
В замыкании выше мы хотим добавить 1 к внешней переменной x во внутренней функции. Это работает неочевидным образом.
По умолчанию вы не сможете получить доступ к внешней переменной из внутренней функции. Однако так же, как мы определяем глобальную переменную в Python, мы можем сообщить внутренней функции замыкания, что переменная не должна рассматриваться как «локальная», это делается с помощью ключевого слова nonlocal .
def outer(): x = 1 def inner(): nonlocal x print(f'x in outer function (before modifying): ') x += 1 print(f'x in outer function (after modifying): ') return inner
Теперь предположим, что мы хотим пять раз добавить единицу к переменной x. Можно просто написать цикл for .
f = outer() for i in range(5): print(f'Run ') f() print('\n')
Фибоначчи с помощью замыкания
Фибоначчи обычно используется как пример рекурсивных функций, как рекурсивный «Hello, World!». Напомню, о чём речь. Последовательность Фибоначчи – это ряд чисел, каждое следующее число – это сумма двух чисел перед ним. Первые два числа, X₀ и X₁, особенные. Это 0 и 1. Значит, X₂, как упоминалось выше, – это сумма X₀ и X₁. И так далее [сократил].
Рекурсивная функция требует, чтобы мы мыслили в обратном порядке, от «текущей ситуации» к «предыдущей ситуации» и, в конечном счёте, об условии завершения рекурсии. При помощи замыкания можно думать о проблеме более естественным образом. В коде ниже показана реализация Фибоначчи через замыкание:
def fib(): x1 = 0 x2 = 1 def get_next_number(): nonlocal x1, x2 x3 = x1 + x2 x1, x2 = x2, x3 return x3 return get_next_number
Мы знаем, что Фибоначчи начинается с двух специальных чисел X₀ = 0 и X₁ = 1, поэтому просто определяем их во внешней функции. Затем внутренняя функция get_next_number просто возвращает сумму двух чисел, полученных от внешней функции. Кроме того, не забудьте обновить X₀ и X₁ с помощью X₁ и X₂. Код можно упростить:
. x3 = x1 + x2 x1, x2 = x2, x3 return x3
x0, x1 = x1, x0 + x1 return x1
Этот код сначала обновляет две переменные, а затем возвращает вторую, что эквивалентно коду выше. Затем мы можем использовать это замыкание, чтобы вычислить числа Фибоначчи. Например, вот последовательность Фибоначчи до двадцатого.
fibonacci = fib() for i in range(2, 21): num = fibonacci() print(f'The th Fibonacci number is ')
Сравниваем производительность
А как насчёт производительности? Давайте сравним! Сначала реализуем функцию Фибоначчи рекурсивно:
def fib_recursion(n): if n == 0: return 0 elif n == 1: return 1 else: return fib_recursion(n-1) + fib_recursion(n-2)
Функцию можно проверить: вывести 20-е число последовательности Фибоначчи.
Теперь напишем то же самое с замыканием.
def fib_closure(n): f = fib() for i in range(2, n+1): num = f() return num
2,79 мс против 2,75 мкс. Замыкание в 1000 раз быстрее рекурсии! Интуитивно понятно: причина в том, что все временные значения для каждого уровня рекурсии хранятся в памяти отдельно, тогда как замыкание каждый раз обновляет одни и те же переменные. Кроме того, существует ограничение глубины рекурсии. В случае замыкания, поскольку это в основном цикл for, никаких ограничений нет. Вот пример получения 1000-го числа Фибоначчи.
Это действительно огромное число, но метод замыкания может завершить вычисление примерно за 100 мкс, тогда как рекурсия сталкивается со своими ограничениями.
Как ещё применять замыкание?
Замыкания Python очень полезны не только как замена рекурсивных функций. В некоторых случаях оно также может заменить классы Python на решение изящнее, особенно когда в классе не слишком много атрибутов и методов. Предположим, у нас есть словарь студентов с их экзаменационными отметками.
Хочется иметь несколько функций, которые помогут нам фильтровать студентов по оценкам, помещать их в разные классы. Однако со временем критерии могут измениться. В этом случае можно определить замыкание Python, вот так:
def make_student_classifier(lower_bound, upper_bound): def classify_student(exam_dict): return return classify_student
Замыкание определяет функцию, которая, в свою очередь, определяет другие функции на основе динамически передаваемых параметров. Мы передадим нижнюю и верхнюю границы класса оценки, и замыкание вернёт нам функцию, которая отфильтрует студентов.
grade_A = make_student_classifier(80, 100) grade_B = make_student_classifier(70, 80) grade_C = make_student_classifier(50, 70) grade_D = make_student_classifier(0, 50)
Код выше даёт нам 4 функции, которые классифицируют учащегося по соответствующим классам на основе указанных нами границ. Обратите внимание, что мы можем изменить границу в любой момент, чтобы выполнить другую функцию или переопределить текущие функции оценки. Давайте теперь проверим функции.
Выглядит очень аккуратно! Но имейте в виду, что в более сложных случаях всё равно нужно определять классы.
Что в итоге?
В этой статье я представил технику, называемую замыканием, в Python. Её можно применить, чтобы переписать рекурсивные функций и в большинстве случаев значительно превзойти их.
В самом деле, замыкание может оказаться не лучшим решением некоторых проблем, с точки зрения производительности, особенно когда применимо динамическое планирование. Однако намного проще работать с ним в плане мышления. Иногда динамическое планирование излишне: например, когда не так уж важна производительность, может быть достаточно замыкания.
Замыкание также может использоваться, чтобы заменить некоторые юзкейсы, для удовлетворения которым мы можем захотеть реализовать класс. В таких случаях замыкание выглядит аккуратнее и элегантнее.
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение: