写入属性子类

Property 类可以进行子类化。 但是,对现有的 Property 子类进行子类化通常更容易。

所有特殊的 Property 特性 (attribute),即使是公共特性,都具有以下划线开头的名称。这是因为 StructuredProperty 使用非下划线特性命名空间来表示嵌套的 Property 名称;在指定针对子属性 (property) 的查询时,这样做很有必要。

Property 类及其预定义子类允许使用可组合(或可堆叠)验证和转换 API 进行子类化。为说明这些概念,我们需要一些术语定义:

  • 用户值是由使用实体标准特性的应用代码设置和访问的值。
  • 基值是从 Datastore 中进行序列化和反序列化的值。

假设某个 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()

假设除了 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 进行子类化(假设 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 并不能满足您的需求。)