Дескрипторы в Python — как работают и примеры
В этом руководстве вы узнаете о дескрипторах в Python, о том, как они работают и как их более эффективно применять.
- Что такое дескрипторы Python?
- Протокол дескриптора
- Определение дескриптора
- Как работают
- Метод __set_name__
- Метод __set__
- Метод __get__
Что такое дескрипторы 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__, который присваивает атрибуту экземпляра класса новое значение.
Определение дескриптора
Дескриптор — это объект класса, реализующий один из методов, указанных в протоколе дескриптора.
Дескрипторы бывают двух типов: дескрипторы данных и дескрипторы, не относящиеся к данным.
- Дескриптор данных — это объект класса, реализующий метод __set__ и/или __delete__.
- Дескриптор, не являющийся данными, — это объект, реализующий только метод __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.