Escribe subclases de propiedades

La clase Property está diseñada para subclasificarse. Sin embargo, por lo general, es más fácil subclasificar una subclase Property existente.

Todos los atributos de Property especiales, incluso aquellos considerados “públicos”, tienen nombres que comienzan con un guion bajo. Esto se debe a que StructuredProperty utiliza el espacio de nombres de atributos sin guion bajo para referirse a nombres de Property anidados; esto es esencial para especificar consultas sobre subpropiedades.

La clase Property y sus subclases predefinidas permiten crear subclases con las API de conversión y validación acoplables (o apilables). Estas requieren algunas definiciones de terminología:

  • Un valor de usuario es un valor como el código de la aplicación que se establecerá y accederá mediante los atributos estándar de la entidad.
  • Un valor base es un valor que se serializará y deserializará desde el almacén de datos.

Una subclase Property que implementa una transformación específica entre valores de usuario y valores serializables debe implementar dos métodos, _to_base_type() y _from_base_type(). Estos no deben llamar al método super(). Esto es lo que se entiende por API acoplables (o apilables).

La API admite clases de apilado con conversiones de usuario cada vez más sofisticadas: la conversión de usuario a base va de más sofisticada a menos sofisticada, mientras que la conversión de base a usuario va de menos sofisticada a más sofisticada. Por ejemplo, mira la relación entre BlobProperty, TextProperty y StringProperty. Por ejemplo, TextProperty se hereda de BlobProperty; su código es bastante simple 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 acoplable.

La API de validación distingue entre valores de usuario estrictos y laxos. 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, cuando se establece el valor de la propiedad, se aceptan valores laxos, mientras que, cuando se obtiene el valor de la propiedad, solo se mostrarán los valores estrictos. Si no se necesita conversión, _validate() puede mostrar None. Si el argumento está fuera del conjunto de valores laxos aceptados, _validate() debe generar una excepción, preferentemente TypeError o datastore_errors.BadValueError.

_validate(), _to_base_type() y _from_base_type() no necesitan controlar lo siguiente:

  • None: no se llamarán con None (y si muestran Ninguna, esto significa que el valor no necesita conversión).
  • Valores repetidos: La infraestructura se encarga de llamar a _from_base_type() o _to_base_type() para cada elemento de la lista en un valor repetido.
  • Distinguir los valores de usuario de los valores básicos: la infraestructura controla esto llamando a las API compuestas.
  • Comparaciones: Las operaciones de comparación llaman a _to_base_type() en su operando.
  • Distinción entre los valores de usuario y base: la infraestructura garantiza que _from_base_type() se llamará con un valor de base (sin envolver), y que a _to_base_type() se llamará con un valor de usuario.

Por ejemplo, imagina que necesitas almacenar enteros realmente largos. IntegerProperty estándar solo admite enteros de 64 bits (firmados). Tu propiedad puede almacenar un número entero tan largo como una string; sería bueno que la clase de propiedad controlara la conversión. Una aplicación que use tu clase de propiedad podría presentar este aspecto

from datetime import date

import my_models
...
class MyModel(ndb.Model):
    name = ndb.StringProperty()
    abc = LongIntegerProperty(default=0)
    xyz = LongIntegerProperty(repeated=True)
