Python 2 は、コミュニティでサポートを終了しました。Python 2 アプリを Python 3 に移行することをおすすめします。

プロパティのサブクラスの作成

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 でシリアル化されます。 エンティティがデータストアから読み出されるときは、イベントが逆順で発生します。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 は想定した動作になりません。