A classe Property
foi projetada para ser subclassificada.
No entanto, normalmente é mais fácil subclassificar uma subclasse
Property
existente.
Todos os atributos de Property
especiais,
mesmo aqueles considerados "públicos",
têm nomes que começam com um sublinhado.
Isso ocorre porque StructuredProperty
usa o namespace de atributo não sublinhado para se referir a nomes de
Property
aninhados. Isso é essencial para especificar consultas em
subpropriedades.
A classe Property
e suas subclasses predefinidas permitem
subclassificar usando APIs de validação
e conversão para composição ou empilhamento. Elas exigem algumas definições de terminologia:
- Um user value é um valor que é definido e acessado pelo código do aplicativo usando atributos padrão na entidade.
- Um base value é um valor que é serializado e desserializado do Datastore.
Uma subclasse Property
que implementa uma transformação
específica entre valores de usuário e valores serializáveis precisa
implementar dois métodos, _to_base_type()
e
_from_base_type()
.
Eles não podem chamar o método
super()
deles.
É isso que APIs para composição (ou empilhamento) quer dizer.
A API é compatível com classes stacking com conversões usuário-base mais sofisticadas: a conversão usuário-para-base vai da mais sofisticada à menos sofisticada, enquanto a conversão base-para-usuário vai da menos sofisticada à mais sofisticada. Por exemplo, veja a relação entre
BlobProperty
, TextProperty
e StringProperty
.
Por exemplo, TextProperty
herda de
BlobProperty
. O código é bem simples porque herda a maior
parte do comportamento de que precisa.
Além de _to_base_type()
e
_from_base_type()
, o método
_validate()
também é uma API composta.
A API de validação distingue entre valores de usuário lax e rígidos. O conjunto de valores lax é um superconjunto do conjunto de valores rígidos. O método _validate()
recebe um valor lax e, se necessário,
o converte em um valor rígido. Isso significa que ao definir o valor da propriedade, os valores lax são aceitos, enquanto ao receber o valor da propriedade, apenas valores rígidos serão retornados. Caso
nenhuma conversão seja necessária, _validate()
pode retornar None. Se o argumento
estiver fora do conjunto de valores
lax aceitos, _validate()
gerará uma exceção,
de preferência TypeError
ou
datastore_errors.BadValueError
.
O _validate()
, _to_base_type()
e _from_base_type()
não precisam lidar com:
None
: eles não serão chamados comNone
(e se retornarem None, isso significa que o valor não precisa de conversão).- Valores repetidos: a infraestrutura cuida de chamar
_from_base_type()
ou_to_base_type()
para cada item da lista em um valor repetido. - Distinguindo valores de usuário dos valores base: a infraestrutura gerencia isso chamando as APIs de composição.
- Comparações: as operações de comparação chamam
_to_base_type()
no operando. - Distinção entre os valores do usuário e da base: a
infraestrutura garante que
_from_base_type()
será chamado com um valor base (sem pacote) e que_to_base_type()
será chamado com um valor de usuário.
Por exemplo, imagine que você precisa armazenar números inteiros muito longos.
O padrão IntegerProperty
suporta apenas números inteiros de 64 bits
(assinados).
Sua propriedade pode armazenar um inteiro mais longo como uma string. Seria bom que a classe de propriedades manipulasse a conversão.
Um aplicativo usando sua classe de propriedade pode parecer algo como
...
...
...
...
Isso parece simples e direto. Ele também demonstra o
uso de algumas opções de propriedade padrão (padrão, repetidas).
Como autor de LongIntegerProperty
, você ficará feliz
em saber que não precisa escrever nenhum "clichê" para que eles
funcionem. É mais fácil definir uma subclasse de outra propriedade, por exemplo:
Quando você define um valor de propriedade em uma entidade, por
exemplo, ent.abc = 42
, seu método
_validate()
é chamado e, se não gerar uma exceção, o valor
é armazenado na entidade. Quando você escreve a entidade no Datastore,
seu método _to_base_type()
é chamado, convertendo o
valor na string. Então esse valor é serializado pela classe base,
StringProperty
.
A cadeia inversa de eventos ocorre quando a entidade é lida de volta do Datastore. As classes StringProperty
e Property
cuidam dos outros detalhes, como
serializar e desserializar a string, definir o padrão e gerenciar
os valores de propriedade repetidos.
Neste exemplo, compatibilizar desigualdades, ou seja, consultas usando <, <=, >, >=, requer mais trabalho. A implementação do exemplo a seguir impõe um tamanho máximo de inteiro e armazena valores como strings de comprimento fixo:
Isso pode ser usado da mesma forma que LongIntegerProperty
,
exceto que você precisa passar o número de bits para o construtor da propriedade,
por exemplo, BoundedLongIntegerProperty(1024)
.
Você pode criar subclasses de outros tipos de propriedade de maneira semelhante.
Essa abordagem também funciona para armazenar dados estruturados.
Suponha que você tenha uma classe Python FuzzyDate
que represente um
período. Ela usa os campos first
e last
para armazenar o início e o fim do período:
...
É possível criar um FuzzyDateProperty
que seja derivado de
StructuredProperty
. Infelizmente, o último não funciona
com classes Python antigas simples; ele precisa de uma subclasse Model
.
Portanto, defina uma subclasse Model como uma representação intermediária.
Em seguida, construa uma subclasse de StructuredProperty
que codifica o argumento modelclass como FuzzyDateModel
e define os métodos _to_base_type()
e
_from_base_type()
para converter entre FuzzyDate
e
FuzzyDateModel
:
Um aplicativo pode usar essa classe da seguinte maneira:
...
Suponha que você queira aceitar objetos date
simples, além de
objetos FuzzyDate
como valores para
FuzzyDateProperty
. Para fazer isso, modifique o método _validate()
da seguinte maneira:
Em vez disso, você pode criar uma subclasse FuzzyDateProperty
da seguinte maneira
(supondo que FuzzyDateProperty._validate()
seja como mostrado acima).
Quando você atribui um valor a um
campo MaybeFuzzyDateProperty
,
MaybeFuzzyDateProperty._validate()
e
FuzzyDateProperty._validate()
são invocados, nessa ordem.
O mesmo se aplica a _to_base_type()
e
_from_base_type()
: os métodos na
superclass e na subclasse são combinados implicitamente.
Não use super
para controlar o comportamento herdado para isso.
Para esses três métodos,
a interação é sutil e super
não faz o que você quer.