...
# Create an entity and write it to the Datastore.
entity = my_models.MyModel(name='booh', xyz=[10**100, 6**666])
assert entity.abc == 0
key = entity.put()
...
# Read an entity back from the Datastore and update it.
entity = key.get()
entity.abc += 1
entity.xyz.append(entity.abc//3)
entity.put()
...
# Query for a MyModel entity whose xyz contains 6**666.
# (NOTE: using ordering operations don't work, but == does.)
results = my_models.MyModel.query(
    my_models.MyModel.xyz == 6**666).fetch(10)

Esto parece simple y directo. También demuestra el uso de algunas opciones de propiedad estándar (predeterminado, repetido) como autor de LongIntegerProperty, te alegrará saber que no tienes que escribir ningún “código estándar” para que funcionen. Es más fácil definir una subclase de otra propiedad, por ejemplo:

class LongIntegerProperty(ndb.StringProperty):
    def _validate(self, value):
        if not isinstance(value, (int, long)):
            raise TypeError('expected an integer, got %s' % repr(value))

    def _to_base_type(self, value):
        return str(value)  # Doesn't matter if it's an int or a long

    def _from_base_type(self, value):
        return long(value)  # Always return a long

Cuando configuras un valor de propiedad en una entidad, p. ej., 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 a tu método _to_base_type() y se convierte el valor en la string. Entonces ese valor se serializa por la clase base, StringProperty. La cadena inversa de eventos ocurre cuando la entidad se lee desde Datastore. Las clases StringProperty y Property se ocupan de los demás detalles, como la serialización y deserialización de la string, la configuración predeterminada y el control de valores de propiedad repetidos.

En este ejemplo, admitir desigualdades (es decir, consultas que utilizan <, <=, >, >=) requiere más trabajo. La siguiente implementación de ejemplo impone un tamaño máximo de número entero y almacena valores como strings de longitud fija:

class BoundedLongIntegerProperty(ndb.StringProperty):
    def __init__(self, bits, **kwds):
        assert isinstance(bits, int)
        assert bits > 0 and bits % 4 == 0  # Make it simple to use hex
        super(BoundedLongIntegerProperty, self).__init__(**kwds)
        self._bits = bits

    def _validate(self, value):
        assert -(2 ** (self._bits - 1)) <= value < 2 ** (self._bits - 1)

    def _to_base_type(self, value):
        # convert from signed -> unsigned
        if value < 0:
            value += 2 ** self._bits
        assert 0 <= value < 2 ** self._bits
        # Return number as a zero-padded hex string with correct number of
        # digits:
        return '%0*x' % (self._bits // 4, value)

    def _from_base_type(self, value):
        value = int(value, 16)
        if value >= 2 ** (self._bits - 1):
            value -= 2 ** self._bits
        return value

Se puede usar de la misma manera que LongIntegerProperty, con la excepción de que debes pasar la cantidad de bits al constructor de propiedades, p. ej., BoundedLongIntegerProperty(1024).

Puedes subclasificar otros tipos de propiedades de manera similar.

Este enfoque también funciona para almacenar datos estructurados. Supongamos que tienes una clase de Python FuzzyDate que representa un período, esta usa los campos first y last para almacenar el principio y el final del período:

from datetime import date

...
class FuzzyDate(object):
    def __init__(self, first, last=None):
        assert isinstance(first, date)
        assert last is None or isinstance(last, date)
        self.first = first
        self.last = last or first

Puedes crear una FuzzyDateProperty que derive de StructuredProperty. Desafortunadamente, esta última no funciona con clases antiguas de Python y necesita una subclase Model. Por lo tanto, define una subclase de modelo como una representación intermedia;

class FuzzyDateModel(ndb.Model):
    first = ndb.DateProperty()
    last = ndb.DateProperty()

A continuación, crea una subclase de StructuredProperty que codifique el argumento de clase de modelo para que sea FuzzyDateModel y define los métodos _to_base_type() y _from_base_type() para convertir entre FuzzyDate y FuzzyDateModel:

class FuzzyDateProperty(ndb.StructuredProperty):
    def __init__(self, **kwds):
        super(FuzzyDateProperty, self).__init__(FuzzyDateModel, **kwds)

    def _validate(self, value):
        assert isinstance(value, FuzzyDate)

    def _to_base_type(self, value):
        return FuzzyDateModel(first=value.first, last=value.last)

    def _from_base_type(self, value):
        return FuzzyDate(value.first, value.last)

Una aplicación podría usar esta clase así:

class HistoricPerson(ndb.Model):
    name = ndb.StringProperty()
    birth = FuzzyDateProperty()
    death = FuzzyDateProperty()
    # Parallel lists:
    event_dates = FuzzyDateProperty(repeated=True)
    event_names = ndb.StringProperty(repeated=True)
...
columbus = my_models.HistoricPerson(
    name='Christopher Columbus',
    birth=my_models.FuzzyDate(date(1451, 8, 22), date(1451, 10, 31)),
    death=my_models.FuzzyDate(date(1506, 5, 20)),
    event_dates=[my_models.FuzzyDate(
        date(1492, 1, 1), date(1492, 12, 31))],
    event_names=['Discovery of America'])
columbus.put()

# Query for historic people born no later than 1451.
results = my_models.HistoricPerson.query(
    my_models.HistoricPerson.birth.last <= date(1451, 12, 31)).fetch()

Supongamos que deseas aceptar objetos date sin formato además de los objetos FuzzyDate como valores para FuzzyDateProperty. Para hacerlo, modifica el método _validate() de la siguiente manera:

def _validate(self, value):
    if isinstance(value, date):
        return FuzzyDate(value)  # Must return the converted value!
    # Otherwise, return None and leave validation to the base class

En su lugar, puedes subclasificar FuzzyDateProperty de la siguiente manera (suponiendo que FuzzyDateProperty._validate() es como se muestra arriba).

class MaybeFuzzyDateProperty(FuzzyDateProperty):
    def _validate(self, value):
        if isinstance(value, date):
            return FuzzyDate(value)  # Must return the converted value!
        # Otherwise, return None and leave validation to the base class

Cuando asignas un valor a un campo MaybeFuzzyDateProperty, se invocan MaybeFuzzyDateProperty._validate() y FuzzyDateProperty._validate(), en ese orden. Lo mismo se aplica a _to_base_type() y _from_base_type(): los métodos en superclase y subclase se combinan implícitamente. (No uses super para controlar el comportamiento heredado para esto. En cuanto a estos tres métodos, la interacción es sutil y super no lleva a cabo lo que quieres).