验证类实例属性的正确方法


83

有一个像这样的简单Python类:

class Spam(object):
    __init__(self, description, value):
        self.description = description
        self.value = value

我想检查以下约束:

  • “说明不能为空”
  • “值必须大于零”

我应该:
1.在创建垃圾邮件对象之前验证数据吗?
2.检查__init__方法上的数据?
3.is_valid在Spam类上创建一个方法,并使用spam.isValid()进行调用?
4.is_valid在Spam类上创建一个静态方法,并使用Spam.isValid(description,value)进行调用?
5.检查二传手声明中的数据?
6.等

您能推荐一个设计良好/ Pythonic /不冗长(在具有许多属性的类上)/优雅的方法吗?

Answers:


105

您可以使用Python属性将规则明确地分别应用于每个字段,并且即使客户端代码尝试更改该字段也可以强制执行这些规则:

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, d):
        if not d: raise Exception("description cannot be empty")
        self._description = d

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        if not (v > 0): raise Exception("value must be greater than zero")
        self._value = v

任何违反规则的尝试都将引发异常,即使是在__init__函数中,在这种情况下,对象构造也会失败。

更新:从2010年到现在,我了解到operator.attrgetter

import operator

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

    description = property(operator.attrgetter('_description'))

    @description.setter
    def description(self, d):
        if not d: raise Exception("description cannot be empty")
        self._description = d

    value = property(operator.attrgetter('_value'))

    @value.setter
    def value(self, v):
        if not (v > 0): raise Exception("value must be greater than zero")
        self._value = v

1
+1优雅的解决方案谢谢,您不认为这样的小班教学有点冗长吗?
systempuntoout 2010年

2
同意,这不是最漂亮的解决方案。Python更喜欢自由范围类(例如鸡),而控制访问的属性的想法是事后的想法。话虽如此,这在我能想到的任何其他语言中都不会更加简洁。
马塞洛·坎托斯

2
@MarceloCantos我意识到这是一个古老的问题,但是基于文档(尽管适用于Python 3),应该self.description = description使用下划线,即self._description = description,还是没关系?这是必需的还是仅仅是类似于Python的“私有”变量的版本?
John Bensin

12
@JohnBensin:是和否。self.description = …通过属性分配,而self._description = …直接分配给基础字段。在设计过程中使用哪个选项是设计选择,但始终通过属性分配通常更安全。例如,如果您调用Spam('', 1),上面的代码将引发异常。
Marcelo Cantos

1
我认为这不太冗长。替代方法是可以将值设置为无效值。
Tony Ennis

10

如果只想在创建对象时验证值,而将无效值传递视为编程错误,则可以使用断言:

class Spam(object):
    def __init__(self, description, value):
        assert description != ""
        assert value > 0
        self.description = description
        self.value = value

这与您将要获得的内容一样简洁,并且清楚地记录了这些是创建对象的前提。


谢谢戴夫,使用断言,我如何向该类的客户端指定出了什么问题(描述或值)?您不认为断言应该仅用于测试永远不会发生的条件吗?
systempuntoout 2010年

1
您可以在assert语句中添加一条消息,例如assert value > 0, "value attribute to Spam must be greater than zero"。断言实际上是发给开发人员的消息,不应被客户端代码捕获,因为它们表示编程错误。如果您希望客户端捕获并处理错误,请显式引发一个异常,例如ValueError,如其他答案所示。
戴夫·柯比

1
要回答您的第二个问题,应使用yes断言来测试永远不会发生的条件-这就是为什么我说“如果传递无效值被认为是编程错误...”。如果不是这种情况,请不要使用断言。
戴夫·柯比

def 应该在__init__
datapug

1
感谢@datapug,修复了打字错误。
戴夫·柯比

7

除非您习惯自己动手制作,否则只需使用formencode即可。它确实具有许多属性和模式(只是子类模式),并且内置了许多有用的验证器。如您所见,这是“在创建垃圾邮件对象之前验证数据”的方法。

from formencode import Schema, validators

class SpamSchema(Schema):
    description = validators.String(not_empty=True)
    value = validators.Int(min=0)

class Spam(object):
    def __init__(self, description, value):
        self.description = description
        self.value = value

## how you actually validate depends on your application
def validate_input( cls, schema, **input):
    data = schema.to_python(input) # validate `input` dict with the schema
    return cls(**data) # it validated here, else there was an exception

# returns a Spam object
validate_input( Spam, SpamSchema, description='this works', value=5) 

# raises an exception with all the invalid fields
validate_input( Spam, SpamSchema, description='', value=-1) 

您也可以在此期间进行检查__init__(并使用描述符|装饰符|元类使它们完全透明),但是我并不喜欢这样做。我喜欢在用户输入和内部对象之间建立一个清晰的屏障。


6

如果您只想验证传递给构造函数的值,则可以执行以下操作:

class Spam(object):
    def __init__(self, description, value):
        if not description or value <=0:
            raise ValueError
        self.description = description
        self.value = value

这当然不会阻止任何人这样做:

>>> s = Spam('s', 5)
>>> s.value = 0
>>> s.value
0

因此,正确的方法取决于您要完成的工作。


这是我的实际做法;但是当属性数量提高和/或约束检查更加复杂时,我不喜欢它.init方法似乎过于混乱。
systempuntoout 2010年

1
@system:您可以将有效性检查分成自己的方法:关于这种情况,没有严格的规定。
SilentGhost 2010年

1

您可以尝试pyfields

from pyfields import field

class Spam(object):
    description = field(validators={"description can not be empty": lambda s: len(s) > 0})
    value = field(validators={"value must be greater than zero": lambda x: x > 0})

s = Spam()
s.description = "hello"
s.description = ""  # <-- raises error, see below

它产生

ValidationError[ValueError]: Error validating [<...>.Spam.description=''].
  InvalidValue: description can not be empty. 
  Function [<lambda>] returned [False] for value ''.

它与python 2和3.5兼容(与相对pydantic),并且每次值更改时都会进行验证(与相比,不仅是第一次attrs)。它可以为您创建构造函数,但默认情况下不这样做,如上所示。

请注意,mini-lambda如果您希望错误消息更加简单明了(它们将显示失败的表达式),则可能希望选择使用而不是普通的lambda函数。

看到 pyfields详细信息,文档(顺便说一句,我是作者;))

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.