A classe Property
foi concebida para ser uma subclasse.
No entanto, normalmente, é mais fácil criar uma subclasse de uma subclasse Property
existente.
Todos os atributos especiais Property
, mesmo os considerados "públicos", têm nomes que começam com um sublinhado.
Isto acontece porque StructuredProperty
usa o espaço de nomes de atributos sem sublinhado para se referir a nomes
Property
aninhados. Isto é essencial para especificar consultas em
subpropriedades.
A classe Property
e as respetivas subclasses predefinidas permitem a criação de subclasses através de APIs de validação e conversão compostas (ou acumuláveis). Estes requerem algumas definições de terminologia:
- Um valor do utilizador é um valor que seria definido e acedido pelo código da aplicação através de atributos padrão na entidade.
- Um valor base é um valor que seria serializado para e desserializado a partir do Datastore.
Uma subclasse Property
que implementa uma transformação específica entre valores de utilizador e valores serializáveis deve implementar dois métodos: _to_base_type()
e _from_base_type()
.
Estes não devem chamar o respetivo método super()
.
É isto que se entende por APIs compostas (ou acumuláveis).
A API suporta classes de sobreposição com conversões de base de utilizadores cada vez mais sofisticadas: a conversão de utilizador para base passa de mais sofisticada para menos sofisticada, enquanto a conversão de base para utilizador passa de menos sofisticada para mais sofisticada. Por exemplo, veja a relação entre
BlobProperty
, TextProperty
e
StringProperty
.
Por exemplo, TextProperty
herda de
BlobProperty
; o respetivo código é bastante simples porque
herda a maioria do comportamento de que precisa.
Além de _to_base_type()
e _from_base_type()
, o método _validate()
também é uma API componível.
A API Validation distingue entre valores de utilizadores flexíveis e rígidos. O conjunto de valores permissivos é um superconjunto do conjunto de valores restritos. O método _validate()
recebe um valor flexível e, se necessário, converte-o num valor rigoroso. Isto significa que, quando define o valor da propriedade, são aceites valores frouxos, enquanto que, quando obtém o valor da propriedade, apenas são devolvidos valores rigorosos. Se não for necessária nenhuma conversão, _validate()
pode devolver o valor None. Se o argumento estiver fora do conjunto de valores tolerados aceites, _validate()
deve gerar uma exceção, de preferência TypeError
ou datastore_errors.BadValueError
.
O _validate()
, o _to_base_type()
e o _from_base_type()
não precisam de processar:
None
: não são chamados comNone
(e, se devolverem None, isto significa que o valor não precisa de conversão).- Valores repetidos: a infraestrutura encarrega-se de chamar
_from_base_type()
ou_to_base_type()
para cada item da lista num valor repetido. - Distinguir os valores do utilizador dos valores base: a infraestrutura processa esta ação chamando as APIs de composição.
- Comparações: as operações de comparação chamam
_to_base_type()
no respetivo operando. - Distinguir entre valores de utilizador e base: a infraestrutura garante que
_from_base_type()
é chamado com um valor base (não anulado) e que_to_base_type()
é chamado com um valor de utilizador.
Por exemplo, suponha que precisa de armazenar números inteiros muito longos.
O padrão IntegerProperty
só suporta números inteiros de 64 bits (com sinal).
A sua propriedade pode armazenar um número inteiro mais longo como uma string. Seria
bom que a classe de propriedade processasse a conversão.
Uma aplicação que use a sua classe de propriedade pode ter um aspeto semelhante ao seguinte:
...
...
...
...
Parece simples e direto. Também demonstra a utilização de algumas opções de propriedades padrão (predefinição, repetidas). Como autor de LongIntegerProperty
, vai ficar feliz por saber que não tem de escrever nenhum "modelo" para que funcionem. É mais fácil definir uma subclasse de outra propriedade, por exemplo:
Quando define um valor de propriedade numa entidade, por exemplo, ent.abc = 42
, o método _validate()
é chamado e (se não gerar uma exceção) o valor é armazenado na entidade. Quando escreve a entidade no Datastore, o método _to_base_type()
é chamado, convertendo o valor na string. Em seguida, esse valor é serializado pela classe base,
StringProperty
.
A cadeia inversa de eventos ocorre quando a entidade é lida novamente a partir do Datastore. As classes StringProperty
e Property
cuidam dos outros detalhes, como serializar
e desserializar a string, definir o valor predefinido e processar
valores de propriedades repetidos.
Neste exemplo, a compatibilidade com desigualdades (ou seja, consultas que usam <, <=, >, >=) requer mais trabalho. A implementação de exemplo seguinte impõe um tamanho máximo de números inteiros e armazena valores como strings de comprimento fixo:
Pode usar esta função da mesma forma que LongIntegerProperty
, exceto que tem de transmitir o número de bits ao construtor da propriedade, por exemplo, BoundedLongIntegerProperty(1024)
.
Pode criar subclasses de outros tipos de propriedades de formas semelhantes.
Esta abordagem também funciona para armazenar dados estruturados.
Suponhamos que tem uma classe FuzzyDate
Python que representa um intervalo de datas; usa os campos first
e last
para armazenar o início e o fim do intervalo de datas:
...
Pode criar um FuzzyDateProperty
derivado de
StructuredProperty
. Infelizmente, o último não funciona com as classes Python simples. Precisa de uma subclasse Model
.
Assim, defina uma subclasse Model como uma representação intermédia;
Em seguida, crie uma subclasse de StructuredProperty
que codifica o argumento modelclass para ser FuzzyDateModel
,
e define os métodos _to_base_type()
e
_from_base_type()
para converter entre FuzzyDate
e
FuzzyDateModel
:
Uma aplicação pode usar esta classe da seguinte forma:
...
Suponhamos que quer aceitar objetos date
simples, além de objetos FuzzyDate
, como valores para FuzzyDateProperty
. Para o fazer, modifique o método _validate()
da seguinte forma:
Em alternativa, pode criar uma subclasse de FuzzyDateProperty
da seguinte forma
(partindo do princípio de que FuzzyDateProperty._validate()
é como mostrado acima).
Quando atribui um valor a um campo MaybeFuzzyDateProperty
, são invocados MaybeFuzzyDateProperty._validate()
e FuzzyDateProperty._validate()
, nessa ordem.
O mesmo se aplica a _to_base_type()
e _from_base_type()
: os métodos na superclasse e na subclasse são combinados implicitamente.
(Não use super
para controlar o comportamento herdado para esta opção.
Para estes três métodos,
a interação é subtil e super
não faz o que quer.)