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

84

Имея простой класс 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. и т. Д.

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

systempuntoout
источник

Ответы:

106

Вы можете использовать свойства 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
2
Согласен, это не самое красивое решение. Python предпочитает классы со свободным выгулом (подумайте о цыплятах), и идея свойств, управляющих доступом, была немного запоздалой. Сказав это, это не было бы намного более кратким на любом другом языке, который я могу придумать.
Марсело Кантос,
2
@MarceloCantos Я понимаю, что это старый вопрос, но на основе документации (хотя и для Python 3) следуетself.description = description использовать подчеркивание, т.self._description = description Или это не имеет значения? Это необходимо или просто что-то похожее на версию «частных» переменных в Python?
Джон Бенсин,
12
@JohnBensin: Да и нет. self.description = …назначается через свойство, тогда как self._description = …назначается непосредственно базовому полю. Какой из них использовать во время строительства - это выбор дизайна, но обычно безопаснее всегда назначать через свойство. Например, приведенный выше код вызовет исключение, если вы вызовете Spam('', 1), как и должно быть.
Марсело Кантос,
1
Не думаю, что это слишком многословно. Альтернативой является возможность установить недопустимые значения.
Тони Эннис,
10

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

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

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

Дэйв Кирби
источник
спасибо, Дэйв, используя assert, как мне указать клиенту этого класса, что пошло не так (описание или значение)? Вам не кажется, что утверждение следует использовать только для проверки условий, которых никогда не должно быть?
systempuntoout
1
Вы можете добавить сообщение к утверждению, например 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

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

Тихий призрак
источник
это мой настоящий подход; но мне не нравится, когда увеличивается число атрибутов и \ или проверки ограничений более сложные. Кажется, это слишком загромождает метод инициализации.
systempuntoout
1
@system: вы можете разделить проверку действительности на отдельный метод: в этой ситуации нет жестких правил.
SilentGhost
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вместо простых старых лямбда-функций, если хотите, чтобы сообщения об ошибках были еще более понятными (они будут отображать ошибочное выражение).

Подробности смотрите в pyfieldsдокументации (я, кстати, автор;))

умница
источник