Écrire des sous-classes de propriétés

La classe Property est conçue pour être sous-classée. Cependant, il est normalement plus facile de sous-classer une sous-classe Property existante.

Le nom de tous les attributs spéciaux associés à Property, même ceux considérés comme "publics", commence par un trait de soulignement. En effet, StructuredProperty utilise l'espace de noms d'attribut non souligné pour faire référence aux noms Property imbriqués. Cette caractéristique est essentielle pour spécifier des requêtes sur des sous-propriétés.

La classe Property et ses sous-classes prédéfinies permettent un sous-classement à l'aide des API de validation et de conversion composables (ou empilables). Celles-ci nécessitent les définitions terminologiques suivantes :

  • Une valeur utilisateur correspond à une valeur définie et accessible par le code d'application à l'aide d'attributs standards sur l'entité.
  • Une valeur de base correspond à une valeur sérialisée vers et désérialisée depuis le datastore.

Une sous-classe Property qui met en œuvre une transformation spécifique entre des valeurs utilisateur et des valeurs sérialisables doit utiliser deux méthodes, _to_base_type() et _from_base_type(). Celles-ci ne doivent pas appeler leur méthode super(). C'est ce que l'on entend par des API composables (ou empilables).

L'API accepte les classes d'empilage et utilise une complexité accrue dans le cadre des conversions des valeurs utilisateur/de base : la conversion des valeurs utilisateur vers des valeurs de base devient de moins en moins complexe, tandis que la conversion des valeurs de base vers des valeurs utilisateur devient de plus en plus complexe. Par exemple, notez la relation entre BlobProperty, TextProperty et StringProperty. En effet, TextProperty hérite de BlobProperty ; son code est assez simple, car il hérite de la plupart des comportements dont il a besoin.

Outre _to_base_type() et _from_base_type(), la méthode _validate() constitue également une API composable.

L'API de validation fait la distinction entre les valeurs utilisateur laxistes et strictes. L'ensemble de valeurs laxistes correspond à un sur-ensemble de l'ensemble de valeurs strictes. La méthode _validate() utilise une valeur laxiste et la convertit en valeur stricte si nécessaire. Cela signifie que lors de la définition de la valeur de propriété, les valeurs laxistes sont acceptées, tandis que lors de l'obtention de la valeur de propriété, seules les valeurs strictes sont renvoyées. Si aucune conversion n'est nécessaire, la méthode _validate() peut renvoyer la valeur "None". Si l'argument est en dehors de l'ensemble de valeurs laxistes acceptées, _validate() devrait générer une exception, de préférence TypeError ou datastore_errors.BadValueError.

Les méthodes _validate(), _to_base_type() et _from_base_type() ne nécessitent pas de gérer les éléments suivants :

  • None : les méthodes ne sont pas appelées avec None (si "None" est renvoyé, cela signifie que la valeur n'a pas besoin de conversion).
  • Valeurs répétées : l'infrastructure se charge de l'appel de _from_base_type() ou _to_base_type() pour chaque élément de la liste dans une valeur répétée.
  • Distinction entre les valeurs utilisateur et les valeurs de base : l'infrastructure s'en charge en appelant les API composables.
  • Comparaisons : les opérations de comparaison appellent _to_base_type() sur leur opérande.
  • Distinction entre les valeurs utilisateur et les valeurs de base : l’infrastructure garantit que _from_base_type() est appelé avec une valeur de base (désencapsulée) et que _to_base_type() est appelé avec une valeur utilisateur.

Par exemple, supposons que vous ayez besoin de stocker des entiers très longs. L'élément standard IntegerProperty n'accepte que les entiers de 64 bits signés. La propriété peut stocker un entier plus long sous forme de chaîne ; il serait donc préférable que la classe de propriété gère la conversion. Une application utilisant la classe de propriété peut ressembler à ceci :

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)

Cette méthode semble simple et directe. Elle montre également l'utilisation de certaines options de propriété standards (par défaut et répétées). En tant qu'auteur de LongIntegerProperty, vous ne devez pas écrire de code récurrent pour que ces options fonctionnent. Il est plus facile de définir une sous-classe associée à une autre propriété, par exemple :

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

Lorsque vous définissez une valeur de propriété sur une entité, par exemple ent.abc = 42, la méthode _validate() est appelée et, si elle ne déclenche pas d'exception, la valeur est stockée sur l'entité. Lorsque vous écrivez l'entité dans le datastore, la méthode _to_base_type() est appelée et convertit la valeur en chaîne. Cette valeur est ensuite sérialisée par la classe de base, StringProperty. La chaîne d'événements inverse se produit lorsque l'entité est lue depuis le datastore. Les classes StringProperty et Property se chargent des autres détails, tels que la sérialisation et la désérialisation de la chaîne, la définition de la valeur par défaut et la gestion des valeurs de propriété répétées.

Dans cet exemple, la gestion des inégalités (requêtes utilisant <, <=, > et >=) nécessite davantage de travail. L'exemple de mise en œuvre suivant impose une taille maximale du nombre entier et stocke les valeurs sous forme de chaînes à longueur fixe :

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

Vous pouvez utiliser cette méthode de la même manière que LongIntegerProperty, mais vous devez transmettre le nombre de bits au constructeur de propriété, par exemple BoundedLongIntegerProperty(1024).

Vous pouvez sous-classer d'autres types de propriétés de manière similaire.

Cette approche fonctionne également pour stocker des données structurées. Supposons que vous disposiez d'une classe FuzzyDate Python qui représente une plage de dates ; elle utilise les champs first et last pour stocker le début et la fin de la plage de dates :

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

Vous pouvez créer une propriété FuzzyDateProperty dérivée de StructuredProperty. Malheureusement, cette dernière ne fonctionne pas avec les anciennes classes Python de base ; elle a besoin d'une sous-classe Model. Définissez donc une sous-classe Model comme une représentation intermédiaire.

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

Construisez ensuite une sous-classe de StructuredProperty qui code en dur l'argument "modelclass" en tant que FuzzyDateModel et définit les méthodes _to_base_type() et _from_base_type() pour effectuer la conversion entre FuzzyDate et 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)

Une application peut utiliser cette classe comme suit :

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

Supposons que vous souhaitiez accepter des objets date de base en plus des objets FuzzyDate en tant que valeurs pour FuzzyDateProperty. Pour ce faire, modifiez la méthode _validate() comme suit :

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

Vous pouvez plutôt sous-classer FuzzyDateProperty comme suit (en supposant que FuzzyDateProperty._validate() se présente comme indiqué ci-dessus).

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

Lorsque vous affectez une valeur à un champ MaybeFuzzyDateProperty, les méthodes MaybeFuzzyDateProperty._validate() et FuzzyDateProperty._validate() sont appelées, dans cet ordre. Il en va de même pour _to_base_type() et _from_base_type() : les méthodes de la super-classe et de la sous-classe sont implicitement combinées. Dans ce cas, n'utilisez pas super pour contrôler le comportement hérité. Pour ces trois méthodes, l’interaction est subtile, et super n'est pas adéquat.

Cette page vous a-t-elle été utile ? Évaluez-la :

Envoyer des commentaires concernant…

Environnement standard App Engine pour Python