속성 서브클래스 작성

Property 클래스는 서브클래스로 처리되도록 설계되었습니다. 하지만 일반적으로 기존의 Property 서브클래스를 서브클래스로 만드는 것이 더 쉽습니다.

'공개'로 간주되는 속성을 포함하여 모든 특수 Property 속성의 이름은 밑줄로 시작됩니다. 이는 StructuredProperty가 중첩 Property 이름을 표시할 때 밑줄이 아닌 속성 네임스페이스를 사용하기 때문입니다. 이는 하위 속성에 쿼리를 지정하는 데 필수적입니다.

Property 클래스 및 사전 정의된 서브클래스를 이용하면 구성 가능한(또는 스택 가능한) 유효성 검사와 변환 API를 사용하여 서브클래스로 만들 수 있습니다. 이를 위해서는 몇 가지 용어를 정의해야 합니다.

  • 사용자 값은 항목의 표준 속성을 사용하여 애플리케이션 코드로 설정 및 액세스되는 값입니다.
  • 기본 값은 Datastore에서 직렬화 및 비직렬화되는 값입니다.

사용자 값과 직렬화 가능 값 간의 특정 변환을 구현하는 Property 서브클래스는 _to_base_type()_from_base_type()의 두 가지 메서드를 구현해야 합니다. 이러한 메서드는 super() 메서드를 호출해서는 안 됩니다. 이는 구성 가능한(또는 스택 가능한) API의 영향입니다.

API는 더 정교한 사용자 값에서 기본 값으로의 변환을 지원하는 stacking 클래스를 지원합니다. 사용자 값에서 기본 값으로의 변환은 더 정교한 쪽에서 덜 정교한 쪽으로 변환되며, 기본 값에서 사용자 값으로의 변환은 덜 정교한 쪽에서 더 정교한 쪽으로 변환되는 것입니다. 예를 들어 BlobProperty, TextProperty, StringProperty 간의 관계를 참조하세요. 예를 들어 TextPropertyBlobProperty에서 상속되며, 필요한 동작 대부분을 상속받기 때문에 코드는 매우 간단합니다.

_to_base_type()_from_base_type() 외에도 _validate() 메서드 또한 구성 가능한 API입니다.

유효성 검사 API는 느슨한 사용자 값과 엄격한 사용자 값을 구별합니다. 느슨한 값 집합은 엄격한 값 집합의 상위 집합입니다. _validate() 메서드는 느슨한 값을 취하고 필요한 경우 엄격한 값으로 변환합니다. 즉 속성 값을 설정할 때에는 느슨한 값이 허용되며 속성 값을 가져올 때에는 엄격한 값만이 반환됩니다. 변환할 필요가 없는 경우 _validate()는 None을 반환할 수도 있습니다. 인수가 허용되는 느슨한 값의 집합을 벗어나는 경우 _validate()는 예외를 발생시킵니다(예: TypeError 또는 datastore_errors.BadValueError).

_validate(), _to_base_type(), _from_base_type()은 다음을 처리할 필요가 없습니다.

  • None: None으로 호출되지 않습니다. 만약 None이 반환될 경우 값을 변환할 필요가 없습니다.
  • 반복되는 값: 값이 반복되는 각 목록 항목에 대해 인프라가 _from_base_type() 또는 _to_base_type()에 대한 호출을 처리합니다.
  • 사용자 값과 기본 값 구별: 인프라가 구성 가능한 API를 호출하여 이를 처리합니다.
  • 비교: 비교 연산은 피연산자에 대해 _to_base_type()을 호출합니다.
  • 사용자 값과 기본 값 구별: 인프라는 _from_base_type()이 래핑 해제된 기본 값으로 호출되고 _to_base_type()이 사용자 값으로 호출되도록 보장합니다.

예를 들어, 매우 긴 정수를 저장해야 한다고 가정해 보세요. 표준 IntegerProperty는 서명된 64비트 정수만을 지원합니다. 속성에 더 긴 정수를 문자열로 저장할 수도 있습니다. 이 경우 속성 클래스에서 변환을 처리하도록 하는 것이 좋습니다. 속성 클래스를 사용하는 애플리케이션은 다음과 비슷합니다.

