Правильный подход к проверке атрибутов экземпляра класса
Наличие простого класса 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 и вызовите его с помощью Spam.isValid(description, value)?
5. проверить данные об объявлении сеттеров?
6. и т.д.
Не могли бы вы порекомендовать хорошо спроектированный /Pythonic/not verbose (по классу со многими атрибутами)/элегантный подход?
Ответы
Ответ 1
Вы можете использовать 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
Ответ 2
Если вы хотите проверять значения только тогда, когда объект создан И передача недопустимых значений считается ошибкой программирования, я бы использовал утверждения:
class Spam(object):
def __init__(self, description, value):
assert description != ""
assert value > 0
self.description = description
self.value = value
Это примерно так кратко, как вы собираетесь получить, и четко документирует, что это предварительные условия для создания объекта.
Ответ 3
Если вы не умеете кататься самостоятельно, вы можете просто использовать 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__
(и сделать их полностью прозрачными с помощью дескрипторов | decorators | metaclass), но я не большой поклонник этого. Мне нравится чистый барьер между пользовательским вводом и внутренними объектами.
Ответ 4
если вы хотите только проверить эти значения, переданные конструктору, вы можете сделать:
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
Таким образом, правильный подход зависит от того, что вы пытаетесь выполнить.
Ответ 5
Вы можете попробовать pyfields
:
from pyfields import field
class Spam:
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 [pyfields.tests.test_so.Spam.description=''].
InvalidValue: description can not be empty. Function [<lambda>] returned [False] for value ''.
Он соответствует Python 2 и 3.5 (в отличие от pydantic
), и проверка происходит каждый раз, когда изменяется значение (не только в первый раз, в отличие от attrs
). Он может создать конструктор для вас, но не делает это по умолчанию, как показано выше.
Подробности смотрите в pyfields
документации (кстати, я автор;))