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

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

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

我想检查下列限制因素:

  • “描述不能是空的”
  • “价值必须大于零”

我应该:
1. 在创建垃圾邮件对象之前验证数据?
2. __init__方法检查数据?
3. 在垃圾邮件类上创建一个 is_valid方法并使用 Spam.isValid ()调用它?
4. 在垃圾邮件类上创建一个 is_valid静态方法,并用 Spam.isValid (描述,值)调用它?
5. 检查 setter 声明的数据?
6等。

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

54129 次浏览

You can use Python properties to cleanly apply rules to each field separately, and enforce them even when client code tries to change the field:

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

An exception will be thrown on any attempt to violate the rules, even in the __init__ function, in which case object construction will fail.

UPDATE: Sometime between 2010 and now, I learned about 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

if you want to only validate those values passed to the constructor, you could do:

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

This will of course will not prevent anyone from doing something like this:

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

So, correct approach depends on what you're trying to accomplish.

If you only want to validate the values when the object is created AND passing in invalid values is considered a programming error then I would use assertions:

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

This is about as concise as you are going to get, and clearly documents that these are preconditions for creating the object.

Unless you're hellbent on rolling your own, you can simply use formencode. It really shines with many attributes and schemas (just subclass schemas) and has a lot of useful validators builtin. As you can see this is the "validate data before creating spam object" approach.

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)

You could do the checks during __init__ too (and make them completely transparent with descriptors|decorators|metaclass), but I'm not a big fan of that. I like a clean barrier between user input and internal objects.

You can try 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

It yields

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

It is compliant with python 2 and 3.5 (as opposed to pydantic), and validation happens everytime the value is changed (not only the first time, as opposed to attrs). It can create the constructor for you, but does not do it by default as shown above.

Note that you may wish to optionally use mini-lambda instead of plain old lambda functions if you wish the error messages to be even more straightforward (they will display the failing expression).

See pyfields documentation for details (I'm the author by the way ;) )

I'm working on yet another validation library - convtools models (docs / github).

The vision of this library is:

  • validation first
  • no implicit type casting
  • no implicit data losses during type casting - e.g. casting 10.0 to int is fine, 10.1 is not
  • if there’s a model instance, it is valid.
from collections import namedtuple
from typing import Union


from convtools.contrib.models import ObjectModel, build, validate, validators


# input data to test
SpamTest = namedtuple("SpamTest", ["description", "value"])




class Spam(ObjectModel):
description: str = validate(validators.Length(min_length=1))
value: Union[int, float] = validate(validators.Gt(0))




spam, errors = build(Spam, SpamTest("", 0))
"""
>>> In [34]: errors
>>> Out[34]:
>>> {'description': {'__ERRORS': {'min_length': 'length is 0, but should be >= 1'}},
>>>  'value': {'__ERRORS': {'gt': 'should be > 0'}}
"""




spam, errors = build(Spam, SpamTest("foo", 1))
"""
>>> In [42]: spam
>>> Out[42]: Spam(description='foo', value=1)
>>> In [43]: spam.to_dict()
>>> Out[43]: {'description': 'foo', 'value': 1}
>>> In [44]: spam.description
>>> Out[44]: 'foo'
"""