Scrittura nelle sottoclassi di proprietà

La classe Property è progettata per essere sottoclasse. Tuttavia, di solito è più semplice creare una sottoclasse di una sottoclasse Property esistente.

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 non trattino basso per fare riferimento ai nomi Property nidificati; ciò è essenziale per specificare le query sulle proprietà secondarie.

La classe Property e le sue sottoclassi predefinite consentono di creare sottoclassi utilizzando le API di convalida e conversione componibili (o impilabili). Ciò richiede alcune definizioni terminologiche:

  • Un valore utente è un valore che viene impostato e utilizzato dal codice dell'applicazione utilizzando gli attributi standard dell'entità.
  • Un valore di base è un valore che verrebbe serializzato e deserializzato da Datastore.

Una sottoclasse Property che implementa una trasformazione specifica tra i valori utente e i valori serializzabili deve implementare due metodi: _to_base_type() e _from_base_type(). che non devono chiamare il loro metodo super(). Ecco cosa si intende per API componibili (o sovrapponibili).

L'API supporta classi stack con conversioni della base utenti sempre più sofisticate: la conversione da utente a base passa da più sofisticata a meno sofisticata, mentre la conversione base-utente da meno sofisticata a più sofisticata. Ad esempio, visualizza la relazione tra BlobProperty, TextProperty e StringProperty. Ad esempio, TextProperty eredita da BlobProperty; il suo codice è piuttosto semplice perché eredita la maggior parte del comportamento di cui ha bisogno.

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

L'API di convalida fa distinzione tra i valori utente lax e strict. L'insieme di valori lax è un soprainsieme dell'insieme di valori rigidi. Il metodo _validate() accetta un valore lax e, se necessario, lo converte in un valore fisso. Ciò significa che quando si imposta il valore della proprietà, vengono accettati valori lax, mentre quando si ottiene il valore della proprietà vengono restituiti solo i valori rigidi. Se non è necessaria alcuna conversione, _validate() potrebbe restituire None. Se l'argomento non rientra nell'insieme di valori lax accettati, _validate() deve generare un'eccezione, preferibilmente TypeError o datastore_errors.BadValueError.

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

  • None: non verranno richiamate 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 elemento dell'elenco in un valore ripetuto.
  • Distinguere i valori utente dai valori di base: l'infrastruttura gestisce questa situazione chiamando le API componibili.
  • Confronti: le operazioni di confronto chiamano _to_base_type() sull'operando.
  • Distinguere tra valori utente e valori base: l'infrastruttura garantisce che _from_base_type() verrà chiamato con un valore di base (senza wrapping) e che _to_base_type() verrà chiamato con un valore utente.

Ad esempio, supponiamo che tu debba archiviare numeri interi molto lunghi. Lo standard IntegerProperty supporta solo numeri interi a 64 bit (con segno). La 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 di proprietà potrebbe avere il seguente aspetto:

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)

La procedura è semplice e immediata. Dimostra inoltre l'utilizzo di alcune opzioni di proprietà standard (valore predefinito, ripetuto); in qualità di autore di LongIntegerProperty, sarai felice di sapere che non devi scrivere alcun "boilerplate" per farle funzionare. È più facile definire una sottoclasse di un'altra proprietà, ad 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 un valore della proprietà su un'entità, ad esempio ent.abc = 42, viene chiamato il metodo _validate() e (se non viene generata un'eccezione) il valore viene archiviato nell'entità. Quando scrivi l'entità in Datastore, viene chiamato il metodo _to_base_type(), convertendo il valore nella stringa. Questo valore viene serializzato dalla classe base, StringProperty. La catena inversa di eventi si verifica quando l'entità viene letta da Datastore. Le classi StringProperty e Property insieme si occupano degli altri dettagli, ad esempio la serializzazione e la deserializzazione della stringa, l'impostazione del valore predefinito e la gestione dei valori delle proprietà ripetute.

In questo esempio, supportare le disuguaglianze (ovvero query che usano <, <=, >, >=) richiede più lavoro. La seguente implementazione di esempio impone una dimensione massima per il numero intero e archivia i valori come stringhe di 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

Questa funzione può essere usata allo stesso modo di LongIntegerProperty, ad eccezione del fatto che devi passare il numero di bit al costruttore della proprietà, ad esempio BoundedLongIntegerProperty(1024).

Puoi creare una sottoclasse di altri tipi di proprietà in modo simile.

Questo approccio funziona anche per l'archiviazione di dati strutturati. Supponi di avere una classe Python FuzzyDate che rappresenta un intervallo di date, che utilizza i campi first e last per archiviare 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 elemento FuzzyDateProperty che deriva da StructuredProperty. Sfortunatamente, quest'ultima non funziona con le semplici classi Python precedenti; richiede una sottoclasse Model. Quindi definisci una sottoclasse del modello come rappresentazione intermedia;

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

Quindi, costruisci una sottoclasse StructuredProperty che imposti come hardcoded l'argomento modelclass in FuzzyDateModel e definisca i metodi _to_base_type() e _from_base_type() 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 può utilizzare questa classe nel seguente 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 semplici in aggiunta agli oggetti FuzzyDate come valori per FuzzyDateProperty. Per farlo, modifica il metodo _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

Puoi optare per la sottoclasse FuzzyDateProperty come segue (supponendo che FuzzyDateProperty._validate() sia 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, vengono richiamati sia MaybeFuzzyDateProperty._validate() sia FuzzyDateProperty._validate() in questo ordine. Lo stesso vale per _to_base_type() e _from_base_type(): i metodi nella superclasse e nella sottoclasse sono implicitamente combinati. Non usare super per controllare il comportamento ereditato per questa operazione. Per questi tre metodi, l'interazione è minima e super non fa quello che vuoi.)