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

В этом уроке вы узнаете, как:

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

Содержание

Создание внутренних функций Python

Функция, определенная внутри другой функции, называется внутренней функцией или вложенной функцией. В 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(). Это самый быстрый способ написать внутреннюю функцию на Python. Однако внутренние функции предоставляют множество интересных возможностей помимо того, что вы видите в этом примере.

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

>>> 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(). Имена, которые вы определяете в локальной области внешней функции, известны как нелокальные имена. Они нелокальны с точки зрения inner_func().
Вот пример того, как создать и использовать более сложную внутреннюю функцию:

>>> def factorial(number):
...     # Validate input
...     if not isinstance(number, int):
...         raise TypeError("Sorry. 'number' must be an integer.")
...     if number < 0:
...         raise ValueError("Sorry. 'number' must be zero or positive.")
...     # Calculate the factorial of number
...     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.

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

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

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

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

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

Вот пример, подчеркивающий эту концепцию:

>>> def increment(number):
...     def inner_increment():
...         return number + 1
...     return inner_increment()
...

>>> increment(10)
11

>>> # Call inner_increment()
>>> inner_increment()
Traceback (most recent call last):
  File "", line 1, in 
    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"There are {len(hotspots)} Wi-Fi hotspots in NYC.\n"
            f"{counter.most_common(1)[0][0]} has the most with "
            f"{counter.most_common(1)[0][1]}."
        )

    if isinstance(file, str):
        # Got a string-based filepath
        file_obj = open(file, "r")
        most_common_provider(file_obj)
    else:
        # Got a file object
        most_common_provider(file)

Здесь process_hotspots() принимает файл в качестве аргумента. Функция проверяет, является ли файл строковым путем к физическому файлу или файловому объекту. Затем он вызывает вспомогательную внутреннюю функцию 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)
There are 3319 Wi-Fi hotspots in NYC.
LinkNYC - Citybridge has the most with 1868.

>>> process_hotspots("./NYC_Wi-Fi_Hotspot_Locations.csv")
There are 3319 Wi-Fi hotspots in NYC.
LinkNYC - Citybridge has the most with 1868.

Независимо от того, вызываете ли вы 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

Вот что происходит в этой функции:

  • Строка 3 создает функцию generate_power (), которая является фабричной функцией закрытия. Это означает, что он создает новое закрытие при каждом вызове, а затем возвращает его вызывающей стороне.
  • Строка 4 определяет power (), которая является внутренней функцией, которая принимает единственный аргумент base,и возвращает результат выражения base ** exponent.
  • Строка 6 возвращает мощность как функциональный объект, не вызывая его.

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

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

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

>>> from powers import generate_power

>>> 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}' has access to {page}."
...         else:
...             return f"'{username}' doesn't have access to {page}."
...     return permission
...

>>> check_admin_page_permision = has_permission("Admin Page")

>>> check_admin_page_permision("admin")
"'admin' has access to Admin Page."

>>> check_admin_page_permision("john")
"'john' doesn't have access to Admin Page."

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

Вместо того, чтобы проверять, равен ли пользователь admin, вы можете запросить базу данных SQL, чтобы проверить разрешение, а затем вернуть правильное представление в зависимости от того, верны ли учетные данные.

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

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

>>> 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, сохраняет состояние выборки между последовательными вызовами. Несмотря на то, что вы определяете образец в mean(), он все еще доступен в закрытии, поэтому вы можете его изменить. В этом случае образец работает как своего рода динамическое включающее состояние.

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

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

>>> 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
...     # Attach getters and setters
...     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() возвращает замыкание, представляющее точечный объект. К этому объекту прикреплены функции получения и установки. Вы можете использовать эти функции для получения доступа для чтения и записи к переменным x и y, которые определены во включающей области и поставляются с закрытием.

Несмотря на то, что эта функция создает замыкания, которые могут работать быстрее, чем эквивалентный класс, вы должны знать, что этот метод не предоставляет основных функций, включая наследование, свойства, дескрипторы, а также класс и статические методы. Если вы хотите глубже погрузиться в эту технику, затем ознакомьтесь с SIMPLE TOOL FOR SIMULATING CLASSES USING CLOSURES AND NESTED SCOPES (PYTHON RECIPE).

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

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

Примечание. Дополнительные сведения о вызываемых объектах Python см. В разделе «Иерархия стандартных типов» в документации Python и прокрутите вниз до «Вызываемые типы».

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

Когда у вас есть функция декоратора, вы можете применить ее к любому вызываемому объекту. Для этого нужно использовать символ at (@) перед именем декоратора, а затем поместить его в отдельной строке непосредственно перед декорированным вызываемым объектом:

@decorator
def decorated_func():
    # Тело функции...
    pass

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

decorated_func = decorator(decorated_func)

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

>>> def add_messages(func):
...     def _add_messages():
...         print("This is my first decorator")
...         func()
...         print("Bye!")
...     return _add_messages
...

>>> @add_messages
... def greet():
...     print("Hello, World!")
...

>>> greet()
This is my first decorator
Hello, World!
Bye!

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

Сценарии использования декораторов Python разнообразны. Вот некоторые из них:

Обычной практикой отладки кода 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 и ваша функция будет готова к следующему шагу.

Примечание. Если вы хотите глубже понять, как * args и ** kwargs работают в Python, ознакомьтесь с Python args and kwargs: Demystified.

Вот последний пример того, как создать декоратор. На этот раз вы повторно реализуете 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
...
>>> raise_two(7)
49

>>> @generate_power(3)
... def raise_three(n):
...     return n
...
>>> raise_three(5)
125

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

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

Заключение

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

В этом уроке вы научились:

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

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

По мотивам: Python Inner Functions: What Are They Good For?

Опубликовано Вадим В. Костерин

ст. преп. кафедры ИТЭ. Автор более 130 научных и учебно-методических работ. Лауреат ВДНХ (серебряная медаль).

Оставьте комментарий

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