Сообщить об ошибке.

Практический пример дескриптора в Python

В этом материале создается практичный и мощный инструмент (валидатор) для обнаружения известных ошибок, связанных с повреждением данных.

Валидатор - это дескриптор управляемого доступа к атрибутам. Перед сохранением каких-либо данных он проверяет, соответствует ли новое значение различным ограничениям типа и диапазона. Если эти ограничения не соблюдаются, возникает исключение, которое предотвращает повреждение данных в источнике.

Представленный ниже класс Validator является одновременно абстрактным базовым классом и дескриптором управляемого атрибута:

from abc import ABC, abstractmethod

class Validator(ABC):

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

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

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    def validate(self, value):
        pass

Пользовательские валидаторы должны наследоваться от Validator и должны предоставлять/переопределять метод .validate() для проверки различных ограничений по мере необходимости.

Пользовательские валидаторы.

Создадим 3 практических утилиты проверки данных:

  • Класс OneOf проверяет, является ли значение одним из ограниченного набора параметров.

  • Класс Number проверяет, является ли значение либо целым числом, либо числом с плавающей запятой. При необходимости он проверяет, находится ли значение между заданным минимумом или максимумом.

  • Класс String проверяет, что значение является строкой. При желании он проверяет заданную минимальную или максимальную длину. Он также может проверять определяемый пользователем предикат.

class OneOf(Validator):

    def __init__(self, *options):
        self.options = set(options)

    def validate(self, value):
        if value not in self.options:
            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

class Number(Validator):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )

class String(Validator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

Практическое применение

Вот как можно использовать валидаторы данных в реальном классе:

class Component:

    # добавляем валидаторы
    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('wood', 'metal', 'plastic')
    quantity = Number(minvalue=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

Дескрипторы предотвращают создание недопустимых экземпляров:

# Заблокировано: 'Widget' не весь в верхнем регистре
>>> Component('Widget', 'metal', 5)
# Traceback (most recent call last):
#     ...
# ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

# Заблокировано: слово "metle" написано с ошибкой
>>> Component('WIDGET', 'metle', 5) 
# Traceback (most recent call last):
#     ...
# ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

# Заблокировано: -5 отрицательное число
>>> Component('WIDGET', 'metal', -5)
# Traceback (most recent call last):
#     ...
# ValueError: Expected -5 to be at least 0

# Заблокировано: "V" - это не число
>>> Component('WIDGET', 'metal', 'V')
# Traceback (most recent call last):
#     ...
# TypeError: Expected 'V' to be an int or float

# Разрешено: ввод допустим
>>> c = Component('WIDGET', 'metal', 5)