【问题标题】:Validating input when mutating a dataclass改变数据类时验证输入
【发布时间】:2019-06-26 13:51:49
【问题描述】:

在 Python 3.7 中有这些新的“数据类”容器,它们基本上类似于可变的命名元组。假设我创建了一个代表一个人的数据类。我可以像这样通过__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)

从而绕过输入验证。

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

【问题讨论】:

  • 使用@dataclass(frozen=True) 使其“不可变”
  • @juanpa.arrivillaga 这首先会破坏使用数据类的目的。如果我想要一个不可变的数据容器,我只会使用一个命名元组。我打算在初始化变量后的某个时间更新字段。
  • 好吧,namedtuples 是元组,@dataclass 只是一个装饰器,让您可以跳过编写大量样板来创建经常遇到的类,它不是只是“一个可变的命名元组”。但是我想,您将不得不将您的属性隐藏在property 或其他东西后面,但这会消除数据类的一些优点

标签: python validation python-dataclasses


【解决方案1】:

也许使用getters and setters 锁定属性,而不是直接改变属性。如果您随后将验证逻辑提取到单独的方法中,则可以从您的 setter 和 __post_init__ 函数中以相同的方式进行验证。

【讨论】:

    【解决方案2】:

    Dataclasses 是一种提供默认初始化以接受属性作为参数的机制,以及一个很好的表示,加上一些细节,如 __post_init__ 钩子。

    幸运的是,它们不会与 Python 中的任何其他属性访问机制混淆——而且您仍然可以将您的 dataclassess 属性创建为property 描述符,或者如果需要,可以创建自定义描述符类。这样,任何属性访问都将自动通过您的 getter 和 setter 函数。

    使用默认的 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__ 本身的代码可以更改为内省owner 类中的__annotations__ 以自动记录类型 - 这样MyAttr 类本身就不需要类型参数。但正如我之前所说:您可以根据需要将其设置为复杂的。

    【讨论】:

    • 请注意,使用property 使其表现为具有默认值的字段,即它不能在没有默认值的字段之前使用(“TypeError:非默认参数'second'跟随默认参数) . 如果设置了字段/属性,我最终在包装的数据类上使用__setattr__ 使某些缓存无效。
    • 是的 - 数据类会将带有描述符的任何字段视为具有“默认值” - 改变它的唯一方法是在 @dataclass 装饰器之后分配描述符已经运行 - 这将需要另一个装饰器,以及注释描述符本身的方法。
    • @jsbueno,我才刚刚开始理解 Python 类,但我是否正确地指出,在您的第一个示例中,"name" 应该在 self.__dict__.get(name) 中引用?
    • 是的 - 它应该被引用。我现在正在修。
    • 与其显式创建您自己的描述符类,不如使用内置的property() 函数创建一个可能更简单——类似于@987654345 @ 函数显示在我的这个answer 中。
    【解决方案3】:

    一个简单灵活的解决方案是覆盖__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
    

    【讨论】:

    • 虽然有效,但不可扩展。最好使用更元/内省的东西。
    • @Rebs 为什么不可扩展?
    猜你喜欢
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-20
    • 2020-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-13
    相关资源
    最近更新 更多