클래스 인스턴스의 속성을 검증하는 올바른 접근 방식


84

다음과 같은 간단한 Python 클래스가 있습니다.

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

다음 제약 조건을 확인하고 싶습니다.

  • "설명은 비워 둘 수 없습니다."
  • "값은 0보다 커야합니다."


1. 스팸 개체를 만들기 전에 데이터의 유효성을 검사 해야합니까 ?
2. __init__방법 에 대한 데이터를 확인 ?
3. is_validSpam 클래스에 메서드를 만들고 spam.isValid ()로 호출합니까?
4. is_validSpam 클래스에 정적 메서드를 만들고 Spam.isValid (description, value)?
5. setters 선언에 대한 데이터를 확인합니까?
6. 등

잘 디자인 된 / 파이 토닉 / 장황하지 않은 (많은 속성을 가진 수업에서) / 우아한 접근 방식을 추천 해 주시겠습니까?

답변:


106

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
동의합니다. 가장 좋은 해결책은 아닙니다. 파이썬은 자유 범위 클래스 (닭을 생각해보십시오)를 선호하며, 접근을 제어하는 ​​속성에 대한 아이디어는 약간의 사후 고려였습니다. 그러나 이것은 내가 생각할 수있는 다른 언어로는 훨씬 더 간결하지 않을 것입니다.
Marcelo Cantos

2
@MarceloCantos 나는 이것이 오래된 질문이라는 것을 알고 있지만 (Python 3의 경우에도 불구 하고) 설명서를 기반으로 self.description = description밑줄을 사용해야 self._description = description합니까? 아니면 중요하지 않습니까? 이것이 필요하거나 단순히 파이썬 버전의 "개인"변수와 유사한 것입니까?
John Bensin 2013 년

12
@JohnBensin : 예, 아니오. self.description = …속성을 통해 할당하는 반면 self._description = …기본 필드에 직접 할당합니다. 시공 중에 사용할 디자인은 디자인 선택이지만 일반적으로 항상 속성을 통해 할당하는 것이 더 안전합니다. 예를 들어 위의 코드는를 호출하면 예외가 발생 Spam('', 1)합니다.
Marcelo Cantos 2013 년

1
너무 장황하다고 생각하지 않습니다. 대안은 값을 잘못된 값으로 설정할 수있는 것입니다.
Tony Ennis

10

개체가 생성 될 때만 값의 유효성을 검사하고 잘못된 값을 전달하는 것이 프로그래밍 오류로 간주되면 어설 션을 사용합니다.

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

이것은 당신이 얻는 것만 큼 간결하며 이것이 객체를 만들기위한 전제 조건임을 명확하게 문서화합니다.


감사합니다 Dave, assert를 사용하여 해당 클래스의 클라이언트에게 무엇이 잘못되었는지 (설명 또는 값) 어떻게 지정합니까? 절대 발생해서는 안되는 조건을 테스트하기 위해 단언을 사용해야한다고 생각하지 않습니까?
systempuntoout

1
예를 들어 assert 문에 메시지를 추가 할 수 있습니다 assert value > 0, "value attribute to Spam must be greater than zero". 어설 션은 실제로 개발자에게 보내는 메시지이며 프로그래밍 오류를 나타내므로 클라이언트 코드에서 포착해서는 안됩니다. 클라이언트가 오류를 포착하고 처리하기를 원하면 다른 답변에 표시된 것처럼 ValueError와 같은 예외를 명시 적으로 발생시킵니다.
Dave Kirby

1
두 번째 질문에 답하기 위해 yes는 절대 발생해서는 안되는 조건을 테스트하는 데 사용해야한다고 주장합니다. 이것이 제가 "유효하지 않은 값을 전달하는 것이 프로그래밍 오류로 간주되는 경우 ..."라고 말한 이유입니다. 그렇지 않은 경우 어설 션을 사용하지 마십시오.
Dave Kirby

def 이전에 삽입되어야합니다__init__
datapug

1
감사합니다 @datapug, 오타를 수정했습니다.
Dave Kirby

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__(그리고 descriptors | decorators | metaclass로 완전히 투명하게 만들 수 있습니다),하지만 저는 그 팬이 아닙니다. 저는 사용자 입력과 내부 개체 사이의 깨끗한 장벽을 좋아합니다.


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

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문서 를 참조하십시오 (저는 저자입니다;))

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.