Abgeleitete Attributklassen schreiben

Die Property-Klasse ist als abgeleitete Klasse konzipiert. In der Regel ist es jedoch einfacher, eine vorhandene abgeleitete Property-Klasse als abgeleitete Klasse festzulegen.

Die Namen von speziellen Property-Attributen beginnen mit einem Unterstrich. Dies trifft sogar auf Attribute zu, die als "öffentlich" gelten. Dies liegt daran, dass StructuredProperty den Attribut-Namespace ohne Unterstrich verwendet, um auf verschachtelte Property-Namen zu verweisen. Dies ist für die Angabe von Abfragen für untergeordnete Properties unverzichtbar.

Die Property-Klasse und ihre vordefinierten abgeleiteten Klassen ermöglichen ein einfaches Erstellen von abgeleiteten Klassen mithilfe von zusammensetzbaren (oder stapelbaren) Validierungs- und Konvertierungs-APIs. Einige Begriffe müssen in diesem Zusammenhang erläutert werden:

  • Ein Nutzerwert ist ein Wert, der durch den Anwendungscode unter Verwendung von Standardattributen für die Entität festgelegt und abgerufen wird.
  • Ein Basiswert ist ein Wert, der im Datenspeicher serialisiert und deserialisiert wird.

Eine abgeleitete Property-Klasse zur Implementierung einer bestimmten Transformation zwischen Nutzerwerten und serialisierbaren Werten sollte zwei Methoden implementieren: _to_base_type() und _from_base_type(). Diese sollten nicht die eigene Methode super() aufrufen. Hierbei handelt es sich um das Konzept, das mit zusammensetzbaren (oder stapelbaren) APIs gemeint ist.

Die API unterstützt stacking-Klassen mit immer komplexeren Konvertierungen zwischen Nutzer- und Basiswerten: Die Konvertierung von Nutzer- zu Basiswert geht von sehr komplex zu weniger komplex, die umgekehrte Konvertierung hingegen von weniger komplex zu sehr komplex. Siehe zum Beispiel die Beziehung zwischen BlobProperty, TextProperty und StringProperty. TextPropertyist beispielsweise BlobProperty untergeordnet. Dessen Code ist ziemlich einfach, da das erforderliche Verhalten größtenteils übernommen wird.

Neben _to_base_type() und _from_base_type() ist die Methode _validate() auch eine zusammensetzbare API.

Die Validierungs-API unterscheidet zwischen laxen und strikten Nutzerwerten. Die Menge der laxen Werte ist eine Obermenge der Menge der strikten Werte. Die Methode _validate() nimmt einen laxen Wert an und wandelt ihn bei Bedarf in einen strikten Wert um. Dies bedeutet, dass beim Festlegen des Property-Werts laxe Werte angenommen werden, während beim Abrufen des Property-Werts nur strikte Werte zurückgegeben werden. Wenn keine Konvertierung erforderlich ist, kann _validate() den Wert "None" zurückgeben. Liegt das Argument außerhalb der Menge von akzeptierten laxen Werten, sollte _validate() eine Ausnahme auslösen, vorzugsweise TypeError oder datastore_errors.BadValueError.

Die Parameter _validate(), _to_base_type() und _from_base_type() müssen nicht verarbeitet werden:

  • None: Sie werden nicht mit None aufgerufen. Wenn sie "None" zurückgeben, bedeutet dies, dass der Wert nicht konvertiert werden muss.
  • Wiederholte Werte: Die Infrastruktur übernimmt den Aufruf von _from_base_type() oder _to_base_type() für jedes Listenelement in einem wiederholten Wert.
  • Unterscheidung zwischen Nutzerwerten und Basiswerten: Die Infrastruktur übernimmt dies durch Aufrufen der zusammensetzbaren APIs.
  • Vergleiche: Die Vergleichsvorgänge rufen _to_base_type() auf ihrem Operanden auf.
  • Unterscheidung zwischen Nutzer- und Basiswerten: Die Infrastruktur gewährleistet, dass _from_base_type() mit einem (nicht eingebetteten) Basiswert und _to_base_type() mit einem Nutzerwert aufgerufen wird.

