Привет, Хабр!
Дескриптор — это объектовый атрибут с поведением, определяемым методами в его классе. Если просто — это способ, с помощью которого объект может контролировать доступ к его атрибутам, используя специально определенные методы __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.
ссылка на оригинал статьи https://habr.com/ru/articles/801595/
Добавить комментарий