Изучите дескрипторы

Beginner

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

Введение

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

Цели этого практического занятия включают понимание протокола дескрипторов, создание и использование пользовательских дескрипторов, реализацию валидации данных с помощью дескрипторов и оптимизацию реализации дескрипторов. В ходе практического занятия вы создадите несколько файлов, в том числе descrip.py, stock.py и validate.py.

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня начальный с процентом завершения 91%. Он получил 100% положительных отзывов от учащихся.

Понимание протокола дескрипторов

На этом этапе мы узнаем, как работают дескрипторы в Python, создав простой класс Stock. Дескрипторы в Python - это мощная возможность, которая позволяет настроить, как осуществляется доступ к атрибутам, как они устанавливаются и удаляются. Протокол дескрипторов состоит из трех специальных методов: __get__(), __set__() и __delete__(). Эти методы определяют, как дескриптор ведет себя при доступе к атрибуту, при присвоении ему значения или при удалении, соответственно.

Сначала нам нужно создать новый файл с именем stock.py в директории проекта. Этот файл будет содержать наш класс Stock. Вот код, который вы должны поместить в файл stock.py:

## stock.py

class Stock:
    __slots__ = ['_name', '_shares', '_price']

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

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an integer')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._shares = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        self._price = value

    def cost(self):
        return self.shares * self.price

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

В этом классе Stock мы используем декоратор property для определения методов-геттеров и сеттеров для атрибутов name, shares и price. Эти методы-геттеры и сеттеры действуют как дескрипторы, что означает, что они контролируют, как к этим атрибутам осуществляется доступ и как они устанавливаются. Например, методы-сеттеры валидируют входные значения, чтобы убедиться, что они имеют правильный тип и находятся в приемлемом диапазоне.

Теперь, когда наш файл stock.py готов, откроем оболочку Python, чтобы поэкспериментировать с классом Stock и увидеть, как работают дескрипторы на практике. Для этого откройте терминал и выполните следующие команды:

cd ~/project
python3 -i stock.py

Опция -i в команде python3 сообщает Python запустить интерактивную оболочку после выполнения файла stock.py. Таким образом, мы можем напрямую взаимодействовать с классом Stock, который мы только что определили.

В оболочке Python создадим объект акции и попробуем получить доступ к его атрибутам. Вот как это можно сделать:

s = Stock('GOOG', 100, 490.10)
s.name      ## Should return 'GOOG'
s.shares    ## Should return 100

Когда вы получаете доступ к атрибутам name и shares объекта s, Python на самом деле использует метод __get__ дескриптора в фоновом режиме. Декораторы property в нашем классе реализованы с использованием дескрипторов, что означает, что они контролируют доступ к атрибутам и их присваивание.

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

Stock.__dict__.keys()

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

dict_keys(['__module__', '__slots__', '__init__', 'name', '_name', 'shares', '_shares', 'price', '_price', 'cost', 'sell', '__repr__', '__doc__'])

Ключи name, shares и price представляют объекты дескрипторов, созданные декораторами property.

Теперь давайте рассмотрим, как работают дескрипторы, вызвав их методы вручную. В качестве примера мы будем использовать дескриптор shares. Вот как это можно сделать:

## Get the descriptor object for 'shares'
q = Stock.__dict__['shares']

## Use the __get__ method to retrieve the value
q.__get__(s, Stock)  ## Should return 100

## Use the __set__ method to set a new value
q.__set__(s, 75)
s.shares  ## Should now be 75

## Try to set an invalid value
try:
    q.__set__(s, '75')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

Когда вы получаете доступ к атрибуту, такому как s.shares, Python вызывает метод __get__ дескриптора, чтобы получить значение. Когда вы присваиваете значение, как в s.shares = 75, Python вызывает метод __set__ дескриптора. Затем дескриптор может проверить данные и вызвать ошибки, если входное значение не является допустимым.

После того, как вы закончите экспериментировать с классом Stock и дескрипторами, вы можете выйти из оболочки Python, выполнив следующую команду:

exit()

Создание пользовательских дескрипторов

На этом этапе мы создадим собственный класс дескриптора. Но сначала разберемся, что такое дескриптор. Дескриптор - это объект Python, реализующий протокол дескрипторов, состоящий из методов __get__, __set__ и __delete__. Эти методы позволяют дескриптору управлять доступом к атрибуту, его установкой и удалением. Создав собственный класс дескриптора, мы сможем лучше понять, как работает этот протокол.

Создайте новый файл с именем descrip.py в директории проекта. Этот файл будет содержать наш пользовательский класс дескриптора. Вот код:

## descrip.py

class Descriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        print(f'{self.name}:__get__')
        ## In a real descriptor, you would return a value here

    def __set__(self, instance, value):
        print(f'{self.name}:__set__ {value}')
        ## In a real descriptor, you would store the value here

    def __delete__(self, instance):
        print(f'{self.name}:__delete__')
        ## In a real descriptor, you would delete the value here

В классе Descriptor метод __init__ инициализирует дескриптор с именем. Метод __get__ вызывается при доступе к атрибуту, метод __set__ - при установке атрибута, а метод __delete__ - при удалении атрибута.

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

## test_descrip.py

from descrip import Descriptor

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

## Create an instance and try accessing the attributes
if __name__ == '__main__':
    f = Foo()
    print("Accessing attribute f.a:")
    f.a

    print("\nAccessing attribute f.b:")
    f.b

    print("\nSetting attribute f.a = 23:")
    f.a = 23

    print("\nDeleting attribute f.a:")
    del f.a

В файле test_descrip.py мы импортируем класс Descriptor из файла descrip.py. Затем создаем класс Foo с тремя атрибутами a, b и c, каждый из которых управляется дескриптором. Создаем экземпляр класса Foo и выполняем такие операции, как доступ к атрибутам, их установка и удаление, чтобы увидеть, как вызываются методы дескриптора.

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

cd ~/project
python3 test_descrip.py

Вы должны увидеть такой вывод:

Accessing attribute f.a:
a:__get__

Accessing attribute f.b:
b:__get__

Setting attribute f.a = 23:
a:__set__ 23

Deleting attribute f.a:
a:__delete__

Как вы можете видеть, каждый раз, когда вы обращаетесь к атрибуту, управляемому дескриптором, устанавливаете его значение или удаляете, вызывается соответствующий магический метод (__get__, __set__ или __delete__).

Давайте также проверим наш дескриптор в интерактивном режиме. Это позволит нам протестировать дескриптор в реальном времени и сразу увидеть результаты. Откройте терминал, перейдите в директорию проекта и запустите интерактивную сессию Python с файлом descrip.py:

cd ~/project
python3 -i descrip.py

Теперь введите следующие команды в интерактивной сессии Python, чтобы увидеть, как работает протокол дескрипторов:

class Foo:
    a = Descriptor('a')
    b = Descriptor('b')
    c = Descriptor('c')

f = Foo()
f.a         ## Should call __get__
f.b         ## Should call __get__
f.a = 23    ## Should call __set__
del f.a     ## Should call __delete__
exit()

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

Реализация валидаторов с использованием дескрипторов

На этом этапе мы создадим систему валидации с использованием дескрипторов. Но сначала разберемся, что такое дескрипторы и почему мы их используем. Дескрипторы - это объекты Python, реализующие протокол дескрипторов, который включает методы __get__, __set__ или __delete__. Они позволяют настроить, как осуществляется доступ к атрибуту объекта, как он устанавливается или удаляется. В нашем случае мы используем дескрипторы для создания системы валидации, которая обеспечивает целостность данных. Это означает, что данные, хранящиеся в наших объектах, всегда будут соответствовать определенным критериям, например, иметь определенный тип или быть положительными значениями.

Теперь приступим к созданию нашей системы валидации. Создадим новый файл с именем validate.py в директории проекта. Этот файл будет содержать классы, реализующие наши валидаторы.

## validate.py

class Validator:
    def __init__(self, name):
        self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

В файле validate.py мы сначала определяем базовый класс Validator. Этот класс имеет метод __init__, который принимает параметр name, который будет использоваться для идентификации атрибута, подлежащего валидации. Метод check - это метод класса, который просто возвращает переданное ему значение. Метод __set__ - это метод дескриптора, который вызывается при установке атрибута объекта. Он вызывает метод check для валидации значения, а затем сохраняет валидированное значение в словаре объекта.

Затем мы определяем три подкласса Validator: String, PositiveInteger и PositiveFloat. Каждый из этих подклассов переопределяет метод check для выполнения конкретных проверок валидации. Класс String проверяет, является ли значение строкой, класс PositiveInteger проверяет, является ли значение положительным целым числом, а класс PositiveFloat проверяет, является ли значение положительным числом (целым или вещественным).

Теперь, когда мы определили наши валидаторы, изменим наш класс Stock, чтобы использовать эти валидаторы. Создадим новый файл с именем stock_with_validators.py и импортируем валидаторы из файла validate.py.

## stock_with_validators.py

from validate import String, PositiveInteger, PositiveFloat

class Stock:
    name = String('name')
    shares = PositiveInteger('shares')
    price = PositiveFloat('price')

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

    def cost(self):
        return self.shares * self.price

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

