Share This
Связаться со мной
Крути в низ
Categories
//🐍 Как в Python применяются вложенные функции

🐍 Как в Python применяются вложенные функции

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

kak v python primenjajutsja vlozhennye funkcii d7780f5 - 🐍 Как в Python применяются вложенные функции

Публикация представляет собой незначительно сокращенный перевод статьи Леоданиса Посо Рамоса Python Inner Functions: What Are They Good For? Материал также адаптирован в виде Jupyter-блокнота, так что код можно запустить в интерактивном режиме в Colab.

Вложенные (или внутренние, англ. inner, nested) функции – это функции, которые мы определяем внутри других функций. В Python такая функция имеет прямой доступ к переменным и именам, определенным во включающей её функции. Вложенные функции имеют множество применений, в первую очередь для создания замыканий и декораторов.

В этом руководстве мы…

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

Создание вложенных функций в Python

Начнем с примера кода, содержащего вложенную функцию:

         def outer_func():     def inner_func():         print("Hello, World!")     inner_func()         
         >>> outer_func() Hello, World!     

В этом коде мы определяем inner_func() внутри outer_func() для вывода на экран строки Hello, World!. Для этого мы вызываем inner_func() в последней строке outer_func().

Основная особенность внутренних функций — их способность обращаться к переменным и объектам из включающей («внешней») функции. Включающая функция предоставляет пространство имен, доступное вложенной в нее функции:

         def outer_func(who):     def inner_func():         print(f"Hello, {who}")     inner_func()     
         >>> outer_func("World!") Hello, World!     

Теперь мы можем передать строку в качестве аргумента функции external_func(), и inner_func() будет обращаться к этому аргументу через имя who. Это имя определяется в локальной области видимости outer_func(). Имена, которые мы определяем в локальной области внешней функции, определяются как nonlocal. Они нелокальны с точки зрения inner_func().

Ещё один пример более сложной вложенной функции:

         def factorial(number):     # Валидация входного значения     if not isinstance(number, int):         raise TypeError("Число должно быть целым.")     if number < 0:         raise ValueError("Число должно быть неотрицательным.")     # Расчет факториала     def inner_factorial(number):         if number <= 1:             return 1         return number * inner_factorial(number - 1)     return inner_factorial(number)     
         >>> factorial(4) 24     

В функции factorial() мы сначала проверяем входные данные, чтобы убедиться, что пользователь предоставляет неотрицательное целое число. Затем мы определяем рекурсивную внутреннюю функцию с именем inner_factorial(), выполняющую вычисление факториала. На последнем шаге вызывается inner_factorial() и выполняется соответствующий расчет.

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

Основы применения вложенных функций в Python

Инкапсуляция

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

Начнем с поясняющего примера:

         def increment(number):     def inner_increment():         return number + 1     return inner_increment()          
         >>> print(increment(10)) 11 # Вызовем вложенную функцию inner_increment() >>> print(inner_increment()) NameError: name 'inner_increment' is not defined     

В этом примере у нас нет прямого доступа к inner_increment(). Попытавшись обратиться к вложенной функции, мы получаем NameError. Функция increment() полностью скрывает функцию inner_increment(), предотвращая доступ из глобальной области.

Создание внутренних вспомогательных функций

Иногда нам нужна функция, выполняющая один и тот же фрагмент кода в нескольких местах своего тела. Например, мы хотим написать функцию для обработки файла CSV, содержащего информацию о точках доступа Wi-Fi в Нью-Йорке. Чтобы узнать общее количество точек доступа, а также информацию о компании, которая их предоставляет, мы создали следующий скрипт:

hotspots.py

         import csv from collections import Counter  def process_hotspots(file):     def most_common_provider(file_obj):         hotspots = []         with file_obj as csv_file:             content = csv.DictReader(csv_file)              for row in content:                 hotspots.append(row["Provider"])          counter = Counter(hotspots)         print(             f"В Нью-Йорке {len(hotspots)} точек Wi-Fi.n"             f"{counter.most_common(1)[0][1]} из них предоставляет"             f"{counter.most_common(1)[0][0]}."         )      if isinstance(file, str):         # Получаем путь к файлу         file_obj = open(file, "r")         most_common_provider(file_obj)     else:         # Забираем объект файла         most_common_provider(file)     

Здесь process_hotspots() принимает аргумент file и проверяет, является ли файл строковым путем к физическому файлу или файловым объектом. Затем функция вызывает вспомогательную внутреннюю функцию most_common_provider(), которая принимает файловый объект и выполняет следующие операции:

  1. Считывает содержимое файла в генератор, который создает словари с помощью csv.DictReader.
  2. Составляет список провайдеров Wi-Fi.
  3. Подсчитывает количество точек доступа Wi-Fi для каждого поставщика с помощью объекта collections.Counter.
  4. Печатает сообщение с полученной информацией.

