寫入屬性子類別

Property 類別專門用於劃分子類別。不過,通常會將現有的 Property 子類別做為子類別。

所有特殊 Property 屬性 (即使是視為「public」的屬性) 的名稱開頭都是底線。這是因為 StructuredProperty 使用非底線屬性命名空間來參照巢狀 Property 名稱,這對於指定子屬性查詢至關重要。

Property 類別及其預先定義的子類別可讓您使用可組合 (或可堆疊) 驗證和轉換 API 進行子類別化。這些分類作業需要一些術語定義:

  • 使用者值是指應用程式程式碼使用實體上的標準屬性設定及存取的值。
  • 基本值是指會序列化至資料儲存庫,並從資料儲存庫反序列化的值。

在使用者值和可序列化的值之間實作特定轉換的 Property 子類別,應實作兩個方法:_to_base_type()_from_base_type()。這些方法「不」應該呼叫本身的 super() 方法。這就是可組合 (或可堆疊) API 的意思。

API 支援使用複雜度較高的使用者-底數轉換來「堆疊」類別:使用者-底數轉換是屬於複雜度由高至低的轉換,而底數-使用者轉換則是屬於複雜度由低至高的轉換。例如,請查看 BlobPropertyTextPropertyStringProperty 之間的關係。舉例來說,TextProperty 會繼承 BlobProperty;由於其繼承了所需的大部分行為,因此程式碼相當簡單。

除了 _to_base_type()_from_base_type() 之外,_validate() 方法也是可組合 API。

這個驗證 API 會區分「鬆散」和「嚴格」兩種使用者值。鬆散值集是嚴格值集的超集合。_validate() 方法會採用寬鬆值,並視需要將其轉換為嚴格值。也就是說,在設定屬性值時,鬆散值是可接受的,但在擷取屬性值時,只會傳回嚴格值。如果不需要轉換,_validate() 可能會傳回 None。如果引數位於可接受的鬆散值集以外,_validate() 應會引發例外狀況,偏好使用 TypeErrordatastore_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 讀取時,就會發生事件的反向鏈結。StringPropertyProperty 類別會共同處理其他詳細資料,例如序列化和反序列化字串、設定預設值,以及處理重複的屬性值。

在這個範例中,支援不等式 (也就是使用 <、<=、>、>= 的查詢) 需要更多工作。以下實作範例會強制設下整數的最大大小,並以固定長度的字串儲存值:

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

您可以建立衍生自 StructuredPropertyFuzzyDateProperty。很抱歉,後者無法與舊版 Python 類別搭配運作,需要 Model 子類別。因此,請將 Model 子類別定義為中繼表示法;

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

接下來,請建構 StructuredProperty 的子類別,將 modelclass 引數硬式編碼為 FuzzyDateModel,並定義 _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()

假設您想接受 date 物件 (除了 FuzzyDate 物件) 做為 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 子類別化為下列形式 (假設 FuzzyDateProperty._validate() 如上所示)。

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 無法達到您想要的效果)。