Richtiger Ansatz zum Überprüfen der Attribute einer Klasseninstanz

83

Eine einfache Python-Klasse wie diese haben:

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

Ich möchte die folgenden Einschränkungen überprüfen:

  • "Beschreibung darf nicht leer sein"
  • "Wert muss größer als Null sein"

Sollte ich:
1. Daten validieren, bevor ich ein Spam-Objekt erstelle?
2. Daten zur __init__Methode prüfen ?
3. Erstellen Sie eine is_validMethode für die Spam-Klasse und rufen Sie sie mit spam.isValid () auf.
4. Erstellen Sie eine is_validstatische Methode für die Spam-Klasse und rufen Sie sie mit Spam.isValid auf (Beschreibung, Wert).
5. Daten zur Setterdeklaration prüfen?
6. etc.

Könnten Sie einen gut gestalteten / pythonischen / nicht ausführlichen (auf Klasse mit vielen Attributen) / eleganten Ansatz empfehlen?

systempuntoout
quelle

Antworten:

105

Sie können Python- Eigenschaften verwenden , um Regeln separat auf jedes Feld anzuwenden und sie auch dann durchzusetzen, wenn der Clientcode versucht, das Feld zu ändern:

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

Bei jedem Versuch, die Regeln zu verletzen, wird eine Ausnahme ausgelöst, selbst in der __init__Funktion. In diesem Fall schlägt die Objektkonstruktion fehl.

UPDATE: Irgendwann zwischen 2010 und jetzt habe ich gelernt über 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
quelle
1
+1 elegante Lösung danke, denkst du nicht, dass es für eine kleine Klasse wie diese ein bisschen ausführlich ist?
Systempuntoout
2
Einverstanden, es ist nicht die schönste Lösung. Python bevorzugt Freilandklassen (denken Sie an Hühner), und die Idee, dass Eigenschaften den Zugriff steuern, war ein wenig nachträglicher Gedanke. Allerdings wäre dies in keiner anderen Sprache, die mir einfällt, viel prägnanter.
Marcelo Cantos
2
@MarceloCantos Mir ist klar, dass dies eine alte Frage ist, aber basierend auf der Dokumentation (wenn auch für Python 3) sollte self.description = descriptionein Unterstrich verwendet werden, dh self._description = descriptionoder spielt das keine Rolle? Ist dies notwendig oder nur etwas Ähnliches wie Pythons Version von "privaten" Variablen?
John Bensin
12
@ JohnBensin: Ja und nein. self.description = …wird durch Eigenschaft zugewiesen, während self._description = …direkt dem zugrunde liegenden Feld zugewiesen wird. Welche während des Baus verwendet werden soll, ist eine Entwurfsentscheidung, aber es ist normalerweise sicherer, immer über die Eigenschaft zuzuweisen. Der obige Code löst beispielsweise eine Ausnahme aus, wenn Sie Spam('', 1)wie gewünscht aufrufen .
Marcelo Cantos
1
Ich denke nicht, dass es zu ausführlich ist. Die Alternative besteht darin, dass der Wert auf ungültige Werte gesetzt werden kann.
Tony Ennis
10

Wenn Sie die Werte nur beim Erstellen des Objekts überprüfen möchten UND die Übergabe ungültiger Werte als Programmierfehler betrachtet wird, würde ich folgende Aussagen verwenden:

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

Dies ist ungefähr so ​​kurz wie möglich und dokumentiert deutlich, dass dies Voraussetzungen für die Erstellung des Objekts sind.

Dave Kirby
quelle
danke Dave, mit assert, wie gebe ich dem Kunden dieser Klasse an, was schief gelaufen ist (Beschreibung oder Wert)? Denken Sie nicht, dass die Behauptung nur dazu verwendet werden sollte, Bedingungen zu testen, die niemals eintreten sollten?
Systempuntoout
1
Sie können der assert-Anweisung eine Nachricht hinzufügen, z assert value > 0, "value attribute to Spam must be greater than zero". Behauptungen sind wirklich Nachrichten an den Entwickler und sollten nicht vom Client-Code abgefangen werden, da sie auf einen Programmierfehler hinweisen. Wenn Sie möchten, dass der Client den Fehler abfängt und behandelt, lösen Sie explizit eine Ausnahme wie ValueError aus, wie in den anderen Antworten gezeigt.
Dave Kirby
1
Um Ihre zweite Frage zu beantworten, sollten Ja-Asserts verwendet werden, um Bedingungen zu testen, die niemals eintreten sollten. Deshalb habe ich gesagt, "wenn die Übergabe ungültiger Werte als Programmierfehler angesehen wird ...". Wenn dies nicht der Fall ist, verwenden Sie keine Asserts.
Dave Kirby
def sollte vor__init__
datapug
1
Danke @datapug, den Tippfehler behoben.
Dave Kirby
7

Sie können einfach Formencode verwenden, es sei denn, Sie möchten unbedingt Ihre eigenen Rollen spielen . Es glänzt wirklich mit vielen Attributen und Schemata (nur Unterklassenschemata) und hat viele nützliche Validatoren eingebaut. Wie Sie sehen, ist dies der Ansatz "Daten vor dem Erstellen eines Spam-Objekts validieren".

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) 

Sie könnten die Überprüfungen auch während durchführen __init__(und sie mit Deskriptoren | Dekoratoren | Metaklasse vollständig transparent machen), aber ich bin kein großer Fan davon. Ich mag eine saubere Barriere zwischen Benutzereingaben und internen Objekten.

Jochen Ritzel
quelle
6

Wenn Sie nur die an den Konstruktor übergebenen Werte überprüfen möchten, können Sie Folgendes tun:

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

Dies wird natürlich niemanden daran hindern, so etwas zu tun:

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

Der richtige Ansatz hängt also davon ab, was Sie erreichen möchten.

SilentGhost
quelle
das ist mein eigentlicher Ansatz; aber ich mag es nicht, wenn Attribute Nummer erhöhen und \ oder Einschränkungen Prüfungen ausgefeilter sind. Es scheint die init-Methode zu überladen.
Systempuntoout
1
@system: Sie können die Gültigkeitsprüfung in eine eigene Methode unterteilen: Es gibt keine festen Regeln für diese Situation.
SilentGhost
1

Sie können versuchen 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

Es gibt nach

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

Es ist kompatibel mit Python 2 und 3.5 (im Gegensatz zu pydantic) und die Validierung erfolgt jedes Mal, wenn der Wert geändert wird (nicht nur beim ersten Mal im Gegensatz zu attrs). Es kann den Konstruktor für Sie erstellen, tut dies jedoch nicht standardmäßig, wie oben gezeigt.

Beachten Sie, dass Sie möglicherweise mini-lambdaanstelle der einfachen alten Lambda-Funktionen optional verwenden möchten, wenn die Fehlermeldungen noch einfacher sein sollen (sie zeigen den fehlerhaften Ausdruck an).

Einzelheiten finden Sie in der pyfieldsDokumentation (ich bin übrigens der Autor;))

smarie
quelle