La clase Property
se ha diseñado para que se pueda crear una subclase a partir de ella.
Sin embargo, normalmente es más fácil crear una subclase de una subclase Property
.
Todos los atributos especiales Property
, incluso los que se consideran "públicos", tienen nombres que empiezan por un guion bajo.
Esto se debe a que StructuredProperty
usa el espacio de nombres de atributo sin guion bajo para hacer referencia a nombres
Property
anidados, lo cual es esencial para especificar consultas en
subpropiedades.
La clase Property
y sus subclases predefinidas permiten crear subclases mediante APIs de validación y conversión componibles (o apilables). Para ello, necesitamos definir algunos términos:
- Un valor de usuario es un valor que se define y al que se accede mediante el código de la aplicación usando atributos estándar de la entidad.
- Un valor base es un valor que se serializaría en y se deserializaría desde Datastore.
Una subclase Property
que implemente una transformación específica entre valores de usuario y valores serializables debe implementar dos métodos: _to_base_type()
y _from_base_type()
.
No deben llamar a su método super()
.
Esto es lo que se conoce como APIs componibles (o apilables).
La API admite clases de acumulación con conversiones de base de usuarios cada vez más sofisticadas: la conversión de usuario a base pasa de más sofisticada a menos sofisticada, mientras que la conversión de base a usuario pasa de menos sofisticada a más sofisticada. Por ejemplo, consulta la relación entre BlobProperty
, TextProperty
y StringProperty
.
Por ejemplo, TextProperty
hereda de BlobProperty
. Su código es bastante sencillo porque hereda la mayor parte del comportamiento que necesita.
Además de _to_base_type()
y _from_base_type()
, el método _validate()
también es una API componible.
La API de validación distingue entre los valores de usuario lax y strict. El conjunto de valores laxos es un superconjunto del conjunto de valores estrictos. El método _validate()
toma un valor laxo y, si es necesario, lo convierte en un valor estricto. Esto significa que, al definir el valor de la propiedad, se aceptan valores laxos, mientras que, al obtener el valor de la propiedad, solo se devuelven valores estrictos. Si no es necesario realizar ninguna conversión, _validate()
puede devolver None. Si el argumento está fuera del conjunto de valores laxos aceptados, _validate()
debería generar una excepción, preferiblemente TypeError
o datastore_errors.BadValueError
.
_validate()
, _to_base_type()
y _from_base_type()
no tienen que gestionar lo siguiente:
None
: no se llamará conNone
(y, si devuelve None, significa que el valor no necesita conversión).- Valores repetidos: la infraestructura se encarga de llamar a
_from_base_type()
o_to_base_type()
por cada elemento de la lista de un valor repetido. - Distinguir los valores de usuario de los valores base: la infraestructura se encarga de esto llamando a las APIs componibles.
- Comparaciones: las operaciones de comparación llaman a
_to_base_type()
en su operando. - Distinguir entre los valores de usuario y los valores base: la infraestructura garantiza que se llamará a
_from_base_type()
con un valor base (sin envolver) y que se llamará a_to_base_type()
con un valor de usuario.
Por ejemplo, supongamos que necesitas almacenar números enteros muy largos.
El estándar IntegerProperty
solo admite números enteros de 64 bits (con signo).
Es posible que tu propiedad almacene un número entero más largo como una cadena. Sería conveniente que la clase de propiedad gestionara la conversión.
Una aplicación que use tu clase de propiedad podría tener un aspecto similar al siguiente:
...
...
...
...
Parece sencillo y directo. También muestra el uso de algunas opciones de propiedad estándar (predeterminada, repetida). Como autor de LongIntegerProperty
, te alegrará saber que no tienes que escribir ningún código repetitivo para que funcionen. Es más fácil definir una subclase de otra propiedad. Por ejemplo:
Cuando asignas un valor de propiedad a una entidad, por ejemplo, ent.abc = 42
, se llama a tu método _validate()
y, si no genera una excepción, el valor se almacena en la entidad. Cuando escribes la entidad en Datastore, se llama al método _to_base_type()
, que convierte el valor en una cadena. A continuación, la clase base serializa ese valor,
StringProperty
.
La cadena de eventos inversa se produce cuando la entidad se lee de nuevo desde Datastore. Las clases StringProperty
y Property
se encargan de otros detalles, como serializar
y deserializar la cadena, definir el valor predeterminado y gestionar
los valores de propiedad repetidos.
En este ejemplo, admitir desigualdades (es decir, consultas que usan <, <=, >, >=) requiere más trabajo. En el siguiente ejemplo de implementación se impone un tamaño máximo de número entero y se almacenan los valores como cadenas de longitud fija:
Se puede usar de la misma forma que LongIntegerProperty
excepto que debes pasar el número de bits al constructor de la propiedad,
por ejemplo, BoundedLongIntegerProperty(1024)
.
Puedes crear subclases de otros tipos de propiedad de forma similar.
Este enfoque también funciona para almacenar datos estructurados.
Supongamos que tienes una clase de Python FuzzyDate
que representa un periodo. Utiliza los campos first
y last
para almacenar el inicio y el final del periodo:
...
Puedes crear un FuzzyDateProperty
que derive de
StructuredProperty
. Lamentablemente, esta última no funciona con las clases de Python antiguas, sino que necesita una subclase Model
.
Por lo tanto, define una subclase de Model como representación intermedia.
A continuación, crea una subclase de StructuredProperty
que codifica el argumento modelclass como FuzzyDateModel
y define los métodos _to_base_type()
y
_from_base_type()
para convertir entre FuzzyDate
y
FuzzyDateModel
:
Una aplicación podría usar esta clase de la siguiente manera:
...
Supongamos que quiere aceptar objetos date
sin formato además de objetos FuzzyDate
como valores de FuzzyDateProperty
. Para ello, modifica el método _validate()
de la siguiente manera:
En su lugar, puedes crear una subclase de FuzzyDateProperty
de la siguiente manera (suponiendo que FuzzyDateProperty._validate()
es como se muestra arriba).
Cuando asignas un valor a un campo MaybeFuzzyDateProperty
, se invocan MaybeFuzzyDateProperty._validate()
y FuzzyDateProperty._validate()
, en ese orden.
Lo mismo ocurre con _to_base_type()
y _from_base_type()
: los métodos de la superclase y la subclase se combinan implícitamente.
No uses super
para controlar el comportamiento heredado en este caso.
En estos tres métodos, la interacción es sutil y super
no hace lo que quieres.