Correct approach to validate attributes of an instance of class

只愿长相守 提交于 2019-12-17 17:24:03

问题


Having a simple Python class like this:

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

I would like to check the following constraints:

  • "description cannot be empty"
  • "value must be greater than zero"

Should I:
1. validate data before creating spam object ?
2. check data on __init__ method ?
3. create an is_valid method on Spam class and call it with spam.isValid() ?
4. create an is_valid static method on Spam class and call it with Spam.isValid(description, value) ?
5. check data on setters declaration ?
6. etc.

Could you recommend a well designed/Pythonic/not verbose (on class with many attributes)/elegant approach?


回答1:


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



回答2:


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, value):
        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.




回答3:


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.




回答4:


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.




回答5:


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.

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



来源:https://stackoverflow.com/questions/2825452/correct-approach-to-validate-attributes-of-an-instance-of-class

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!