Escrever subclasses de propriedades

A classe Property foi concebida para ser uma subclasse. No entanto, normalmente, é mais fácil criar uma subclasse de uma subclasse Propertyexistente.

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 com None (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:

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)

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:

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

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:

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

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:

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

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;

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

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:

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)

Uma aplicação pode usar esta classe da seguinte forma:

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()

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:

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

Em alternativa, pode criar uma subclasse de FuzzyDateProperty da seguinte forma (partindo do princípio de que FuzzyDateProperty._validate() é como mostrado acima).

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

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.)