La classe Property
è progettata per essere suddivisa in sottoclassi.
Tuttavia, in genere è più facile creare una sottoclasse di una sottoclasse Property
esistente.
Tutti gli attributi speciali Property
,
anche quelli considerati "pubblici",
hanno nomi che iniziano con un trattino basso.
Questo perché StructuredProperty
utilizza lo spazio dei nomi degli attributi senza trattino basso per fare riferimento ai nomi
Property
nidificati; questo è essenziale per specificare le query sulle
sottoproprietà.
La classe Property
e le relative sottoclassi predefinite consentono
la creazione di sottoclassi utilizzando API di convalida e
conversione componibili (o impilabili). Questi richiedono alcune definizioni terminologiche:
- Un valore utente è un valore che viene impostato e a cui si accede tramite il codice dell'applicazione utilizzando gli attributi standard dell'entità.
- Un valore di base è un valore che verrà serializzato in e deserializzato da Datastore.
Una sottoclasse Property
che implementa una trasformazione specifica tra valori utente e valori serializzabili deve implementare due metodi, _to_base_type()
e _from_base_type()
.
Questi non devono chiamare il metodo super()
.
Questo è ciò che si intende per API componibili (o impilabili).
L'API supporta le classi di stacking con conversioni della base utenti sempre più sofisticate: la conversione da utente a base diventa più sofisticata, mentre la conversione da base a utente diventa meno sofisticata. Ad esempio, vedi 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()
, anche il
metodo _validate()
è un'API componibile.
L'API di convalida distingue tra valori utente flessibili e rigidi. L'insieme dei valori lax è un sovrainsieme dell'insieme dei valori strict. Il metodo _validate()
accetta un valore approssimativo e, se necessario,
lo converte in un valore esatto. 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 valori strict. Se non è necessaria alcuna conversione, _validate()
potrebbe restituire None. Se l'argomento
non rientra nell'insieme dei valori permissivi 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 chiamati conNone
(e se restituiscono None, significa che il valore non richiede la conversione).- Valori ripetuti: l'infrastruttura si occupa di chiamare
_from_base_type()
o_to_base_type()
per ogni elemento di elenco in un valore ripetuto. - Distinguere i valori utente dai valori di base: l'infrastruttura gestisce questa operazione chiamando le API componibili.
- Confronti: le operazioni di confronto chiamano
_to_base_type()
sul relativo operando. - Distinzione tra valori utente e di base: l'infrastruttura garantisce che
_from_base_type()
verrà chiamato con un valore di base (non sottoposto a wrapping) e che_to_base_type()
verrà chiamato con un valore utente.
Ad esempio, supponiamo che tu debba memorizzare numeri interi molto lunghi.
Lo standard IntegerProperty
supporta solo numeri interi a 64 bit (con segno).
La tua proprietà potrebbe memorizzare un numero intero più lungo come stringa. Sarebbe
opportuno che la classe della proprietà gestisca la conversione.
Un'applicazione che utilizza la classe di proprietà potrebbe avere un aspetto simile a
...
...
...
...
Sembra semplice e diretto. Mostra anche l'utilizzo di alcune opzioni di proprietà standard (predefinita, ripetuta). 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:
Quando imposti un valore di proprietà su un'entità, ad esempio
ent.abc = 42
, viene chiamato il metodo _validate()
e (se non genera un'eccezione) il valore
viene memorizzato nell'entità. Quando scrivi l'entità in Datastore,
viene chiamato il metodo _to_base_type()
, che converte il
valore nella stringa. Il valore viene quindi serializzato dalla classe base,
StringProperty
.
La catena inversa di eventi si verifica quando l'entità viene riletta da
Datastore. Le classi StringProperty
e Property
si occupano insieme degli altri dettagli, come la serializzazione
e la deserializzazione della stringa, l'impostazione del valore predefinito e la gestione
dei valori delle proprietà ripetuti.
In questo esempio, il supporto delle disuguaglianze (ovvero le query che utilizzano <, <=, >, >=) richiede più lavoro. La seguente implementazione di esempio impone una dimensione massima per gli interi e memorizza i valori come stringhe a lunghezza fissa:
Può essere utilizzato nello stesso modo di LongIntegerProperty
, tranne per il fatto che devi passare il numero di bit al costruttore della proprietà, ad es. BoundedLongIntegerProperty(1024)
.
Puoi creare sottoclassi di altri tipi di proprietà in modo simile.
Questo approccio funziona anche per l'archiviazione di dati strutturati.
Supponiamo 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:
...
Puoi creare un FuzzyDateProperty
derivato da
StructuredProperty
. Purtroppo, quest'ultima non
funziona con le normali classi Python, ma richiede una sottoclasse Model
.
Quindi, definisci una sottoclasse Model come rappresentazione intermedia.
Successivamente, crea una sottoclasse di StructuredProperty
che codifica l'argomento modelclass in modo che sia FuzzyDateModel
e definisce i metodi _to_base_type()
e
_from_base_type()
per la conversione tra FuzzyDate
e
FuzzyDateModel
:
Un'applicazione potrebbe utilizzare questa classe nel seguente modo:
...
Supponiamo che tu voglia accettare oggetti date
semplici oltre agli oggetti FuzzyDate
come valori per FuzzyDateProperty
. Per farlo, modifica il metodo _validate()
come segue:
In alternativa, puoi creare una sottoclasse di FuzzyDateProperty
come segue
(supponendo che FuzzyDateProperty._validate()
sia come mostrato sopra).
Quando assegni un valore a un
campo MaybeFuzzyDateProperty
,
vengono richiamati sia MaybeFuzzyDateProperty._validate()
che
FuzzyDateProperty._validate()
, in quest'ordine.
Lo stesso vale per _to_base_type()
e
_from_base_type()
: i metodi nella superclasse e nella sottoclasse vengono combinati implicitamente.
Non utilizzare super
per controllare il comportamento ereditato.
Per questi tre metodi,
l'interazione è sottile e super
non fa quello che vuoi.)