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

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() メソッドが呼び出されます。例外が発生しなければ、値がエンティティに格納されます。エンティティを 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

これは 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()

プレーンな date オブジェクトを受け入れ、FuzzyDateProperty の値として FuzzyDate オブジェクトを受け入れます。この処理を行うため、_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 は想定した動作になりません。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Python 2 の App Engine スタンダード環境