改变数据类时验证输入
Validating input when mutating a dataclass
在 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)
从而绕过输入验证。
那么,在 初始化之后,确保数据类的字段不会突变为不良内容的最佳方法是什么?
也许使用 getters and setters 锁定属性而不是直接改变属性。如果您随后将验证逻辑提取到单独的方法中,则可以从 setter 和 __post_init__
函数中以相同的方式进行验证。
Dataclasses 是一种提供默认初始化以接受属性作为参数的机制,以及一个很好的表示,加上一些细节,如 __post_init__
钩子。
幸运的是,它们不会与 Python 中的任何其他属性访问机制混淆 - 您仍然可以将数据 classess 属性创建为 property
描述符,或者如果需要,自定义描述符 class。这样,任何属性访问都将自动通过您的 getter 和 setter 函数。
使用默认 property
内置的唯一缺点是您必须在 "old way" 中使用它,而不是使用装饰器语法 - 这允许您为您的创建注释属性。
因此,"descriptors" 是分配给 Python 中的 class 属性的特殊对象,任何对该属性的访问都会调用描述符 __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
通过使用这种方法,您必须将每个属性的访问权限写为两个 methods/functions,但不再需要编写您的 __post_init__
:每个属性都会自行验证。
另请注意,此示例采用了将属性正常存储在实例 __dict__
中的小常用方法。在网络上的示例中,实践是使用正常的属性访问,但在名称前加上 _
。这将使这些属性在您的最终实例上污染 dir
,并且私有属性将不受保护。
另一种方法是编写您自己的描述符class,并让它检查您要保护的属性的实例和其他属性。这可以根据您的需要进行复杂化,最终以您自己的框架结束。因此,对于将检查属性类型并接受验证器列表的描述符 class,您将需要:
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,])
就是这样 - 创建你自己的描述符 class 需要更多关于 Python 的知识,但上面给出的代码应该很好用,即使在生产中 - 欢迎你使用它。
请注意,您可以轻松地为每个属性添加许多其他检查和转换 -
__set_name__
中的代码本身可以更改为自省 owner
class 中的 __annotations__
以自动记录类型 - 这样就不需要类型参数对于 MyAttr
class 本身。但正如我之前所说:您可以根据需要将其复杂化。
一个简单而灵活的解决方案可以是覆盖__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
provided by @jsbueno 很棒,但它不允许默认参数。我将其扩展为允许默认值:
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
在 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)
从而绕过输入验证。
那么,在 初始化之后,确保数据类的字段不会突变为不良内容的最佳方法是什么?
也许使用 getters and setters 锁定属性而不是直接改变属性。如果您随后将验证逻辑提取到单独的方法中,则可以从 setter 和 __post_init__
函数中以相同的方式进行验证。
Dataclasses 是一种提供默认初始化以接受属性作为参数的机制,以及一个很好的表示,加上一些细节,如 __post_init__
钩子。
幸运的是,它们不会与 Python 中的任何其他属性访问机制混淆 - 您仍然可以将数据 classess 属性创建为 property
描述符,或者如果需要,自定义描述符 class。这样,任何属性访问都将自动通过您的 getter 和 setter 函数。
使用默认 property
内置的唯一缺点是您必须在 "old way" 中使用它,而不是使用装饰器语法 - 这允许您为您的创建注释属性。
因此,"descriptors" 是分配给 Python 中的 class 属性的特殊对象,任何对该属性的访问都会调用描述符 __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
通过使用这种方法,您必须将每个属性的访问权限写为两个 methods/functions,但不再需要编写您的 __post_init__
:每个属性都会自行验证。
另请注意,此示例采用了将属性正常存储在实例 __dict__
中的小常用方法。在网络上的示例中,实践是使用正常的属性访问,但在名称前加上 _
。这将使这些属性在您的最终实例上污染 dir
,并且私有属性将不受保护。
另一种方法是编写您自己的描述符class,并让它检查您要保护的属性的实例和其他属性。这可以根据您的需要进行复杂化,最终以您自己的框架结束。因此,对于将检查属性类型并接受验证器列表的描述符 class,您将需要:
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,])
就是这样 - 创建你自己的描述符 class 需要更多关于 Python 的知识,但上面给出的代码应该很好用,即使在生产中 - 欢迎你使用它。
请注意,您可以轻松地为每个属性添加许多其他检查和转换 -
__set_name__
中的代码本身可以更改为自省 owner
class 中的 __annotations__
以自动记录类型 - 这样就不需要类型参数对于 MyAttr
class 本身。但正如我之前所说:您可以根据需要将其复杂化。
一个简单而灵活的解决方案可以是覆盖__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
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