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


83

Наявність такого простого класу 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.isValid (опис, значення)?
5. перевірити дані про декларацію установників?
6. тощо

Чи можете ви порекомендувати добре розроблений / пітонічний / не багатослівний (у класі з багатьма атрибутами) / елегантний підхід?

Відповіді:


105

Ви можете використовувати властивості 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), як слід.
Marcelo Cantos

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, наприклад 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

Отже, правильний підхід залежить від того, що ви намагаєтесь досягти.


це мій фактичний підхід; але мені не подобається, коли перевірка атрибутів number і \ або обмежень є більш складною. Здається, це занадто захаращує метод init.
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документацію для деталей (я, до речі, автор;))

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.