ζ༼Ɵ͆ل͜Ɵ͆༽ᶘ

Если вы не слышали о дескрипторах, значит, вы не знаете Python.

0 комментов
21.08.2022
4 мин чтения

Итак… что такое дескрипторы?

Дескрипторы — это способ управления доступом к атрибутам объекта. На самом деле, хорошая вещь в них заключается в том, что вы снимаете с класса ответственность за установку и получение атрибутов и передаете эту ответственность другому классу, который имеет только эту единственную цель. Так что это поможет вам следовать принципу SRP, ура!

Позвольте привести пример. Большинству из вас знаком декоратор @property, который в основном делает то же самое, что и дескриптор (вероятно, он реализуется с помощью дескриптора). Итак, давайте напишем класс, который проверяет атрибут:

class Car:
    def __init__(self):
        self.fuel_amount = 0

    @property
    def fuel_amount(self):
        return self._fuel_amount

    @fuel_amount.setter
    def fuel_amount(self, amount):
        if amount < 0:
            raise ValueError("Tank can't be less than empty!")

        if amount > 60:
            raise ValueError("Tank can't take more than 60 l!")

        self._fuel_amount = amount

В этом примере у нас есть класс Car с топливным баком, и мы хотим ограничить fuel_amount, чтобы он не мог быть меньше пустого или больше предела в 60 литров.

Самая интересная часть — это @fuel_amount.setter, где мы вызываем ValueError, если количество, которое мы пытаемся установить, меньше 0 или больше 60, и, наконец, мы сохраняем количество топлива в частном атрибуте _fuel_amount. Для получения количества топлива метод получения (определенный @property ) просто возвращает значение из частного атрибута.

Если мы воспользуемся этим классом Car и попытаемся установить разное количество топлива, результат будет следующим:

>>> car = Car()
>>> car.fuel_amount = 50
>>> car.fuel_amount
50
>>> car.fuel_amount = 70
Traceback (most recent call last):
  ...
ValueError: Tank can't take more than 60 l!
>>> car.fuel_amount = -10
Traceback (most recent call last):
  ...
ValueError: Tank can't be less than empty!

Как видите, декоратор свойств — удобный и простой способ проверки значений атрибутов. Но что произойдет, если мы добавим в наш класс Car дополнительные атрибуты, которые необходимо проверить? Как вы можете себе представить, наш класс становится все более и более загроможденным операторами @property, и фактическая логика класса не очень очевидна для читателя. К счастью, Python предлагает решение этой проблемы!

Дескрипторы для победы!

Дескрипторы предоставляют решение, которое помогает нам разделить проблемы внутри нашего класса, поэтому наш код остается красивым и НАДЕЖНЫМ. Используя дескриптор вместо свойства, наш класс Car становится таким коротким:

class Car:

    fuel_amount = SixtyLitresCapacity()

    def __init__(self):
        self.fuel_amount = 0

Поведение точно такое же, как и раньше, только ответственность за проверку правильного количества топлива перешла к другому классу. В результате получился хороший и чистый Car класс. Так как же реализуется эта волшебная штука SixtyLitresCapacity?

class SixtyLitresCapacity:
    def __set__(self, car, amount):
        if amount < 0:
            raise ValueError("Tank can't be less than empty!")

        if amount > 60:
            raise ValueError("Tank can't take more than 60 l!")

        car._fuel_amount = amount

    def __get__(self, car, objtype=None):
        return car._fuel_amount

Единственное, что мы изменили, это то, что мы создали новый класс с методами __set__ и __get__, которые делают то же самое, что и @property и @fuel_amount.setter ранее. Этот класс точно определяет дескриптор.

Чтобы сообщить классу Car, что мы хотим проверить нашу fuel_amount с помощью этого дескриптора, единственное, что нам нужно сделать, это дать атрибуту класса экземпляр дескриптора fuel_amount = SixtyLitresCapacity() . И вуаля! Каждый раз, когда мы устанавливаем количество топлива на одном из наших экземпляров автомобиля, оно проверяется для нас.

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

Конечно, мы все еще можем сделать лучше

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

class IsBetween:
    def __init__(self,
                 min_value, 
                 max_value, 
                 below_exception=ValueError(),                        
                 above_exception=ValueError()):
        self.min_value = min_value
        self.max_value = max_value

        self.below_exception = below_exception
        self.above_exception = above_exception

    def __set_name__(self, owner, name):
        self.private_name = '_' + name
        self.public_name = name

    def __set__(self, obj, value):
        if value < self.min_value:
            raise self.below_exception

        if value > self.max_value:
            raise self.above_exception

        setattr(obj, self.private_name, value)

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

Здесь мы добавили несколько новых вещей. Прежде всего, мы добавили метод __init__ для включения параметров для нижней и верхней границ (min_value и max_value). Таким образом, мы можем не только иметь 60-литровые баки, но и, например, выдает ошибку, когда в баке осталось всего 5 литров топлива.

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

Самая интересная часть — это новый метод __set_name__(self, owner, name). Таким образом, приватный атрибут количества топлива не обязательно должен быть _fuel_amount, но может быть любым по вашему желанию. Этот метод открывает дверь для фактического класса автомобиля, чтобы дать имя атрибута дескриптору. Без этого метода дескриптор не получает никакой информации от своего owner.

Класс автомобиля теперь выглядит так:

class Car:

    fuel_amount = IsBetween(0, 60, ValueError(), ValueError())

    def __init__(self):
        self.fuel_amount = 0

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

1
Сегодня
День улёта