Правильный подход к проверке атрибутов экземпляра класса

Имея простой класс 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(описание, значение)?
5. проверить данные по декларации сеттеров?
6. и т. Д.

Не могли бы вы порекомендовать хорошо разработанный /Pythonic/ не многословный (на уроке со многими атрибутами)/ элегантный подход?

6 ответов

Решение

Вы можете использовать свойства 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

Если вы хотите проверять значения только тогда, когда объект создан И передача недопустимых значений считается ошибкой программирования, тогда я бы использовал утверждения:

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

Это примерно так же кратко, как вы собираетесь получить, и четко документирует, что это предварительные условия для создания объекта.

Если вы не склонны кататься, вы можете просто использовать http://formencode.org/. Он действительно сияет множеством атрибутов и схем (только схемы подкласса) и имеет множество полезных встроенных валидаторов. Как вы можете видеть, это подход "проверить данные перед созданием объекта спама".

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__ тоже (и сделать их полностью прозрачными с помощью дескрипторов | декораторов | метаклассов), но я не большой поклонник этого. Мне нравится чистый барьер между пользовательским вводом и внутренними объектами.

Если вы хотите проверить только те значения, которые передаются в конструктор, вы можете сделать:

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

Таким образом, правильный подход зависит от того, чего вы пытаетесь достичь.

Ты можешь попробовать 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 вместо простых старых лямбда-функций, если вы хотите, чтобы сообщения об ошибках были еще более простыми (они будут отображать ошибочное выражение).

Видеть pyfieldsдокументация для подробностей (я кстати автор;))

Я работаю над еще одной валидационной библиотекой — моделями convtools (docs / github).

Видение этой библиотеки:

  • сначала проверка
  • нет неявного приведения типов
  • никаких неявных потерь данных во время приведения типов - например, приведение 10.0 к int нормально, 10.1 - нет
  • если есть экземпляр модели, он действителен.
      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'
"""
Другие вопросы по тегам