Prawidłowe podejście do walidacji atrybutów instancji klasy

84

Posiadanie prostej klasy Pythona, takiej jak ta:

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

Chciałbym sprawdzić następujące ograniczenia:

  • „opis nie może być pusty”
  • „wartość musi być większa od zera”

Czy powinienem:
1. zweryfikować dane przed utworzeniem obiektu spamu?
2. sprawdzić dane dotyczące __init__metody?
3. utwórz is_validmetodę w klasie Spam i wywołaj ją za pomocą spam.isValid ()?
4. utworzyć is_validstatyczną metodę na klasie Spam i wywołać ją za pomocą Spam.isValid (opis, wartość)?
5. sprawdzić dane na deklaracji ustawiających?
6. itp.

Czy mógłbyś polecić dobrze zaprojektowane / Pythonic / nierozpoznane (w klasie z wieloma atrybutami) / eleganckie podejście?

systempuntoout
źródło

Odpowiedzi:

106

Możesz użyć właściwości Pythona , aby czysto zastosować reguły do ​​każdego pola osobno i wymusić je nawet wtedy, gdy kod klienta próbuje zmienić pole:

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

Wyjątek zostanie zgłoszony przy każdej próbie naruszenia reguł, nawet w __init__funkcji, w którym to przypadku konstrukcja obiektu zakończy się niepowodzeniem.

AKTUALIZACJA: Pomiędzy 2010 a teraz dowiedziałem się o 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
Marcelo Cantos
źródło
1
+1 eleganckie rozwiązanie dzięki, nie sądzisz, że jest to trochę rozwlekłe dla takiej małej klasy?
systempuntoout
2
Zgoda, to nie jest najładniejsze rozwiązanie. Python preferuje zajęcia z wolnego wybiegu (pomyśl o kurczakach), a pomysł właściwości kontrolujących dostęp był trochę późniejszy. Powiedziawszy to, nie byłoby to dużo bardziej zwięzłe w żadnym innym języku, jaki przychodzi mi do głowy.
Marcelo Cantos
2
@MarceloCantos Zdaję sobie sprawę, że to stare pytanie, ale na podstawie dokumentacji (choć dla Pythona 3) należy self.description = descriptionużyć podkreślenia, czyli self._description = descriptionczy to nie ma znaczenia? Czy jest to konieczne, czy po prostu coś podobnego do wersji „prywatnych” zmiennych w Pythonie?
John Bensin,
12
@JohnBensin: Tak i nie. self.description = …przypisuje przez właściwość, podczas gdy self._description = …przypisuje bezpośrednio do pola bazowego. Który z nich należy użyć podczas budowy, jest wyborem projektu, ale zwykle bezpieczniej jest zawsze przypisywać go za pośrednictwem nieruchomości. Na przykład powyższy kod zgłosi wyjątek, jeśli zadzwonisz Spam('', 1), tak jak powinien.
Marcelo Cantos
1
Nie sądzę, by było to zbyt szczegółowe. Alternatywą jest umożliwienie ustawienia wartości na nieprawidłowe wartości.
Tony Ennis
10

Jeśli chcesz zweryfikować wartości tylko wtedy, gdy obiekt jest tworzony ORAZ przekazywanie nieprawidłowych wartości jest uważane za błąd programowania, użyłbym asercji:

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

Jest to mniej więcej tak zwięzłe, jak to tylko możliwe, i jasno dokumentuje, że są to warunki wstępne do utworzenia obiektu.

Dave Kirby
źródło
dzięki Dave, używając assert, w jaki sposób mam wskazać klientowi tej klasy, co poszło nie tak (opis lub wartość)? Czy nie uważasz, że asercja powinna być używana tylko do testowania warunków, które nigdy nie powinny się zdarzyć?
systempuntoout
1
Możesz dodać wiadomość do instrukcji assert, np assert value > 0, "value attribute to Spam must be greater than zero". Asercje są tak naprawdę komunikatami dla programisty i nie powinny być przechwytywane przez kod klienta, ponieważ wskazują na błąd programowania. Jeśli chcesz, aby klient wychwycił i obsłużył błąd, jawnie zgłoś wyjątek, taki jak ValueError, jak pokazano w innych odpowiedziach.
Dave Kirby
1
Odpowiadając na drugie pytanie, twierdzenia tak powinny być używane do testowania warunków, które nigdy nie powinny się zdarzyć - dlatego powiedziałem „jeśli przekazanie niepoprawnych wartości jest uważane za błąd w programowaniu ...”. Jeśli tak nie jest, nie używaj potwierdzeń.
Dave Kirby
def należy wstawić przed__init__
datapug
1
Dzięki @datapug, naprawiłem literówkę.
Dave Kirby
7

Jeśli nie masz ochoty na własne, możesz po prostu użyć formencode . Naprawdę wyróżnia się wieloma atrybutami i schematami (tylko schematami podklas) i ma wiele wbudowanych przydatnych walidatorów. Jak widać, jest to podejście polegające na „sprawdzaniu danych przed utworzeniem obiektu spamu”.

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) 

Możesz też sprawdzić w trakcie __init__(i uczynić je całkowicie przezroczystymi za pomocą deskryptorów | dekoratorów | metaklasy), ale nie jestem tego wielkim fanem. Podoba mi się czysta bariera między danymi wprowadzanymi przez użytkownika a obiektami wewnętrznymi.

Jochen Ritzel
źródło
6

jeśli chcesz zweryfikować tylko wartości przekazane do konstruktora, możesz zrobić:

class Spam(object):
    def __init__(self, description, value):
        if not description or value <=0:
            raise ValueError
        self.description = description
        self.value = value

To oczywiście nie powstrzyma nikogo przed zrobieniem czegoś takiego:

>>> s = Spam('s', 5)
>>> s.value = 0
>>> s.value
0

Zatem prawidłowe podejście zależy od tego, co próbujesz osiągnąć.

SilentGhost
źródło
to jest moje rzeczywiste podejście; ale nie podoba mi się, gdy zwiększa się liczba atrybutów i \ lub kontrole ograniczeń są bardziej wyrafinowane. Wydaje się, że zbytnio zaśmieca to metodę init.
systempuntoout
1
@system: możesz oddzielić sprawdzanie poprawności do własnej metody: nie ma sztywnych i szybkich reguł dotyczących tej sytuacji.
SilentGhost
1

Możesz spróbować 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

Poddaje się

ValidationError[ValueError]: Error validating [<...>.Spam.description=''].
  InvalidValue: description can not be empty. 
  Function [<lambda>] returned [False] for value ''.

Jest zgodny z Pythonem 2 i 3.5 (w przeciwieństwie do pydantic), a walidacja ma miejsce za każdym razem, gdy wartość jest zmieniana (nie tylko za pierwszym razem, a nie za attrs). Może utworzyć konstruktora dla Ciebie, ale nie robi tego domyślnie, jak pokazano powyżej.

Zauważ, że możesz opcjonalnie użyć mini-lambdazamiast zwykłych starych funkcji lambda, jeśli chcesz, aby komunikaty o błędach były jeszcze prostsze (wyświetlą one nieprawidłowe wyrażenie).

Zobacz pyfieldsdokumentację po szczegóły (swoją drogą jestem autorem;))

smarie
źródło