Python: проверка правильности ввода при изменении класса данных
В Python 3.7 есть эти новые контейнеры "dataclass", которые в основном похожи на изменяемые именованные кортежи. Предположим, я создаю класс данных, предназначенный для представления человека. Я могу добавить подтверждение ввода через __post_init__()
функционировать так:
@dataclass
class Person:
name: str
age: float
def __post_init__(self):
if type(self.name) is not str:
raise TypeError("Field 'name' must be of type 'str'.")
self.age = float(self.age)
if self.age < 0:
raise ValueError("Field 'age' cannot be negative.")
Это даст хороший вклад через:
someone = Person(name="John Doe", age=30)
print(someone)
Person(name='John Doe', age=30.0)
В то время как все эти неверные данные приведут к ошибке:
someone = Person(name=["John Doe"], age=30)
someone = Person(name="John Doe", age="thirty")
someone = Person(name="John Doe", age=-30)
Однако, поскольку классы данных являются изменяемыми, я могу сделать это:
someone = Person(name="John Doe", age=30)
someone.age = -30
print(someone)
Person(name='John Doe', age=-30)
Таким образом, минуя входные проверки.
Итак, как лучше всего убедиться, что поля класса данных не видоизменены во что-то плохое после инициализации?
5 ответов
Классы данных - это механизм, обеспечивающий инициализацию по умолчанию для принятия атрибутов в качестве параметров, и хорошее представление, а также некоторые тонкости, такие как __post_init__
крюк.
К счастью, они не связываются с каким-либо другим механизмом доступа к атрибутам в Python - и вы все равно можете создавать свои атрибуты dataclassess как property
дескрипторы или пользовательский класс дескриптора, если хотите. Таким образом, любой доступ к атрибутам будет проходить через функции получения и установки автоматически.
Единственный недостаток использования по умолчанию property
встроенным является то, что вы должны использовать его "по-старому", а не с синтаксисом декоратора - это позволяет вам создавать аннотации для ваших атрибутов.
Итак, "дескрипторы" - это специальные объекты, назначенные атрибутам класса в Python таким образом, что любой доступ к этому атрибуту будет вызывать дескрипторы __get__
, __set__
или же __del__
методы. property
встроенный является условием для создания дескриптора, переданного от 1 до 3 функций, которые будут вызываться из этих методов.
Итак, без пользовательского дескриптора, вы можете сделать:
@dataclass
class MyClass:
def setname(self, value):
if not isinstance(value, str):
raise TypeError(...)
self.__dict__["name"] = value
def getname(self):
return self.__dict__.get(name)
name: str = property(getname, setname)
# optionally, you can delete the getter and setter from the class body:
del setname, getname
Используя этот подход, вы должны будете написать доступ к каждому атрибуту как два метода / функции, но вам больше не нужно будет писать __post_init__
: каждый атрибут будет проверен сам.
Также обратите внимание, что в этом примере использовался небольшой обычный подход для обычного хранения атрибутов в __dict__
, В примерах в Интернете практика заключается в использовании обычного доступа к атрибутам, но с добавлением имени перед _
, Это оставит эти атрибуты загрязняющими dir
на вашем последнем экземпляре, и личные атрибуты не будут охраняться.
Другой подход - написать собственный класс дескриптора и позволить ему проверять экземпляр и другие свойства атрибутов, которые вы хотите защитить. Это может быть настолько изощренным, насколько вы хотите, достигая кульминации с вашей собственной структурой. Таким образом, для класса дескриптора, который будет проверять тип атрибута и принимать список валидаторов, вам потребуется:
def positive_validator(name, value):
if value <= 0:
raise ValueError(f"values for {name!r} have to be positive")
class MyAttr:
def __init__(self, type, validators=()):
self.type = type
self.validators = validators
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if not instance: return self
return instance.__dict__[self.name]
def __delete__(self, instance):
del instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.type):
raise TypeError(f"{self.name!r} values must be of type {self.type!r}")
for validator in self.validators:
validator(self.name, value)
instance.__dict__[self.name] = value
#And now
@dataclass
class Person:
name: str = MyAttr(str)
age: float = MyAttr((int, float), [positive_validator,])
Вот и все - создание собственного дескрипторного класса требует немного больше знаний о Python, но приведенный выше код должен быть полезен даже в работе - вы можете его использовать.
Обратите внимание, что вы можете легко добавить множество других проверок и преобразований для каждого из ваших атрибутов - и код в __set_name__
Сам может быть изменен, чтобы самоанализ __annotations__
в owner
класс, чтобы автоматически принять к сведению типы - так, чтобы параметр типа не был необходим для MyAttr
сам класс. Но, как я уже говорил, вы можете сделать это настолько изощренным, насколько захотите.
Простым и гибким решением может быть переопределение__setattr__
метод:
@dataclass
class Person:
name: str
age: float
def __setattr__(self, name, value):
if name == 'age':
assert value > 0, f"value of {name} can't be negative: {value}"
self.__dict__[name] = value
Возможно, заблокируйте атрибут, используя методы получения и установки, вместо того, чтобы изменять атрибут напрямую. Если вы затем извлечете свою логику проверки в отдельный метод, вы можете проверить одинаково как с вашего установщика, так и с __post_init__
функция.
Ответ , предоставленный @jsbueno , user108205 , но он не допускает аргументов по умолчанию. Я расширил его, чтобы разрешить значения по умолчанию:
def positive_validator(name, value):
if value <= 0:
raise ValueError(f"values for {name!r} have to be positive")
class MyAttr:
def __init__(self, typ, validators=(), default=None):
if not isinstance(typ, type):
if isinstance(typ, tuple) and all([isinstance(t,type) for t in typ]):
pass
else:
raise TypeError(f"'typ' must be a {type(type)!r} or {type(tuple())!r}` of {type(type)!r}")
else:
typ=(typ,)
self.type = typ
self.name = f"MyAttr_{self.type!r}"
self.validators = validators
self.default=default
if self.default is not None or type(None) in typ:
self.__validate__(self.default)
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if not instance: return self
return instance.__dict__[self.name]
def __delete__(self, instance):
del instance.__dict__[self.name]
def __validate__(self, value):
for validator in self.validators:
validator(self.name, value)
def __set__(self, instance, value):
if value == self:
value = self.default
if not isinstance(value, self.type):
raise TypeError(f"{self.name!r} values must be of type {self.type!r}")
instance.__dict__[self.name] = value
#And now
@dataclass
class Person:
name: str = MyAttr(str,[]) # required attribute, must be a str, cannot be none
age: float = MyAttr((int, float), [positive_validator,],2) # optional attribute, must be an int >0, defaults to 2
posessions: Union[list, type(None)] = MyAttr((list, type(None)),[]) # optional attribute in which None is default
Это мое улучшение для дескрипторов, вы можете определить наши собственные функции
from dataclasses import dataclass
import re
from enum import Enum
class Descriptor:
def __init__(self, type, validators=(), **kwargs):
self.type = type
self.default = kwargs.get("default")
self.validators = validators
self.kwargs = kwargs
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if not instance:
return self
# return instance.__dict__[self.name]
return instance.__dict__.get(self.name, self.default)
def __delete__(self, instance):
del instance.__dict__[self.name]
def __set__(self, instance, value):
if isinstance(value, Descriptor):
value = self.default
else:
if not isinstance(value, self.type):
raise TypeError(
f"{self.name!r} VALUES MUST BE TYPE {self.type!r} NOT {type(value)}!"
)
switch = {
"POSITIVE": self.val_positive,
"BETWEEN": self.val_between_values,
"MAXMINZISE": self.val_between_sizes,
"SIZE": self.val_size,
"EMAIL": self.val_email,
"NUMBER": self.val_number,
"ONEOF": self.val_one_of,
}
for validator in self.validators:
if validator in switch:
switch[validator](value)
instance.__dict__[self.name] = value
def val_positive(self, value: int | float):
if value <= 0:
raise ValueError(f"VALUE IS NOT VALID FOR {self.name!r} MUST BE POSITIVE")
def val_between_values(self, value: int | float):
maxval = self.kwargs.get("maxval")
if maxval is not None:
if not isinstance(maxval, int) or isinstance(maxval, float):
raise TypeError(
f"MAX VALUE MUST BE TYPE INTEGER OR FLOAT NOT {type(value)}!"
)
if value > maxval:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} MUST BE GREATER THAN {maxval}"
)
minval = self.kwargs.get("minval")
if minval is not None:
if not isinstance(minval, int) or isinstance(maxval, float):
raise TypeError(
f"MIN VALUE MUST BE TYPE INTEGER OR FLOAT NOT {type(value)}!"
)
if value < minval:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} MUST BE LESS THAN {minval}"
)
def val_between_sizes(self, value: int):
maxsize = self.kwargs.get("maxsize")
if maxsize is not None:
if not isinstance(maxsize, int):
raise TypeError(f"MAX VALUE MUST BE TYPE INTEGER NOT {type(value)}!")
if len(value) > maxsize:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} MUST BE GREATER THAN {maxsize}"
)
minsize = self.kwargs.get("minsize")
if minsize is not None:
if not isinstance(minsize, int):
raise TypeError(f"MIN VALUE MUST BE TYPE INTEGER NOT {type(value)}!")
if len(value) < minsize:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} MUST BE LESS THAN {minsize}"
)
def val_size(self, value: str):
length = self.kwargs.get("size")
if length is None:
raise ValueError("LEN NOT DEFINED")
if len(value) > length:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} THE LIMIT OF CHACRTERS IS {length}"
)
def val_email(self, value: str):
regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
if not re.fullmatch(regex, value):
raise ValueError("MAIL NOT VALID")
def val_number(self, value: str):
regex = "[0-9]+"
if not re.match(regex, value):
raise ValueError("STRING NUMBER NOT VALID")
def val_one_of(self, value: str):
posible_values = self.kwargs.get("posible_values")
if posible_values is None:
raise ValueError("POSIBLE VALUES NOT DEFINED")
if not issubclass(posible_values, Enum):
raise TypeError(f"POSIBLE VALUES MUST BE TYPE ENUM NOT {type(value)}!")
values = [e.value for e in posible_values]
if value not in values:
raise ValueError(
f"VALUE IS NOT VALID FOR {self.name!r} IS NOT PRESENT IN ARRAY {posible_values}"
)
@dataclass
class Person():
personid: str = Descriptor((str), ["SIZE"], size=8)