В этом материале создается практичный и мощный инструмент (валидатор) для обнаружения известных ошибок, связанных с повреждением данных.
Валидатор - это дескриптор управляемого доступа к атрибутам. Перед сохранением каких-либо данных он проверяет, соответствует ли новое значение различным ограничениям типа и диапазона. Если эти ограничения не соблюдаются, возникает исключение, которое предотвращает повреждение данных в источнике.
Представленный ниже класс 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)