Дескрипторы в Python — как работают и примеры

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

Содержание

Что такое дескрипторы Python?

Предположим, у вас есть класс Person с двумя атрибутами экземпляра first_name и Last_name:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

И вы хотите, чтобы атрибуты first_name и Last_name были непустыми строками. Эти простые атрибуты не могут этого гарантировать.

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

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise ValueError('The first name must a string')

        if len(value) == 0:
            raise ValueError('The first name cannot be empty')

        self._first_name = value

    @property
    def last_name(self):
        return self._last_name

    @last_name.setter
    def last_name(self, value):
        if not isinstance(value, str):
            raise ValueError('The last name must a string')

        if len(value) == 0:
            raise ValueError('The last name cannot be empty')

        self._last_name = value

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

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

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

Сначала определите класс дескриптора, реализующий три метода __set_name__, __get__ и __set__:

class RequiredString:
    def __set_name__(self, owner, name):
        self.property_name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self

        return instance.__dict__[self.property_name] or None

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError(f'The {self.property_name} must be a string')

        if len(value) == 0:
            raise ValueError(f'The {self.property_name} cannot be empty')

        instance.__dict__[self.property_name] = value

Во-вторых, используйте класс RequiredString в классе Person:

class Person:
    first_name = RequiredString()
    last_name = RequiredString()

Если вы присвоите пустую строку или нестроковое значение атрибуту first_name или Last_name класса Person, вы получите сообщение об ошибке.

Например, следующая попытка присвоить пустую строку атрибуту first_name:

try:
    person = Person()
    person.first_name = ''
except ValueError as e:
    print(e)

вызывает ошибку:

The first_name must be a string

Кроме того, вы можете использовать класс RequiredString в любом классе с атрибутами, для которых требуется непустое строковое значение.

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

Давайте разберемся, как работают дескрипторы.

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

В Python протокол дескриптора состоит из трех методов:

  • __get__ получает значение атрибута;
  • __set__ устанавливает значение атрибута;
  • __delete__ удаляет атрибут.

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

Определение дескриптора

Дескриптор — это объект класса, реализующий один из методов, указанных в протоколе дескриптора.

Дескрипторы бывают двух типов: дескрипторы данных и дескрипторы, не относящиеся к данным.

  1. Дескриптор данных — это объект класса, реализующий метод __set__ и/или __delete__.
  2. Дескриптор, не являющийся данными, — это объект, реализующий только метод __get__.

Тип дескриптора определяет разрешение поиска свойств, которое мы рассмотрим в следующем руководстве.

Как работают

Следующий код изменяет класс RequiredString, включив в него операторы печати, которые распечатывают аргументы.

class RequiredString:
    def __set_name__(self, owner, name):
        print(f'__set_name__ was called with owner={owner} and name={name}')
        self.property_name = name

    def __get__(self, instance, owner):
        print(f'__get__ was called with instance={instance} and owner={owner}')
        if instance is None:
            return self

        return instance.__dict__[self.property_name] or None

    def __set__(self, instance, value):
        print(f'__set__ was called with instance={instance} and value={value}')

        if not isinstance(value, str):
            raise ValueError(f'The {self.property_name} must a string')

        if len(value) == 0:
            raise ValueError(f'The {self.property_name} cannot be empty')

        instance.__dict__[self.property_name] = value


class Person:
    first_name = RequiredString()
    last_name = RequiredString()

Метод __set_name__

Когда вы скомпилируете код, вы увидите, что Python создает объекты-дескрипторы для first_name и Last_name и автоматически вызывает метод __set_name__ этих объектов. Вот результат:

__set_name__ was called with owner=<class '__main__.Person'> and name=first_name
__set_name__ was called with owner=<class '__main__.Person'> and name=last_name

В этом примере аргументу __set_name__ присвоен класс Person в модуле __main__, а аргументу name присвоены атрибуты first_name и Last_name соответственно.

Это означает, что Python автоматически вызывает __set_name__ при создании класса-владельца Person. Следующие утверждения эквивалентны:

first_name = RequiredString()

и

first_name.__set_name__(Person, 'first_name')

Внутри метода __set_name__ мы присваиваем аргумент name атрибуту экземпляра property_name объекта дескриптора, чтобы мы могли получить к нему доступ позже в методах __get__ и __set__:

self.property_name = name

First_name и Last_name — это переменные класса Person. Если вы посмотрите на атрибут класса Person.__dict__, вы увидите два объекта-дескриптора first_name и Last_name:

from pprint import pprint

pprint(Person.__dict__)

Выход:

mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'Person' objects>,
            'first_name': <__main__.RequiredString object at 0x0000019D6AB947F0>,
            'last_name': <__main__.RequiredString object at 0x0000019D6ACFBE80>})

Метод __set__

Вот метод __set__ класса RequiredString:

def __set__(self, instance, value):
    print(f'__set__ was called with instance={instance} and value={value}')

    if not isinstance(value, str):
        raise ValueError(f'The {self.property_name} must be a string')

    if len(value) == 0:
        raise ValueError(f'The {self.property_name} cannot be empty')

    instance.__dict__[self.property_name] = value

Когда вы присваиваете новое значение дескриптору, Python вызывает метод __set__, чтобы установить новое значение атрибута экземпляра класса-владельца. Например:

person = Person()
person.first_name = 'John'

Выход:

__set__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and value=John

В этом примере аргументом экземпляра является объект person, а значением является строка «John». Внутри метода __set__ мы вызываем ValueError, если новое значение не является строкой или если это пустая строка.

В противном случае мы присваиваем значение атрибуту экземпляра first_name объекта person:

instance.__dict__[self.property_name] = value

Обратите внимание, что Python использует словарь instance.__dict__ для хранения атрибутов экземпляра объекта экземпляра.

После того как вы установите first_name и Last_name экземпляра объекта Person, вы увидите атрибуты экземпляра с теми же именами в __dict__ экземпляра. Например:

person = Person()
print(person.__dict__)  # {}

person.first_name = 'John'
person.last_name = 'Doe'

print(person.__dict__) # {'first_name': 'John', 'last_name': 'Doe'}

Выход:

{}
{'first_name': 'John', 'last_name': 'Doe'}

Метод __get__

Ниже показан метод __get__ класса RequiredString:

def __get__(self, instance, owner):
    print(f'__get__ was called with instance={instance} and owner={owner}')
    if instance is None:
        return self

    return instance.__dict__[self.property_name] or None

Python вызывает метод __get__ объекта Person при доступе к атрибуту first_name. Например:

person = Person()

person.first_name = 'John'
print(person.first_name)

Выход:

__set__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and value=John
__get__ was called with instance=<__main__.Person object at 0x000001F85F7167F0> and owner=<class '__main__.Person'>

Метод __get__ возвращает дескриптор, если экземпляр имеет значение None. Например, если вы получите доступ к first_name или Last_name из класса Person, вы увидите объект дескриптора:

print(Person.first_name)

Выход:

<__main__.RequiredString object at 0x000001AF1DA147F0>

Если экземпляр не имеет значения None, метод __get__() возвращает значение атрибута с именем property_name объекта instance.

Похожие посты
Добавить комментарий

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