Привет, Хабр!
Дескриптор — это объектовый атрибут с поведением, определяемым методами в его классе. Если просто — это способ, с помощью которого объект может контролировать доступ к его атрибутам, используя специально определенные методы __get__
, __set__
, и __delete__
. Если говорить еще проще — дескрипторы позволяют задавать точки доступа к атрибутам объекта, добавляя дополнительную логику, когда атрибут читается, записывается или удаляется.
В этой статье поговорим подробней про дескрпиторы.
Основные методы
Протокол дескриптора в Python определяется наличием методов __get__
, __set__
и __delete__
в классе. Эти методы позволяют объектам управлять тем, как значения атрибутов извлекаются, устанавливаются или удаляются. Также существует необязательный метод __set_name__
, который позволяет дескриптору узнавать имя атрибута, к которому он присвоен в классе.
Метод __get__
вызывается, когда значение атрибута извлекается, он принимает два аргумента: self
и instance
. instance
- это экземпляр объекта, через который доступен дескриптор, или None
, если обращение идет через класс. Возвращаемое значение этого метода будет значением указанного атрибута:
class Descriptor:
def __get__(self, instance, owner):
return 'значение'
class MyClass:
attr = Descriptor()
my_object = MyClass()
print(my_object.attr) # выведет 'значение'
__set__
позволяет управлять изменением значения атрибута. Он принимает три аргумента: self
, instance
и value
, где value
- это новое значение атрибута:
class Descriptor:
def __set__(self, instance, value):
print(f"Установка значения {value}")
self.__value = value
class MyClass:
attr = Descriptor()
my_object = MyClass()
my_object.attr = 10 # выведет 'Установка значения 10'
__delete__
вызывается при удалении атрибута с использованием оператора del
. Он принимает два аргумента: self
и instance
:
class Descriptor:
def __delete__(self, instance):
print("Удаление атрибута")
del self.__value
class MyClass:
attr = Descriptor()
my_object = MyClass()
del my_object.attr # выведет 'Удаление атрибута'
Необязательный метод __set_name__
вызывается в момент создания класса для каждого дескриптора, что позволяет дескриптору знать имя атрибута, к которому он привязан. Этот метод принимает два аргумента: self
и name
, где name
- это имя атрибута:
class Descriptor:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = '_' + name
def __get__(self, instance, owner):
return getattr(instance, self.private_name, 'еще не установлено')
def __set__(self, instance, value):
setattr(instance, self.private_name, value)
class MyClass:
attr = Descriptor()
my_object = MyClass()
print(my_object.attr) # выведет 'еще не установлено'
my_object.attr = 99
print(my_object.attr) # выведет 99
Примеры использования
Создадим дескриптор для валидации данных, который будет проверять, что возраст пользователя не может быть отрицательным числом и не может превышать 100 лет:
class ValidateAge:
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, instance, owner):
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError("Возраст должен быть между 0 и 100 годами")
setattr(instance, self.private_name, value)
class Person:
age = ValidateAge()
def __init__(self, name, age):
self.name = name
self.age = age
try:
p = Person("Kolya", 30) # валидный возраст
print(p.age)
p.age = -5 # невалидный возраст, будет вызвано исключение ValueError
except ValueError as e:
print(e)
Теперь создадим дескриптор для кэширования результатов тяжелых вычислений. Предположим, есть функция, выполнение которой занимает значительное время, и ye;yj кэшировать ее результат для одних и тех же входных данных:
import time
class CachedAttribute:
def __init__(self, method):
self.method = method
self.cache = {}
def __get__(self, instance, owner):
if instance not in self.cache:
self.cache[instance] = self.method(instance)
return self.cache[instance]
class HeavyComputation:
@CachedAttribute
def compute(self):
# имитация длительного вычисления
time.sleep(2)
return "Результат вычисления"
hc = HeavyComputation()
start_time = time.time()
print(hc.compute) # первый вызов занимает время
print(f"Выполнено за {time.time() - start_time} секунд")
start_time = time.time()
print(hc.compute) # второй вызов мгновенный, использует кэшированный результат
print(f"Выполнено за {time.time() - start_time} секунд")
Создадим дескриптор, который будет логировать любые изменения значений атрибутов:
class LoggedAttribute:
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, instance, owner):
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
print(f"Установка {self.private_name} в {value}")
setattr(instance, self.private_name, value)
class User:
name = LoggedAttribute()
age = LoggedAttribute()
def __init__(self, name, age):
self.name = name
self.age = age
u = User("Katya", 30)
u.name = "Katyuha" # Логируется изменение
u.age = 31 # Логируется изменение
Реализация паттернов Singleton и Factory
Не будем объяснять в контексте этой статьи о том, зачем нужен паттерн Singleton. Но если коротко, то он гарантирует, что класс имеет только один экземпляр и предоставляет глобальную точку доступа к этому экземпляру.
Создадим дескриптор Singleton
, который будет управлять созданием экземпляров другого класса, гарантируя, что создается только один экземпляр:
class Singleton:
def __init__(self, cls):
self.cls = cls
self.instance = None
def __get__(self, instance, owner):
if self.instance is None:
self.instance = self.cls()
return self.instance
class Database:
def __init__(self):
print("Создание базы данных")
# применение дескриптора Singleton
class AppConfig:
db = Singleton(Database)
# тестирование паттерна Singleton
config1 = AppConfig()
config2 = AppConfig()
db1 = config1.db # создание БД
db2 = config2.db # не создает новый экземпляр, использует существующий
print(db1 is db2) # выведет True, подтверждая, что db1 и db2 - один и тот же объект
Factory — это паттерн проектирования, который используется для создания объектов без указания конкретных классов объектов.
Для реализации этого паттерна можно создать дескриптор, который будет динамически определять, какой объект создавать, основываясь на каком-либо условии или конфигурации:
class VehicleFactory:
def __init__(self, cls):
self.cls = cls
def __get__(self, instance, owner):
return self.cls()
class Car:
def drive(self):
print("Вождение автомобиля")
class Bike:
def ride(self):
print("Езда на велосипеде")
# фабрика, создающая автомобили
class AppConfigCar:
vehicle = VehicleFactory(Car)
# фабрика, создающая велосипеды
class AppConfigBike:
vehicle = VehicleFactory(Bike)
# создание и использование автомобиля
car_config = AppConfigCar()
car = car_config.vehicle # создает объект Car
car.drive()
# создание и использование велосипеда
bike_config = AppConfigBike()
bike = bike_config.vehicle # создает объект Bike
bike.ride()
Встроенные функции property, classmethod и staticmethod
property
- это встроенная функция Python, с ее помощью можно создать атрибут, значение которого генерируется динамически через методы геттер и сеттер. Юзабельно, когда надо добавить логику валидации при присвоении значения атрибуту или когда значение атрибута зависит от других атрибутов.
property
работает как дескриптор, используя методы __get__
, __set__
и __delete__
для управления доступом к атрибуту:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
def get_temperature(self):
print("Получение значения")
return self._temperature
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Температура не может быть ниже -273.15 градусов Цельсия")
print("Установка значения")
self._temperature = value
temperature = property(get_temperature, set_temperature)
c = Celsius(37)
print(c.temperature)
c.temperature = -300 # вызовет исключение
classmethod
- это декоратор, который изменяет метод так, что он получает класс (а не экземпляр класса) в качестве первого аргумента.
Внутренне classmethod
реализован как дескриптор. Когда метод декорирован как classmethod
, его вызов приводит к вызову метода __get__
дескриптора, который возвращает привязанный метод - функцию, первым аргументом которой автоматически становится класс:
class A:
@classmethod
def method(cls):
return f"вызван classmethod класса {cls}"
print(A.method()) # вызван classmethod класса <class '__main__.A'>
staticmethod
- это декоратор, который изменяет метод класса так, что он ведет себя как обычная функция, не принимающая ни self
, ни cls
в качестве первого аргумента.
staticmethod
также реализован как дескриптор. При его использовании метод __get__
дескриптора просто возвращает функцию без привязки к экземпляру или классу:
class Math:
@staticmethod
def add(x, y):
return x + y
print(Math.add(5, 7)) # 12
Несколько особенностей
Дескрипторы данных __get__
и __set__
имеют приоритет над атрибутами экземпляра, тогда как дескрипторы без данных __get__
без __set__
уступают им. Это важно учитывать при проектировании классов и их поведения.
__set__
не будет вызываться, если попытаться изменить атрибут через прямое обращение к __dict__
экземпляра. Чтобы избежать этой ошибки, важно всегда использовать обычное присваивание для установки значений атрибутов, тем самым обеспечивая вызов метода __set__
.
Если в классе определен дескриптор и атрибут экземпляра с одинаковым именем, это может привести к неожиданному поведению. В таком случае дескриптор будет иметь приоритет при доступе через класс, но атрибут экземпляра может своеобразно скрыть дескриптор при доступе напрямую. Чтобы избежать подобной ситуации, следует избегать именования атрибутов экземпляра и дескрипторов одинаковыми именами.
При реализации метода __get__
важно помнить, что он должен корректно обрабатывать ситуацию, когда instance
аргумент равен None
. Это случается, когда доступ к атрибуту осуществляется через сам класс, а не его экземпляр. Обычно в таком случае рекомендуется возвращать сам дескриптор (т.е., self
).
Дескрипторы в Python — это удобный способ добавления логики к доступу к атрибутам.
Статья подготовлена в преддверии запуска нового потока специализации Python Developer.
nameisBegemot
Питон простой язык, говорили они. Главное не обращать внимание на механизмы языка, даже смысл которых понять не просто. И дескрипторы не самое страшное.
Чего, например, стоят мета-классы. Где даже сами разработчики питона описывают их как - "если не знаешь что это, значит оно тебе не нужно"