Scrittura nelle sottoclassi di proprietà

La classe Property è progettata per essere creata con sottoclassi. Tuttavia, in genere è più semplice creare una sottoclasse Property sottoclasse.

Tutti gli attributi Property speciali, anche quelli considerati "pubblici", hanno nomi che iniziano con un trattino basso. Questo perché StructuredProperty utilizza lo spazio dei nomi dell'attributo senza trattino basso per fare riferimento Property nomi; Ciò è essenziale per specificare le query proprietà secondarie.

La classe Property e le sue sottoclassi predefinite consentono creazione di sottoclassi mediante la convalida componibili (o impilabili) API di conversione. Queste richiedono alcune definizioni terminologiche:

  • Un user value è un valore che verrebbe impostato e a cui potrebbe accedere il del codice dell'applicazione utilizzando attributi standard sull'entità.
  • Un valore di base è un valore come quello che verrebbe serializzato su e deserializzato da Datastore.

Una sottoclasse Property che implementa una specifica la trasformazione tra i valori utente e quelli serializzabili a implementare due metodi, _to_base_type() e _from_base_type(). Questi non devono chiamare il proprio super(). Questo è ciò che si intende per API componibili (o impilabili).

L'API supporta classi di stacking con regole sempre più sofisticate conversioni base utenti: la conversione base utente dal più sofisticato al meno sofisticato, la conversione da base a utente passa da meno sofisticata a più sofisticato. Ad esempio, controlla la relazione tra BlobProperty, TextProperty, e StringProperty. Ad esempio, TextProperty eredita da BlobProperty; il suo codice è piuttosto semplice perché eredita gran parte del comportamento di cui ha bisogno.

Oltre a _to_base_type() e _from_base_type(), _validate() è anche un'API componibile.

L'API di convalida distingue tra lax e utente strict e i relativi valori. L'insieme di valori lax è un soprainsieme dell'insieme di valori e i relativi valori. Il metodo _validate() accetta un valore permissivo e se necessario lo converte in un valore preciso. Ciò significa che quando imposti della proprietà, sono accettati valori permissivi, mentre quando si ottiene il , verranno restituiti solo valori rigidi. In caso contrario la conversione è necessaria, _validate() può restituire Nessuna. Se l'argomento non rientra nell'insieme dei valori lax accettati, _validate() dovrebbe aumentare un'eccezione, preferibilmente TypeError o datastore_errors.BadValueError.

_validate(), _to_base_type(), e _from_base_type() non devono gestire:

  • None: non verranno chiamati con None (e se restituisce None, significa che il valore non ha bisogno di conversione).
  • Valori ripetuti: l'infrastruttura si occupa di chiamare _from_base_type() o _to_base_type() per ogni voce dell'elenco in un valore ripetuto.
  • Distinguere i valori utente dai valori di base: l'infrastruttura gestisce chiamando le API componibili.
  • Confronti: le operazioni di confronto richiamano _to_base_type() sul loro operando.
  • La distinzione tra i valori utente e di base: infrastruttura garantisce che _from_base_type() verrà chiamato con un valore di base (senza wrapper) e Verrà chiamato _to_base_type() con un valore utente.

Ad esempio, supponiamo di dover archiviare numeri interi molto lunghi. Lo standard IntegerProperty supporta soltanto (firmato) Numeri interi a 64 bit. La tua proprietà potrebbe memorizzare un numero intero più lungo come stringa. sarebbe è utile che la classe della proprietà gestisca la conversione. Un'applicazione che utilizza la classe della proprietà potrebbe essere simile a

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)

Sembra semplice e immediato. Dimostra inoltre utilizzo di alcune opzioni della proprietà standard (predefinita, ripetuta); come l'autore di LongIntegerProperty, ne sarà felice senza dover scrivere "boilerplate" per ottenere funziona. È più facile definire una sottoclasse di un'altra proprietà, esempio:

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

Quando imposti il valore di una proprietà su un'entità, ad es. ent.abc = 42, il tuo _validate() e (se non genera un'eccezione) il valore sono archiviati nell'entità. Quando scrivi l'entità in Datastore, viene chiamato il metodo _to_base_type(), convertendo alla stringa. Quindi quel valore viene serializzato dalla classe base, StringProperty. La catena inversa degli eventi si verifica quando l'entità viene letta per il datastore. StringProperty e Property si occupano degli altri dettagli, come la serializzazione e deserializzare la stringa, impostare il valore predefinito e gestire valori della proprietà ripetuti.

In questo esempio, il supporto delle disuguaglianze (ovvero le query che usano <, <=, >, >=) richiedono un maggior lavoro. L'implementazione dell'esempio seguente impone una dimensione massima per il numero intero e Archivia i valori come stringhe a lunghezza fissa:

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

È possibile usarla come LongIntegerProperty devi passare il numero di bit al costruttore della proprietà, ad es. BoundedLongIntegerProperty(1024).

Puoi creare sottoclassi di altri tipi di proprietà in modi simili.

Questo approccio funziona anche per l'archiviazione dei dati strutturati. Supponi di avere una classe Python FuzzyDate che rappresenta un intervallo di date; utilizza i campi first e last per memorizzare l'inizio e la fine dell'intervallo di date:

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

Puoi creare un valore FuzzyDateProperty che deriva StructuredProperty. Purtroppo quest'ultima usare le vecchie classi Python, ha bisogno di una sottoclasse Model. Definisci quindi una sottoclasse del modello come rappresentazione intermedia;

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

Quindi, crea una sottoclasse di StructuredProperty che imposta come hardcoded l'argomento modelclass in FuzzyDateModel, e definisce _to_base_type() e _from_base_type() metodi per convertire tra FuzzyDate e FuzzyDateModel:

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)

Un'applicazione potrebbe utilizzare questa classe in questo modo:

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

Supponi di voler accettare oggetti date normali in oltre a FuzzyDate oggetti come valori per FuzzyDateProperty. Per farlo, modifica il _validate() come segue:

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

Potresti invece creare la sottoclasse FuzzyDateProperty nel seguente modo: (supponendo che FuzzyDateProperty._validate() come mostrato sopra).

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

Quando assegni un valore a un Campo MaybeFuzzyDateProperty, sia MaybeFuzzyDateProperty._validate() che FuzzyDateProperty._validate() vengono richiamati, in questo ordine. Lo stesso vale per _to_base_type() e _from_base_type(): i metodi in come superclasse e sottoclasse vengono combinate in modo implicito. (Non utilizzare super per controllare il comportamento ereditato per questa impostazione. Per questi tre metodi, l'interazione è impercettibile e super non fa ciò che vuoi).