from datetime import date

import my_models
...
class MyModel(ndb.Model):
    name = ndb.StringProperty()
    abc = LongIntegerProperty(default=0)
    xyz = LongIntegerProperty(repeated=True)
...
# Create an entity and write it to the Datastore.
entity = my_models.MyModel(name='booh', xyz=[10**100, 6**666])
assert entity.abc == 0
key = entity.put()
...
# Read an entity back from the Datastore and update it.
entity = key.get()
entity.abc += 1
entity.xyz.append(entity.abc//3)
entity.put()
...
# Query for a MyModel entity whose xyz contains 6**666.
# (NOTE: using ordering operations don't work, but == does.)
results = my_models.MyModel.query(
    my_models.MyModel.xyz == 6**666).fetch(10)

이는 간단하고 직관적입니다. 또한 일부 표준 속성 옵션(기본, 반복)을 사용하는 방법도 보여줍니다. LongIntegerProperty를 작성해 본 사용자라면 작업을 수행하기 위해 '상용구'를 쓸 필요가 없다는 것은 좋은 소식일 것입니다. 다른 속성의 서브클래스를 정의하는 것은 더 쉽습니다. 예를 들면 다음과 같습니다.

class LongIntegerProperty(ndb.StringProperty):
    def _validate(self, value):
        if not isinstance(value, (int, long)):
            raise TypeError('expected an integer, got %s' % repr(value))

    def _to_base_type(self, value):
        return str(value)  # Doesn't matter if it's an int or a long

    def _from_base_type(self, value):
        return long(value)  # Always return a long

항목에 속성 값을 설정하면(예: ent.abc = 42) _validate() 메서드가 호출되며 예외가 발생하지 않는 경우 값이 항목에 저장됩니다. Datastore에 항목을 쓸 때 _to_base_type() 메서드가 호출되어 값을 문자열로 변환합니다. 그런 다음 해당 값은 기본 클래스 StringProperty에 의해 직렬화됩니다. 이벤트의 역 체인은 항목을 Datastore에서 다시 읽을 때 발생합니다. StringProperty 클래스와 Property 클래스는 함께 문자열 직렬화 및 비직렬화, 기본값 설정, 반복되는 속성 값 처리와 같은 기타 세부 작업을 처리합니다.

이 예에서 부등식(예: <, <=, >, >=을 사용하는 쿼리)을 지원하려면 더 많은 작업이 필요합니다. 다음 예시 구현은 정수의 최대 크기를 적용하고 값을 고정 길이 문자열로 저장합니다.

class BoundedLongIntegerProperty(ndb.StringProperty):
    def __init__(self, bits, **kwds):
        assert isinstance(bits, int)
        assert bits > 0 and bits % 4 == 0  # Make it simple to use hex
        super(BoundedLongIntegerProperty, self).__init__(**kwds)
        self._bits = bits

    def _validate(self, value):
        assert -(2 ** (self._bits - 1)) <= value < 2 ** (self._bits - 1)

    def _to_base_type(self, value):
        # convert from signed -> unsigned
        if value < 0:
            value += 2 ** self._bits
        assert 0 <= value < 2 ** self._bits
        # Return number as a zero-padded hex string with correct number of
        # digits:
        return '%0*x' % (self._bits // 4, value)

    def _from_base_type(self, value):
        value = int(value, 16)
        if value >= 2 ** (self._bits - 1):
            value -= 2 ** self._bits
        return value

이는 속성 생성자에 비트 수를 전달해야 한다는 점을 제외하면(예: BoundedLongIntegerProperty(1024)) LongIntegerProperty와 같은 방식으로 사용할 수 있습니다.

비슷한 방식으로 다른 속성 유형을 서브클래스로 만들 수 있습니다.

이 접근법은 구조화된 데이터 저장에도 사용할 수 있습니다. 기간을 나타내는 FuzzyDate Python 클래스가 있다고 가정해 보겠습니다. 이 클래스는 firstlast 필드를 사용하여 기간의 시작과 종료를 저장합니다.

from datetime import date

...
class FuzzyDate(object):
    def __init__(self, first, last=None):
        assert isinstance(first, date)
        assert last is None or isinstance(last, date)
        self.first = first
        self.last = last or first

StructuredProperty에서 파생된 FuzzyDateProperty를 만들 수 있습니다. 하지만 후자의 경우 기존 Python 클래스에서는 사용할 수 없으며 Model 서브클래스가 필요합니다. 따라서 Model 서브클래스를 중간 표현으로 정의해야 합니다.

class FuzzyDateModel(ndb.Model):
    first = ndb.DateProperty()
    last = ndb.DateProperty()

그런 다음, modelclass 인수를 FuzzyDateModel로 하드코딩하는 StructuredProperty의 서브클래스를 구성하고 _to_base_type()_from_base_type() 메서드를 정의하여 FuzzyDateFuzzyDateModel 간의 변환을 수행합니다.

class FuzzyDateProperty(ndb.StructuredProperty):
    def __init__(self, **kwds):
        super(FuzzyDateProperty, self).__init__(FuzzyDateModel, **kwds)

    def _validate(self, value):
        assert isinstance(value, FuzzyDate)

    def _to_base_type(self, value):
        return FuzzyDateModel(first=value.first, last=value.last)

    def _from_base_type(self, value):
        return FuzzyDate(value.first, value.last)

애플리케이션은 다음과 같이 이 클래스를 사용할 수 있습니다.

class HistoricPerson(ndb.Model):
    name = ndb.StringProperty()
    birth = FuzzyDateProperty()
    death = FuzzyDateProperty()
    # Parallel lists:
    event_dates = FuzzyDateProperty(repeated=True)
    event_names = ndb.StringProperty(repeated=True)
...
columbus = my_models.HistoricPerson(
    name='Christopher Columbus',
    birth=my_models.FuzzyDate(date(1451, 8, 22), date(1451, 10, 31)),
    death=my_models.FuzzyDate(date(1506, 5, 20)),
    event_dates=[my_models.FuzzyDate(
        date(1492, 1, 1), date(1492, 12, 31))],
    event_names=['Discovery of America'])
columbus.put()

# Query for historic people born no later than 1451.
results = my_models.HistoricPerson.query(
    my_models.HistoricPerson.birth.last <= date(1451, 12, 31)).fetch()

FuzzyDate 객체 이외에도 일반 date 객체를 FuzzyDateProperty 값으로 허용하려 한다고 가정해 보세요. 이렇게 하려면 _validate() 메서드를 다음과 같이 수정해야 합니다.

def _validate(self, value):
    if isinstance(value, date):
        return FuzzyDate(value)  # Must return the converted value!
    # Otherwise, return None and leave validation to the base class

FuzzyDateProperty._validate()가 위에 표시된 대로라고 가정한다면 대신 다음과 같이 FuzzyDateProperty를 서브클래스로 만들 수 있습니다.

class MaybeFuzzyDateProperty(FuzzyDateProperty):
    def _validate(self, value):
        if isinstance(value, date):
            return FuzzyDate(value)  # Must return the converted value!
        # Otherwise, return None and leave validation to the base class

MaybeFuzzyDateProperty 필드에 값을 할당하면 MaybeFuzzyDateProperty._validate()FuzzyDateProperty._validate()가 모두 순서대로 호출됩니다. 이는 _to_base_type()_from_base_type()에도 동일하게 적용됩니다. 즉, 슈퍼클래스 및 서브클래스의 메서드는 암시적으로 결합됩니다. (이를 위해 상속된 동작을 제어하는데 super를 사용하지 마세요. 이 세 가지 메서드 간의 상호작용은 미묘하며 super로는 제어할 수 없습니다.)