В файле stock_with_validators.py мы определяем класс Stock и используем валидаторы в качестве атрибутов класса. Это означает, что каждый раз, когда устанавливается атрибут объекта Stock, будет вызван метод __set__ соответствующего валидатора для валидации значения. Метод __init__ инициализирует атрибуты объекта Stock, а методы cost, sell и __repr__ предоставляют дополнительную функциональность.

Теперь протестируем наш класс Stock, основанный на валидаторах. Откроем терминал, перейдем в директорию проекта и запустим файл stock_with_validators.py в интерактивном режиме.

cd ~/project
python3 -i stock_with_validators.py

После запуска интерпретатора Python мы можем попробовать несколько команд для тестирования системы валидации.

## Create a stock with valid values
s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing to valid values
s.shares = 75
print(s.shares)  ## Should return 75

## Try setting invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error setting shares to string: {e}")

try:
    s.shares = -50  ## Should raise ValueError
except ValueError as e:
    print(f"Error setting negative shares: {e}")

exit()

В тестовом коде мы сначала создаем объект Stock с допустимыми значениями и выводим его атрибуты, чтобы убедиться, что они установлены правильно. Затем мы пытаемся изменить атрибут shares на допустимое значение и выводим его снова, чтобы подтвердить изменение. Наконец, мы пытаемся установить атрибут shares в недопустимое значение (строку и отрицательное число) и перехватываем исключения, которые вызываются валидаторами.

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

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

Улучшение реализации дескрипторов

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

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

Теперь обновим файл validate.py, чтобы он включал метод __set_name__. Вот как будет выглядеть обновленный код:

## validate.py

class Validator:
    def __init__(self, name=None):
        self.name = name

    def __set_name__(self, cls, name):
        ## This gets called when the class is defined
        ## It automatically sets the name of the descriptor
        if self.name is None:
            self.name = name

    @classmethod
    def check(cls, value):
        return value

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.check(value)


class String(Validator):
    expected_type = str

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return value


class PositiveInteger(Validator):
    expected_type = int

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        if value < 0:
            raise ValueError('Expected a positive value')
        return value


class PositiveFloat(Validator):
    expected_type = float

    @classmethod
    def check(cls, value):
        if not isinstance(value, (int, float)):
            raise TypeError('Expected a number')
        if value < 0:
            raise ValueError('Expected a positive value')
        return float(value)

В приведенном выше коде метод __set_name__ в классе Validator проверяет, является ли атрибут name равным None. Если это так, он устанавливает name равным фактическому имени атрибута, используемому в определении класса. Таким образом, нам не нужно явно указывать имя при создании экземпляров классов дескрипторов.

Теперь, когда мы обновили файл validate.py, мы можем создать улучшенную версию нашего класса Stock. В этой новой версии нам не нужно будет избыточно указывать имена. Вот код улучшенного класса Stock:

## improved_stock.py

from validate import String, PositiveInteger, PositiveFloat

class Stock:
    name = String()  ## No need to specify 'name' anymore
    shares = PositiveInteger()
    price = PositiveFloat()

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

    def cost(self):
        return self.shares * self.price

    def sell(self, amount):
        self.shares -= amount

    def __repr__(self):
        return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'

В этом классе Stock мы просто создаем экземпляры классов дескрипторов String, PositiveInteger и PositiveFloat без указания имен. Метод __set_name__ в классе Validator автоматически позаботится о установке имен.

Давайте протестируем наш улучшенный класс Stock. Сначала откройте терминал и перейдите в директорию проекта. Затем запустите файл improved_stock.py в интерактивном режиме. Вот команды для этого:

cd ~/project
python3 -i improved_stock.py

Как только вы войдете в интерактивную сессию Python, вы можете попробовать следующие команды, чтобы протестировать функциональность класса Stock:

s = Stock('GOOG', 100, 490.10)
print(s.name)    ## Should return 'GOOG'
print(s.shares)  ## Should return 100
print(s.price)   ## Should return 490.1

## Try changing values
s.shares = 75
print(s.shares)  ## Should return 75

## Try invalid values
try:
    s.shares = '75'  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    s.price = -10.5  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

exit()

Эти команды создают экземпляр класса Stock, выводят его атрибуты, изменяют значение атрибута, а затем пытаются установить недопустимые значения, чтобы проверить, вызываются ли соответствующие ошибки.

Метод __set_name__ автоматически устанавливает имя дескриптора при определении класса. Это делает ваш код чище и менее избыточным, так как вам больше не нужно указывать имя атрибута дважды.

Это улучшение демонстрирует, как протокол дескрипторов Python продолжает развиваться, делая проще написание чистого и поддерживаемого кода.

Резюме

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

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