Узнайте больше о замыканиях

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132506{{"Узнайте больше о замыканиях"}} python/function_definition -.-> lab-132506{{"Узнайте больше о замыканиях"}} python/arguments_return -.-> lab-132506{{"Узнайте больше о замыканиях"}} python/scope -.-> lab-132506{{"Узнайте больше о замыканиях"}} python/classes_objects -.-> lab-132506{{"Узнайте больше о замыканиях"}} python/raising_exceptions -.-> lab-132506{{"Узнайте больше о замыканиях"}} end

Замыкания как структура данных

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

Давайте создадим файл с именем counter.py, чтобы продемонстрировать эту концепцию:

  1. Откройте WebIDE и создайте новый файл с именем counter.py в директории /home/labex/project. Именно здесь мы напишем код, который определяет наш счётчик на основе замыкания.

  2. Добавьте следующий код в файл:

def counter(value):
    """
    Create a counter with increment and decrement functions.

    Args:
        value: Initial value of the counter

    Returns:
        Two functions: one to increment the counter, one to decrement it
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

В этом коде мы определяем функцию с именем counter(). Эта функция принимает начальное значение value в качестве аргумента. Внутри функции counter() мы определяем две вложенные функции: incr() и decr(). Эти вложенные функции имеют общий доступ к одной и той же переменной value. Ключевое слово nonlocal используется, чтобы сообщить Python, что мы хотим изменить переменную value из внешней области видимости (функции counter()). Без ключевого слова nonlocal Python бы создал новую локальную переменную внутри вложенных функций вместо изменения переменной value из внешней области видимости.

  1. Теперь давайте создадим тестовый файл, чтобы увидеть это в действии. Создайте новый файл с именем test_counter.py со следующим содержимым:
from counter import counter

## Create a counter starting at 0
up, down = counter(0)

## Increment the counter several times
print("Incrementing the counter:")
print(up())  ## Should print 1
print(up())  ## Should print 2
print(up())  ## Should print 3

## Decrement the counter
print("\nDecrementing the counter:")
print(down())  ## Should print 2
print(down())  ## Should print 1

В этом тестовом файле мы сначала импортируем функцию counter() из файла counter.py. Затем мы создаем счётчик, начиная с значения 0, вызвав counter(0) и распаковав возвращаемые функции в up и down. Затем мы вызываем функцию up() несколько раз, чтобы увеличить счётчик и выводим результаты. После этого мы вызываем функцию down(), чтобы уменьшить счётчик и выводим результаты.

  1. Запустите тестовый файл, выполнив следующую команду в терминале:
python3 test_counter.py

Вы должны увидеть следующий вывод:

Incrementing the counter:
1
2
3

Decrementing the counter:
2
1

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

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

Замыкания как генератор кода

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

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

Теперь приступим к реализации нашей системы проверки типов.

  1. Создайте новый файл с именем typedproperty.py в директории /home/labex/project со следующим кодом:
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Create a property with type checking.

    Args:
        name: The name of the property
        expected_type: The expected type of the property value

    Returns:
        A property object that performs type checking
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

В этом коде функция typedproperty является замыканием. Она принимает два аргумента: name и expected_type. Декоратор @property используется для создания метода-геттера для свойства, который извлекает значение приватного атрибута. Декоратор @value.setter создает метод-сеттер, который проверяет, является ли устанавливаемое значение ожидаемого типа. Если нет, он вызывает исключение TypeError.

  1. Теперь создадим класс, который использует эти типизированные свойства. Создайте файл с именем stock.py со следующим кодом:
from typedproperty import typedproperty

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

В классе Stock мы используем функцию typedproperty для создания атрибутов с проверкой типов для name, shares и price. Когда мы создаем экземпляр класса Stock, проверка типов будет автоматически применена.

  1. Создадим тестовый файл, чтобы увидеть это в действии. Создайте файл с именем test_stock.py со следующим кодом:
from stock import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.shares = "hundred"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

В этом тестовом файле мы сначала создаем объект Stock с правильными типами. Затем мы пытаемся установить атрибут shares в строку, что должно вызвать исключение TypeError, так как ожидаемый тип — целое число.

  1. Запустите тестовый файл:
python3 test_stock.py

Вы должны увидеть вывод, похожий на следующий:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

Этот вывод показывает, что проверка типов работает правильно.

  1. Теперь усовершенствуем файл typedproperty.py, добавив удобные функции для общих типов. Добавьте следующий код в конец файла:
def String(name):
    """Create a string property with type checking."""
    return typedproperty(name, str)

def Integer(name):
    """Create an integer property with type checking."""
    return typedproperty(name, int)

def Float(name):
    """Create a float property with type checking."""
    return typedproperty(name, float)

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

  1. Создайте новый файл с именем stock_enhanced.py, который использует эти удобные функции:
from typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

  1. Создайте тестовый файл test_stock_enhanced.py для тестирования усовершенствованной версии:
from stock_enhanced import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.price = "490.1"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Этот тестовый файл похож на предыдущий, но он тестирует усовершенствованный класс Stock.

  1. Запустите тест:
python3 test_stock_enhanced.py

Вы должны увидеть вывод, похожий на следующий:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

На этом этапе мы продемонстрировали, как замыкания можно использовать для генерации кода. Функция typedproperty создает объекты свойств, которые выполняют проверку типов, а функции String, Integer и Float создают специализированные свойства для общих типов.

✨ Проверить решение и практиковаться

Устранение имен свойств с использованием дескрипторов

На предыдущем этапе при создании типизированных свойств нам приходилось явно указывать имена свойств. Это избыточно, так как имена свойств уже указаны в определении класса. На этом этапе мы используем дескрипторы (descriptors), чтобы избавиться от этой избыточности.

Дескриптор в Python — это специальный объект, который контролирует, как происходит доступ к атрибутам. Когда вы реализуете метод __set_name__ в дескрипторе, он может автоматически получить имя атрибута из определения класса.

Начнем с создания нового файла.

  1. Создайте новый файл с именем improved_typedproperty.py со следующим кодом:
## improved_typedproperty.py

class TypedProperty:
    """
    A descriptor that performs type checking.

    This descriptor automatically captures the attribute name from the class definition.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## This method is called when the descriptor is assigned to a class attribute
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Convenience functions
def String():
    """Create a string property with type checking."""
    return TypedProperty(str)