Запустив функцию, мы получим следующий результат:

          >>> from hotspots import process_hotspots  >>> file_obj = open("./NYC_Wi-Fi_Hotspot_Locations.csv", "r") >>> process_hotspots(file_obj) В Нью-Йорке 3319 точек Wi-Fi. 1868 из них предоставляет LinkNYC - Citybridge.  >>> process_hotspots("./NYC_Wi-Fi_Hotspot_Locations.csv") В Нью-Йорке 3319 точек Wi-Fi. 1868 из них предоставляет LinkNYC - Citybridge.     

Независимо от того, вызываем ли мы process_hotspots() со строковым путем к файлу или с файловым объектом, вы получите один и тот же результат.

Использование внутренних и приватных вспомогательных функций

Обычно мы создаем вспомогательные внутренние функции, такие как most_common_provider(), когда хотим обеспечить инкапсуляцию либо если не собираемся вызывать их где-либо еще, кроме включающей функции.

Хотя написание внутренних вспомогательных функций дает желаемый результат, обычно лучше вынести их, как функции верхнего уровня. В этом случае можно использовать префикс из знака подчеркивания (_) в имени функции, чтобы указать, что она является приватной для текущего модуля или класса. Это позволит получить доступ к вспомогательным функциям из любого места в текущем модуле или классе и повторно использовать их по мере необходимости.

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

Сохранение состояния с помощью вложенных функций: замыкания в Python

Функции Python в своих правах равны любым другим объектам, таким как числа, строки, списки, кортежи, модули и т. д. То есть их можно динамически создавать или уничтожать, сохранять в структурах данных, передавать в качестве аргументов другим функциям, использовать как возвращаемые значения.

В Python также можно создавать функции высшего порядка, которые принимают и возвращают другие функции.

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

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

Чтобы определить замыкание, нужно выполнить три шага:

  1. Создать вложенную функцию.
  2. Сослаться на переменные из включающей функции.
  3. Вернуть вложенную функцию.

Сейчас разберемся на примерах.

Сохранение состояния в замыкании

Итак, замыкание заставляет вложенную функцию при вызове сохранять состояние своего окружения. То есть замыкание – это не только сама внутренняя функция, но и окружающая среда.

Рассмотрим следующий пример:

powers.py

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

Здесь мы определяем функцию generate_power(), которая представляет собой фабрику для создания замыканий. То есть эта функция при каждом вызове создает и возвращает новую функцию-замыкание. В следующей строке определяется функция power(), которая является внутренней функцией, и принимает единственный аргумент base и возвращает результат выражения base ** exponent. Последняя строка возвращает power как функциональный объект, не вызывая его.

Откуда power() получает значение показателя степени esponent? Вот где в игру вступает замыкание. В этом примере power() получает значение экспоненты из внешней функции generate_power(). Вот что делает Python, когда мы вызываем generate_power():

  1. Определяет новый экземпляр power(), который принимает аргумент base.
  2. Делает «снимок» окружения power(). Он включает exponent с текущим значением.
  3. Возвращает power() вместе с состоянием.

Таким образом, когда мы вызываем экземпляр power(), возвращаемый функцией generate_power(), мы видим, что функция запоминает значение степени exponent:

         >>> raise_two = generate_power(2) >>> raise_three = generate_power(3) >>> raise_two(4) 16 >>> raise_two(5) 25 >>> raise_three(4) 64 >>> raise_three(5) 125      

Обратите внимание, что оба замыкания запоминают соответствующий показатель степени между вызовами. В этих примерах raise_two() запоминает, что exponent = 2, а rise_three() запоминает, что exponent = 3.

Рассмотрим другой пример:

         def has_permission(page):     def permission(username):         if username.lower() == "admin":             return f"'{username}' имеет доступ к {page}."         else:             return f"'{username}' не имеет доступа к {page}."     return permission  check_admin_page_permision = has_permission("Admin Page")     
         >>> print(check_admin_page_permision("admin")) 'admin' имеет доступ к Admin Page. >>> print(check_admin_page_permision("john")) 'john' не имеет доступа к Admin Page.     

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

Обычно замыкания не изменяют состояние, которое они получили «при рождении», как было показано в приведенных выше примерах. Но можно создавать и динамические замыкания, используя изменяемые объекты – словари, множества или списки.

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

         def mean():     sample = []     def inner_mean(number):         sample.append(number)         return sum(sample) / len(sample)     return inner_mean  sample_mean = mean()     
         >>> sample_mean(100) 100.0 >>> sample_mean(105) 102.5 >>> sample_mean(101) 102.0 >>> sample_mean(98) 101.0      

