La classe Property
è progettata per essere sottoclassificata.
Tuttavia, in genere è più facile creare una sottoclasse di una
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 degli attributi senza trattini bassi per fare riferimento ai nomi Property
nidificati. Questo è essenziale per specificare le query sulle proprietà secondarie.
La classe Property
e i relativi sottoclassi predefiniti consentono di creare sottoclassi utilizzando API di convalida e conversione composable (o impilabili). Queste richiedono alcune definizioni di terminologia:
- Un valore utente è un valore che verrebbe impostato e a cui verrebbe eseguito l'accesso dal codice dell'applicazione utilizzando gli attributi standard dell'entità.
- Un valore base è un valore che verrebbe serializzato e deserializzato dal 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 metodi non devono chiamare il metodo
super()
.
Questo è ciò che si intende per API composable (o impilabili).
L'API supporta le classi di accodamento con conversioni basate sugli utenti sempre più sofisticate: la conversione da utente a base avviene da più sofisticata a meno sofisticata, mentre la conversione da base a utente avviene da meno sofisticata a più sofisticata. Ad esempio, osserva 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 rilassati e rigorosi. L'insieme di valori lax è un superinsieme dell'insieme di valori rigorosi. Il metodo _validate()
prende un valore lassista e, se necessario, lo trasforma in un valore rigoroso. 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 rigorosi. Se non è necessaria alcuna conversione, _validate()
può restituire None. Se l'argomento 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 chiamati conNone
(e se restituiscono None, significa che il valore non richiede 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 questo compito chiamando le API componibili.
- Confronti: le operazioni di confronto chiamano
_to_base_type()
sul loro operando. - Distinzione tra valori utente e di base: l'infrastruttura garantisce che
_from_base_type()
venga chiamato con un valore di base (scollegato) e che_to_base_type()
venga chiamato con un valore utente.
Ad esempio, supponiamo che tu debba memorizzare numeri interi molto lunghi.
IntegerProperty
standard supporta solo numeri interi (con segno) a 64 bit.
La proprietà potrebbe memorizzare un numero intero più lungo come stringa. È consigliabile che la classe della proprietà gestisca la conversione.
Un'applicazione che utilizza la tua classe di proprietà potrebbe avere il seguente aspetto
...
...
...
...
Sembra semplice e diretto. Inoltre, mostra l'uso di alcune opzioni di proprietà standard (predefinite, ripetute); in qualità di autore di LongIntegerProperty
, ti farà piacere sapere che non devi scrivere alcun "boilerplate" per utilizzarle. È più facile definire una sottoclasse di un'altra proprietà, ad esempio:
Quando imposti un valore della proprietà in un'entità, ad esempio
ent.abc = 42
, viene chiamato il metodo _validate()
e (se non viene sollevata 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 in stringa. Il valore viene poi serializzato dalla classe di base,
StringProperty
.
La catena di eventi inversa si verifica quando l'entità viene letta nuovamente 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 di proprietà ripetuti.
In questo esempio, il supporto delle disuguaglianze (ovvero query che utilizzano <, <=, >, >=) richiede più lavoro. L'implementazione di esempio riportata di seguito impone una dimensione massima di un numero intero e immagazzina i valori come stringhe di 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 modi simili.
Questo approccio funziona anche per l'archiviazione di dati strutturati.
Supponiamo di avere una classe Python FuzzyDate
che rappresenti 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
che deriva da
StructuredProperty
. Purtroppo, quest'ultimo non funziona con le classi Python standard, ma richiede una sottoclasse Model
.
Quindi, definisci una sottoclasse Model come rappresentazione intermedia.
Successivamente, crea una sottoclasse di StructuredProperty
che codifichi l'argomento modelclass come FuzzyDateModel
e definisce i metodi _to_base_type()
e
_from_base_type()
per convertire da FuzzyDate
a
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 invocati 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 vengono combinati implicitamente.
Non utilizzare super
per controllare il comportamento ereditato per questo.
Per questi tre metodi,
l'interazione è sottile e super
non fa ciò che vuoi.