Python attrs 包:实例化后的验证器

Python attrs package: validators after instantiation

python 的 attrs 包提供了一种在实例化时验证传递变量的简单方法 (example taken from attrs page):

>>> @attr.s
... class C(object):
...     x = attr.ib(validator=attr.validators.instance_of(int))
>>> C(42)
C(x=42)
>>> C("42")
Traceback (most recent call last):
   ...
TypeError: ("'x' must be <type 'int'> (got '42' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=<instance_of validator for type <type 'int'>>, type=None), <type 'int'>, '42')

正如抛出的异常所证明的那样,这很好用。但是,当我在实例化后更改 x 的值时,没有抛出异常:

c = C(30)
c.x = '30'

对于静态对象,这种行为可能没问题,但假设对象是静态的对我来说似乎非常危险。是否有一种变通方法可以使验证器具有在实例化后也可以工作的属性?

根据discussion in thread for attrs

So the original implementation of the validation code actually also ran the validators on assignment.

I’ve removed it before merging because in dynamic languages like Python there’s just too many ways to circumvent it and I personally prefer to not mutate my objects anyways. Hence attrs rich support for freezing classes and creating new instances with changed attributes (assoc).

Of course you can add such a feature yourself by implementing a setattr method that calls your validators whenever you try to set an attribute.

一种保持可变性的方法是这样的:

@attr.s
class C(object):

    _x = attr.ib(validator=attr.validators.instance_of(int))

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        assert isinstance(value, int), repr(value)  # or whatever error you want
        self._x = value

但即使这样也不能安全地对抗 c._x = '30'

问题不在于 attrs,而在于 python。当 a.b 只是一个变量时,a.b = c 总是有效。这是由于 python 的 "we're all consenting adults here" 概念——即一切都是 public,一切都是可修改的。如果您编辑了不该编辑的内容,那是您的错。

话虽这么说,attrs 确实提供了一种 hack 来防止属性分配给人一种不可变的错觉:

@attr.s(frozen=True)
class C(object):

    x = attr.ib(validator=attr.validators.instance_of(int))


 c = C(1)
 c.x = 30  # raises FrozenInstanceError

version 20.1.0 他们添加了 on_setattr:

A callable that is run whenever the user attempts to set an attribute (either by assignment like i.x = 42 or by using setattr like setattr(i, "x", 42)). It receives the same arguments as validators: the instance, the attribute that is being modified, and the new value.

因此,添加:

import attr

@attr.s
class C(object):
    x = attr.ib(
        validator=attr.validators.instance_of(int),
        on_setattr = attr.setters.validate,  # new in 20.1.0
    )

产量

C(42)
# C(x=42)
C("42")
#  TypeError: ("'x' must be <class 'int'> 

此外,特别是对于您的示例中的字符串输入,您可能会发现 attrs 转换器很方便。例如要自动转换:

@attr.s
class CWithConvert(object):
    x = attr.ib(
        converter=int,
        validator=attr.validators.instance_of(int),
        on_setattr = attr.setters.validate,
    )

CWithConvert(42)
# CWithConvert(x=42)
CWithConvert("42")
# CWithConvert(x=42)  # converted!
CWithConvert([42])
# TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'

小心:

CWithConvert(0.8)
# CWithConvert(x=0)  # float to int!