Замыкание, присвоенное sample_mean, сохраняет состояние выборки между вызовами. Хотя мы определяем список sample внутри mean(), он также доступен и в замыкании.

Изменение состояния замыкания

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

         def make_point(x, y):     def point():         print(f"Point({x}, {y})")     def get_x():         return x     def get_y():         return y     def set_x(value):         nonlocal x         x = value     def set_y(value):         nonlocal y         y = value     # Добавляем геттеры и сеттеры     point.get_x = get_x     point.set_x = set_x     point.get_y = get_y     point.set_y = set_y     return point  point = make_point(1, 2)     
         >>> point.get_x() 1 >>> point.get_y() 2 >>> point() Point(1, 2) >>> point.set_x(42) >>> point.set_y(7) >>> point() Point(42, 7)     

Здесь make_point() возвращает замыкание, представляющее объект point. К этому объекту прикреплены функции, которые мы можем использовать для получения доступа к чтению и записи переменных x и y.

Такая фабрика может работать даже быстрее, чем эквивалентный класс, но подход не предоставляет наследование, дескрипторы и прочие возможности классов Python.

Изменение поведения с помощью вложенных функций: декораторы

Декораторы Python – еще один популярный и удобный вариант использования внутренних функций, особенно для замыканий. Декораторы – это функции высшего порядка, которые принимают в качестве аргумента вызываемый объект (функцию, метод, класс) и возвращают другой вызываемый объект.

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

Обычно декораторы применяются для динамического добавления свойств к существующему вызываемому объекту и прозрачного расширения его поведения, не затрагивая и не изменяя вызываемого объекта. Функцию-декоратор можно применить к любому вызываемому объекту. Для этого в предваряющей его строке ставится символ @ и имя декоратора:

         @decorator def decorated_func():     # Function body...     pass     

Этот синтаксис заставляет decorator() автоматически принимать decorator_func() в качестве аргумента и обрабатывать его в своем теле. Эта операция является сокращением для инструкции следующего вида:

         decorated_func = decorator(decorated_func)     

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

         def add_messages(func):     def _add_messages():         print("Это мой первый декоратор.")         func()         print("Пока!")     return _add_messages  @add_messages def greet():     print("Привет, мир!")      greet()     
         Это мой первый декоратор. Привет, мир! Пока!     

В этом примере мы используем @add_messages для декорирования функции greet(). В результате функция приобретает новые функциональные возможности. Теперь, когда мы вызываем greet(), вместо того, чтобы просто напечатать Привет, мир!, она выводит два дополнительных сообщения.

Простейшей практикой отладки кода на Python является вставка вызовов print() для проверки значений переменных. Однако добавляя и удаляя вызовы print() мы рискуем забыть о некоторых из них. Чтобы предотвратить эту ситуацию, мы можем написать следующий декоратор:

         def debug(func):     def _debug(*args, **kwargs):         result = func(*args, **kwargs)         print(             f"{func.__name__}(args: {args}, kwargs: {kwargs}) -> {result}"         )         return result     return _debug   @debug def add(a, b):     return a + b     
         >>> add(5, 6) add(args: (5, 6), kwargs: {}) -> 11 11     

В этом примере функция-декоратор debug () печатает имя декорируемой функции, текущие значения каждого аргумента и возвращаемый результат. Такой декоратор можно использовать для простейшей отладки функций. Как только мы получаем желаемый результат, достаточно удалить вызов декоратора @debug, и отлаженная функция будет работать как обычно.

Приведем последний пример и заново реализуем generate_power() в виде функции-декоратора:

         def generate_power(exponent):     def power(func):         def inner_power(*args):             base = func(*args)             return base ** exponent         return inner_power     return power   @generate_power(2) def raise_two(n):     return n   @generate_power(3) def raise_three(n):     return n      
         >>> raise_two(7) 49 >>> raise_three(5) 125     

Эта версия generate_power() дает те же результаты, что и в исходной реализации. В этом случае мы используем для запоминания показателя степени и замыкание, и декоратор, который возвращает измененную версию функции func().

Здесь декоратор должен принимать аргумент (показатель степени), поэтому нам потребовалось два уровня вложенность. Первый уровень представлен функцией power(), которая принимает в качестве аргумента декорируемую функцию. Второй уровень представлен функцией inner_power(), которая упаковывает показатель степени в args, выполняет окончательный расчет и возвращает результат.

Заключение

Итак, в Python вложенные функции имеют прямой доступ к переменным и именам, которые вы определяете во включающей функции. Это предоставляет механизм для инкапсуляции функций, создания вспомогательных решений, реализации замыканий и декораторов.

  • 17 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close