14

在 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)

从而绕过输入验证。

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

4

3 回答 3

23

数据类是一种提供默认初始化以接受属性作为参数的机制,以及一个很好的表示形式,以及像__post_init__钩子这样的一些细节。

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

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

因此,“描述符”是分配给 Python 中类属性的特殊对象,任何对该属性的访问都将调用描述符__get____set__方法__del__。内置函数可以方便地构建传递 1 到 3 个函数的描述符,这些property函数将从这些方法中调用。

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

@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 班级本身。但正如我之前所说:您可以根据需要将其设置为复杂的。

于 2019-02-02T03:05:36.147 回答
4

一个简单而灵活的解决方案可以是覆盖该__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
于 2020-10-19T23:56:07.017 回答
1

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

于 2019-02-02T00:25:07.817 回答