クラスのインスタンスの属性を検証するための正しいアプローチ


83

このような単純なPythonクラスを持つ:

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

次の制約を確認したいと思います。

  • 「説明を空にすることはできません」
  • 「値はゼロより大きくなければなりません」

万一I:
スパムオブジェクトを作成する前に、1.検証データ?
2.__init__メソッドのデータを確認しますか?
3. is_validSpamクラスにメソッドを作成し、spam.isValid()で呼び出しますか?
4. is_validSpamクラスに静的メソッドを作成し、Spam.isValid(description、value)で呼び出しますか?
5.セッター宣言のデータを確認しますか?
6.など。

適切に設計された/ Pythonic /冗長ではない(多くの属性を持つクラスで)/エレガントなアプローチをお勧めしますか?

回答:


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 2010年

2
同意しました、それは最も美しい解決策ではありません。Pythonは放し飼いのクラス(ニワトリを考えてください)を好み、アクセスを制御するプロパティのアイデアは少し後から考えました。そうは言っても、これは私が考えることができる他のどの言語でもそれほど簡潔ではありません。
マルセロカントス2010年

2
@MarceloCantosこれは古い質問だと思いますが、ドキュメントに基づいて(Python 3の場合でも)、self.description = descriptionアンダースコアを使用する必要がありますself._description = descriptionか、それとも問題ではありませんか?これは必要ですか、それともPythonの「プライベート」変数のバージョンに似たものですか?
John Bensin 2013年

12
@JohnBensin:はい、いいえ。self.description = …プロパティを介してself._description = …割り当てますが、基になるフィールドに直接割り当てます。建設中にどちらを使用するかは設計上の選択ですが、通常は常にプロパティを介して割り当てる方が安全です。たとえば、上記のコードでは、必要に応じてを呼び出すと例外が発生しますSpam('', 1)
マルセロカントス2013年

1
冗長すぎるとは思いません。別の方法は、値を無効な値に設定できるようにすることです。
トニー・エニス

10

オブジェクトの作成時に値を検証するだけで、無効な値を渡すとプログラミングエラーと見なされる場合は、アサーションを使用します。

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

これは、取得するのとほぼ同じくらい簡潔であり、これらがオブジェクトを作成するための前提条件であることを明確に文書化しています。


デイブに感謝します。assertを使用して、そのクラスのクライアントに何がうまくいかなかったか(説明または値)を指定するにはどうすればよいですか?アサーションは、決して起こらない条件をテストするためだけに使用されるべきだと思いませんか?
systempuntoout 2010年

1
たとえば、assertステートメントにメッセージを追加できますassert value > 0, "value attribute to Spam must be greater than zero"。アサーションは実際には開発者へのメッセージであり、プログラミングエラーを示しているため、クライアントコードによってキャッチされるべきではありません。クライアントにエラーをキャッチして処理させたい場合は、他の回答に示されているように、ValueErrorなどの例外を明示的に発生させます。
デイブカービー

1
2番目の質問に答えるには、yesアサートを使用して、決して発生してはならない条件をテストする必要があります。そのため、「無効な値を渡すとプログラミングエラーと見なされる場合...」と言いました。そうでない場合は、assertを使用しないでください。
デイブカービー

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

したがって、正しいアプローチは、達成しようとしていることによって異なります。


これが私の実際のアプローチです。しかし、属性番号の引き上げや制約チェックがより洗練されている場合、私はそれが好きではありません。initメソッドが乱雑になりすぎているようです。
systempuntoout 2010年

1
@system:妥当性チェックを独自のメソッドに分離できます。この状況に関する厳格なルールはありません。
SilentGhost 2010年

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ドキュメントを参照してください(ちなみに私は作者です;))

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.