def Integer():
    """Create an integer property with type checking."""
    return TypedProperty(int)

def Float():
    """Create a float property with type checking."""
    return TypedProperty(float)

В этом коде определен класс-дескриптор TypedProperty, который проверяет тип значений, присваиваемых атрибутам. Метод __set_name__ вызывается автоматически, когда дескриптор присваивается атрибуту класса. Это позволяет дескриптору получить имя атрибута без необходимости вручную его указывать.

Далее создадим класс, который использует эти улучшенные типизированные свойства.

  1. Создайте новый файл с именем stock_improved.py, который использует улучшенные типизированные свойства:
from improved_typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    ## No need to specify property names anymore
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

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

Теперь протестируем наш улучшенный класс.

  1. Создайте тестовый файл test_stock_improved.py для тестирования улучшенной версии:
from stock_improved import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try setting attributes with wrong types
try:
    s.name = 123  ## Should raise TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Should raise TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Should raise TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

Наконец, запустим тест, чтобы убедиться, что все работает как ожидается.

  1. Запустите тест:
python3 test_stock_improved.py

Вы должны увидеть вывод, похожий на следующий:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

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

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

Резюме

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

Вы также узнали, как использовать протокол дескрипторов (descriptor protocol) и метод __set_name__ для создания элегантных атрибутов с проверкой типов, которые автоматически получают свои имена из определений классов. Эти техники демонстрируют мощь и гибкость замыканий, позволяя вам кратко реализовывать сложные поведения. Понимание замыканий и дескрипторов дает вам больше инструментов для создания поддерживаемого и надежного кода на Python.