Python:更改数据类时验证输入

dai*_*ain 4 python validation python-dataclasses

在Python 3.7中,这些新的“数据类”容器基本上类似于可变的namedtuple。假设我制作了一个要代表一个人的数据类。我可以通过如下__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.")
Run Code Online (Sandbox Code Playgroud)

这样可以通过以下方式提供良好的输入:

someone = Person(name="John Doe", age=30)
print(someone)

Person(name='John Doe', age=30.0)
Run Code Online (Sandbox Code Playgroud)

尽管所有这些错误的输入都会引发错误:

someone = Person(name=["John Doe"], age=30)
someone = Person(name="John Doe", age="thirty")
someone = Person(name="John Doe", age=-30)
Run Code Online (Sandbox Code Playgroud)

但是,由于数据类是可变的,所以我可以这样做:

someone = Person(name="John Doe", age=30)
someone.age = -30
print(someone)

Person(name='John Doe', age=-30)
Run Code Online (Sandbox Code Playgroud)

从而绕过输入验证。

因此,初始化,确保数据类的字段不会突变为坏东西的最佳方法是什么?

cre*_*esk 7

一个简单而灵活的解决方案可以是重写该__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
Run Code Online (Sandbox Code Playgroud)

  • @Rebs 为什么它不可扩展? (5认同)

jsb*_*eno 5

数据类是一种提供默认初始化以接受属性作为参数的机制,以及一个漂亮的表示形式以及一些漂亮的东西,例如__post_init__钩子。

值得一提的是,它们不会与Python中的其他任何属性访问机制发生冲突-而且您仍然可以将dataclassess属性创建为property描述符,或者根据需要创建自定义描述符类。这样,任何属性访问都将自动通过您的getter和setter函数。

使用默认property内置函数的唯一缺点是,您必须以“旧方式”使用它,而不是使用装饰器语法-允许您为属性创建注释。

所以,“描述”是一种方式,该属性的任何访问将调用描述符分配给Python类属性的特殊对象__get____set____del__方法。在property内置的是要建立一个描述符通过1至3个功能taht将这些方法被称为convenince。

因此,没有自定义描述符,您可以执行以下操作:

@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
Run Code Online (Sandbox Code Playgroud)

通过使用这种方法,您将必须将每个属性的访问权限编写为两个方法/函数,但不再需要编写your __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,])
Run Code Online (Sandbox Code Playgroud)

就是这样-创建自己的描述符类需要更多有关Python的知识,但是上面给出的代码即使在生产中也应很好用-欢迎使用它。

请注意,您可以轻松地为每个属性添加很多其他检查和转换-并且__set_name__可以更改代码本身__annotations__以对owner类中的int 进行检查以自动记录类型-从而不需要type参数对于MyAttr 班级本身。但是正如我之前所说:您可以根据需要使它变得复杂。