Angenommen, Sie müssen sehr lange Ganzzahlen speichern. Die Standard-IntegerProperty unterstützt lediglich (signierte) 64-Bit-Ganzzahlen. Ihre Eigenschaft speichert möglicherweise eine längere Ganzzahl als ein String. Es empfiehlt sich also die Übernahme der Konvertierung durch die Property-Klasse. Eine Anwendung, die Ihre Property-Klasse verwendet, sieht in etwa folgendermaßen aus:

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)

Das sieht einfach und unkompliziert aus. Es zeigt auch die Verwendung einiger standardmäßiger Attributoptionen (Standard, wiederholt). Als Autor von LongIntegerProperty profitieren Sie davon, dass Sie keinen "Standardcode" schreiben müssen, um damit arbeiten zu können. Es ist einfacher, eine abgeleitete Klasse einer anderen Property zu definieren, zum Beispiel:

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

Wenn Sie einen Attributwert für eine Entität festlegen, z. B. ent.abc = 42, wird Ihre Methode _validate() aufgerufen und, wenn keine Ausnahme ausgelöst wird, in dem Wert gespeichert. Entität. Wenn Sie die Entität in den Datenspeicher schreiben, wird Ihre _to_base_type()-Methode aufgerufen, wobei der Wert in den String konvertiert wird. Dann wird dieser Wert von der Basisklasse StringPropertyStringProperty serialisiert. Die inverse Ereigniskette tritt auf, wenn die Entität aus dem Datenspeicher zurückgelesen wird. Die Klassen StringProperty und Property übernehmen die anderen Details, z. B. die Serialisierung und Deserialisierung des Strings, die Festlegung des Standardwerts und die Verarbeitung wiederholter Property-Werte.

In diesem Beispiel erfordert die Unterstützung von Ungleichungen (d. h. Abfragen mit <, <=, >, >=) mehr Arbeit. Die folgende Beispielimplementierung erzwingt eine maximale Größe von Ganzzahlen und speichert Werte als Strings fester Länge:

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

Dies kann auf die gleiche Weise wie LongIntegerProperty verwendet werden, mit der Ausnahme, dass die Anzahl der Bit an den Attribut-Konstruktor übergeben werden müssen, z. B. BoundedLongIntegerProperty(1024).

Sie können andere Property-Typen auf ähnliche Weise ableiten.

Dieser Ansatz funktioniert auch beim Speichern strukturierter Daten. Angenommen, Ihre Python-Klasse FuzzyDate stellt einen Datumsbereich dar. Sie verwendet die Felder first und last, um den Anfang und das Ende des Datumsbereichs zu speichern:

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

Sie können ein FuzzyDateProperty erstellen, das von StructuredProperty abgeleitet wird. Leider kann das zuletzt genannte nicht mit einfachen alten Python-Klassen verwendet werden, sondern benötigt eine abgeleitete Model-Klasse. Definieren Sie eine abgeleitete Modellklasse als eine Zwischendarstellung.

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

Erstellen Sie als Nächstes eine abgeleitete Klasse von StructuredProperty, die das modelclass-Argument als FuzzyDateModel hartcodiert und die Methoden _to_base_type() und _from_base_type() für die Konvertierung zwischen FuzzyDate und FuzzyDateModel definiert:

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)

Eine Anwendung könnte diese Klasse so verwenden:

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

Angenommen, Sie möchten einfache date-Objekte zusätzlich zu FuzzyDate-Objekten als Werte für FuzzyDateProperty akzeptieren. Ändern Sie dazu die _validate()-Methode so:

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

Sie können stattdessen FuzzyDateProperty unter der Annahme, dass FuzzyDateProperty._validate() der Darstellung oben entspricht, so ableiten:

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

Wenn Sie einem MaybeFuzzyDateProperty-Feld einen Wert zuweisen, werden sowohl MaybeFuzzyDateProperty._validate() als auch FuzzyDateProperty._validate() in dieser Reihenfolge aufgerufen. Das gleiche gilt für _to_base_type() und _from_base_type(): Die Methoden in der Basisklasse und abgeleiteten Klasse werden implizit kombiniert. (Verwenden Sie hierfür nicht super, um übernommenes Verhalten zu steuern. Für diese drei Methoden ist die Interaktion subtil und super verhält sich nicht wie gewünscht.)