プロパティ サブクラスを記述する

Property クラスは、サブクラス化するように設計されています。ただし、通常は既存の Property サブクラスをサブクラス化するほうが簡単です。

特別な Property 属性はすべて、公開用と考えられる場合でも、名前をアンダーバーで始めます。StructuredProperty は、アンダーバー以外の属性の名前空間を使用して、ネストされた Property 名を参照します。これは、サブプロパティにクエリを指定するうえで非常に重要です。

Property クラスと事前定義のサブクラスを使用すると、積み重ね(スタック)可能な検証と変換 API でサブクラス化を行うことができます。このためには、次のように用語を定義する必要があります。

  • ユーザー値。エンティティの標準属性を使用したアプリケーション コードによって設定、アクセスが可能な値です。
  • ベース値。データストアに対してシリアル化または非シリアル化された値です。

Property サブクラス。ユーザー値とシリアル化可能な値の間で特定の変換を行います。_to_base_type()_from_base_type() の 2 つのメソッドを実装する必要があります。これらのメソッドからその super() メソッドを呼び出すことはできません。積み重ね可能(スタック可能)な API というのは、このようなことを意味します。

API はこれまで以上に高度なユーザー / ベース間変換によってクラスの積み重ねをサポートします。ユーザー / ベース間変換はより高度なものからあまり高度ではないものに変わり、ベース / ユーザー間変換はあまり高度ではないものからより高度なものに変わります。たとえば、BlobPropertyTextPropertyStringProperty の関係を見てみましょう。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() メソッドが呼び出され、例外が発生しなければ、値がエンティティに格納されます。エンティティをデータストアに書き込む際は _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

これはプロパティ コンストラクタにビット数を渡す必要がある点を除いて、LongIntegerProperty と同じ方法で使用できます(例 BoundedLongIntegerProperty(1024))。

他のプロパティ タイプも同じようにサブクラス化できます。

構造化データを保存する際にも同じアプローチが利用できます。日付範囲を表す 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()

次に、StructuredProperty のサブクラスを作成して FuzzyDateModel として modelclass 引数をハードコードし、FuzzyDateFuzzyDateModel 間の変換を行う _to_base_type() メソッドと _from_base_type() メソッドを定義します。

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()

FuzzyDateProperty の値として、FuzzyDate オブジェクトに加えてプレーンな date オブジェクトを受け入れるとします。この処理を行うため、_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 を使用しないでください。この 3 つのメソッドの場合、相互作用は希薄で、super は想定した動作になりません。