La classe Property è progettata per essere sottoclassificata.
Tuttavia, in genere è più facile sottoclassificare 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 degli attributi senza trattino basso per fare riferimento ai nomi
Property nidificati. Questo è essenziale per specificare le query sulle
sottoproperties.
La classe Property e le relative sottoclassi predefinite consentono la sottoclassificazione utilizzando le API di convalida e conversione componibili (o impilabili). Queste richiedono alcune definizioni di terminologia:
- Un valore utente è un valore che, ad esempio, verrebbe impostato e a cui si accederebbe dal codice dell'applicazione utilizzando gli attributi standard dell'entità.
- Un valore di base è un valore che, ad esempio, 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().
Questi non devono chiamare il metodo super().
Questo è ciò che si intende per API componibili (o impilabili).
L'API supporta le classi di impilamento 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 lax e strict. L'insieme dei valori lax è un superset dell'insieme dei valori strict. Il metodo _validate() accetta un valore lax e, se necessario, lo converte in un valore strict. Ciò significa che quando si imposta il valore della proprietà, vengono accettati i valori lax, mentre quando si recupera il valore della proprietà, vengono restituiti solo i valori strict. Se non è necessaria alcuna conversione, _validate() può restituire None. Se l'argomento non rientra nell'insieme dei 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 conNonee, 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 dell'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. - Distinguere i valori utente dai valori di base: l'
infrastruttura garantisce che
_from_base_type()venga chiamato con un valore di base (non sottoposto a wrapping) e che_to_base_type()venga chiamato con un valore utente.
Supponiamo, ad esempio, di dover memorizzare numeri interi molto lunghi.
IntegerProperty standard supporta solo numeri interi (con segno) a 64 bit.
La tua proprietà potrebbe memorizzare un numero intero più lungo come stringa. Sarebbe utile che la classe della proprietà gestisse la conversione.
Un'applicazione che utilizza la tua classe di proprietà potrebbe avere un aspetto simile a questo:
...
...
...
...
Sembra semplice e diretto. Mostra anche l'utilizzo di alcune opzioni di proprietà standard (default, repeated). 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 es.
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 letta da
Datastore. Le classi StringProperty e Property
si occupano degli altri dettagli, come la serializzazione
e la deserializzazione della stringa, l'impostazione del valore predefinito e la gestione
dei valori delle proprietà ripetute.
In questo esempio, il supporto delle disuguaglianze (ovvero le query che utilizzano <, <=, >, >=) richiede più lavoro. La seguente implementazione di esempio impone una dimensione massima del numero intero e memorizza 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 sottoclassificare altri tipi di proprietà in modi simili.
Questo approccio funziona anche per la memorizzazione 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 una FuzzyDateProperty che deriva da
StructuredProperty. Purtroppo, quest'ultima non funziona con le normali classi Python. Richiede una sottoclasse Model.
Definisci quindi una sottoclasse Model come rappresentazione intermedia.
Quindi, crea una sottoclasse di StructuredProperty
che codifica l'argomento modelclass come FuzzyDateModel,
e definisce _to_base_type() e
_from_base_type()
metodi per convertire tra FuzzyDate e
FuzzyDateModel:
Un'applicazione potrebbe utilizzare questa classe in questo modo:
...
Supponiamo di voler accettare oggetti date semplici in
aggiunta a oggetti FuzzyDate come valori per
FuzzyDateProperty. Per farlo, modifica il metodo _validate() come segue:
In alternativa, puoi sottoclassificare FuzzyDateProperty come segue
(supponendo che FuzzyDateProperty._validate()
sia come mostrato sopra).
Quando assegni un valore a un
MaybeFuzzyDateProperty campo,
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 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.