2
votes

Comment stocker un nombre complexe dans le modèle Django

J'ai besoin de stocker un nombre complexe dans un modèle Django. Pour ceux qui oublient, cela signifie simplement Z = R + jX où R et X sont des nombres réels représentant les composants réels et imaginaires du complexe. Il y aura des numéros individuels, ainsi que des listes qui doivent être stockées. Mes recherches jusqu'à présent n'ont pas fourni une bonne solution pour les listes, j'ai donc l'intention de laisser la base de données gérer la liste comme des enregistrements individuels.

Je vois deux options pour stocker un nombre complexe:

1) créer un champ personnalisé: class Complex (models.CharField) Cela me permettrait de personnaliser tous les aspects du champ, mais c'est beaucoup de travail supplémentaire pour la validation si cela doit être fait correctement. L'avantage majeur est qu'un seul nombre est représenté par un seul champ dans le tableau.

2) laissez chaque nombre complexe être représenté par une ligne, avec un champ float pour le partie réelle, R, et un autre champ float pour la partie imaginaire, X. L'inconvénient de cette approche est que j'aurais besoin d'écrire des convertisseurs qui créeront un nombre complexe à partir des composants, et vice versa . L'avantage est que la base de données ne le verra que comme un autre enregistrement.

Ce problème a sûrement été résolu dans le passé, mais je ne trouve pas de bonnes références, sans parler d'une référence particulière à Django. p>

C'est ma première fissure sur le terrain, elle est basée sur un autre exemple que j'ai trouvé qui impliquait quelques manipulations de cordes. Ce qui n'est pas clair pour moi, c'est comment et où différentes validations doivent être effectuées (comme forcer un simple flottant en un nombre complexe en ajoutant + 0j). J'ai l'intention d'ajouter également des fonctionnalités de formulaire, afin que le champ se comporte comme un champ flottant, mais avec des restrictions ou des exigences supplémentaires.

Je n'ai pas encore testé ce code, il peut donc y avoir des problèmes avec celui-ci. Il est basé sur le code d'une réponse à cette question SO. Il apparaît après l'exécution du code que des changements ont eu lieu dans les noms de méthodes.

Quelle est la manière la plus efficace de stocker une liste dans les modèles Django?

class ComplexField(models.CharField):

    description = 'A complex number represented as a string'

    def __init__(self, *args, **kwargs):
        kwargs['verbose_name'] = 'Complex Number'
        kwargs['max_length'] = 64
        kwargs['default'] = '0+0j'

        super().__init__(*args, **kwargs)

    def to_python(self, value):
        if not value: return
        if isinstance(value, complex):
            return value
        return complex(value)

    def get_db_prep_value(self, value):
        if not value: return
        assert(isinstance(value, complex))
        return str(item)[1:-1]

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)


3 commentaires

On dirait que vous décrivez exactement le scénario qu'un champ personnalisé est censé traiter. Il est préférable que la conversion vers / depuis soit aussi proche que possible de la base de données afin de ne pas avoir à vous rappeler de convertir une chaîne en un nombre complexe dans vos modèles ou vues.


D'accord, je vais devoir trouver un bon exemple pour les champs personnalisés. Je suis surpris que rien ne soit encore apparu dans mes recherches. La plupart de ce qui est renvoyé pour «complexe» sont des choses perçues comme «difficiles», donc cela ne facilite pas les choses.


Tant que vous n'avez pas besoin d'interroger le ComplexField autrement que sous forme de chaînes, votre approche semble bonne. Cela devient plus poilu si vous avez besoin d'interroger de manière plus complexe.


3 Réponses :


1
votes

Si votre expression à chaque fois comme R + jX, vous pouvez créer la classe suivante

class ComplexNumber(models.Model):
    real_number = models.FloatField('Real number part')
    img_number = models.FloatFoeld('Img number part')

    def __str__(self):
        return complex(self.real_number, self.img_number)

et gérer la chaîne de résultat avec python voir ici

Si vous avez plusieurs parties real et img, vous pouvez gérer cela avec des clés étrangères ou des champs ManyToMany. Cela dépend peut-être de vos besoins.


9 commentaires

Merci d'avoir développé ce @Tobit. Chaque enregistrement serait un nombre, donc la seule ForeignKey qui devrait être requise est celle qui le lie à l'objet propriétaire. La déduplication semble également être une idée, mais je ne vais pas y toucher pendant un moment.


@Brian, ravi de vous aider. Vous pouvez peut-être nous montrer la solution finale ou marquer ma réponse comme correcte.


Cela entraînerait une recherche supplémentaire pour chaque ComplexNumber, ce qui semble un peu malheureux.


@AKX pouvez-vous expliquer ce que vous voulez dire? Si vous essayez de stocker quelque chose dans une base de données, vous devez l'obtenir à partir de la base de données? La classe stocke le nombre complexe complet. Sinon, vous pouvez utiliser un filtre, des annotations ou autre chose pour n'avoir qu'une seule recherche pour plusieurs ComplexNumbers. Je ne comprends pas votre commentaire.


Je suppose que l'intention du PO est de pouvoir stocker des nombres complexes dans un modèle de la même manière qu'ils stockeraient, par exemple, des flottants ou des décimales, c'est-à-dire dans des colonnes, et non comme des références de clé étrangère à une autre table.


avec la prélecture liée, vous pouvez éviter des requêtes de base de données supplémentaires dans votre modèle ou votre script docs.djangoproject.com/en/2.1/topics/db/optimization/...


Je suis conscient de prefetch_related / select_related, oui, mais même ainsi, Django ayant besoin de récupérer ces lignes et de construire les objets du modèle sera un peu lourd.


Dernier commentaire de ma part, nous n'avons aucun problème avec la prélecture liée et une base de données massive. À mon avis, l'OP devrait décider si c'est une manière valable de gérer son problème. Si vous avez une autre option, n'hésitez pas à expliquer.


Certains sont un peu au-dessus de ma tête mais je vais essayer. Je ne filtrerais pas ou ne classerais pas par valeur, je ne me soucie que d'obtenir et de définir la valeur en fonction du PK. Les valeurs seraient utilisées dans d'autres modèles soit comme des nombres individuels, soit comme des éléments d'une liste ou d'un tuple. De nombreuses opérations et validations (mais pas toutes) s'appliquent à partir de float, et il peut être utile de définir l'opération d'ajout (par exemple) comme R_sum = R1 + R2 et X_sum = X1 + X2 < / code> ou quelque chose comme ça. Je vais être honnête que je n'ai pas du tout considéré les performances de la base de données ... faire entrer et sortir des données de la base de données est ma principale préoccupation pour le moment.



1
votes

En ce qui concerne les champs personnalisés, vous avez probablement trouvé la partie pertinente dans le Documentation Django déjà.

La question de savoir si un champ personnalisé (ou un type de base de données personnalisé, voir ci-dessous) vaut vraiment la peine dépend vraiment de ce que vous devez faire avec les nombres stockés. Pour le stockage et quelques déplacements occasionnels, vous pouvez opter pour la solution la plus simple et sensée (votre numéro deux amélioré par Tobit).

Avec PostgreSQL, vous avez la possibilité d'implémenter des types personnalisés directement dans la base de données, y compris opérateurs . Voici la partie pertinente de la documentation Postgres , avec un exemple de nombres complexes, rien de moins.

Bien sûr, vous devez ensuite exposer le nouveau type et les opérateurs à Django. Un peu de travail, mais vous pourriez alors faire de l'arithmétique avec des champs individuels directement dans la base de données en utilisant Django ORM.


3 commentaires

J'utilise MariaDB et j'aimerais garder les choses indépendantes de DB pour le moment. Je recherche une solution qui équilibre l'effort initial, avec la possibilité d'aller suffisamment loin dans le projet pour repenser certaines choses si nécessaire (j'apprends encore Django, j'ai l'impression de n'avoir fait qu'effleurer la surface). Je penche vers ce sur quoi @Tobit a développé. J'ai besoin de comprendre un peu plus les méthodes des modèles.


J'ai fini par implémenter un champ personnalisé, et cela fonctionne assez bien pour que je l'oublie.


C'est bon à entendre! Vous pouvez publier votre solution comme réponse; il serait utile que d'autres s'attaquent au problème.



0
votes

Pour être honnête, je diviserais simplement le nombre complexe en deux champs flottants / décimaux et ajouterais une propriété de lecture et d'écriture sous forme de nombre complexe unique.

J'ai créé ce champ personnalisé qui se termine par un divise le champ sur le modèle réel et injecte également la propriété susmentionnée.

  • contrib_to_class s'appelle profondément dans la machinerie du modèle Django pour tous les champs qui sont déclarés sur le modèle. Généralement, ils peuvent simplement ajouter le champ lui-même au modèle, et peut-être des méthodes supplémentaires comme get_latest_by _... , mais ici nous détournons ce mécanisme pour ajouter à la place deux champs que nous construisons à l'intérieur, et non le le champ "self" réel lui-même, car il n'a pas besoin d'exister comme colonne de base de données. (Cela pourrait casser quelque chose, qui sait ...) Une partie de ce mécanisme est expliqué ici dans le wiki Django.

  • La classe ComplexProperty est une descripteur de propriété , qui permet de personnaliser ce qui se passe lorsque la propriété "attachée en tant que" dans une instance est accédée (en lecture ou en écriture). (Le fonctionnement des descripteurs dépasse un peu le cadre de cette réponse, mais il existe un guide pratique dans la documentation Python .)

NB: Je n'ai pas testé cela au-delà de l'exécution de migrations, donc les choses peuvent être interrompues de manière inattendue, mais au moins la théorie est solide. :)

migrations.CreateModel(
    name="Test",
    fields=[
        (
            "id",
            models.AutoField(
                auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
            ),
        ),
        ("num1_real", models.FloatField()),
        ("num1_imag", models.FloatField()),
        ("num2_real", models.FloatField()),
        ("num2_imag", models.FloatField()),
        ("num3_real", models.FloatField()),
        ("num3_imag", models.FloatField()),
    ],
)

La migration pour cela ressemble à

from django.db import models


class ComplexField(models.Field):
    def __init__(self, **kwargs):
        self.field_class = kwargs.pop('field_class', models.FloatField)
        self.field_kwargs = kwargs.pop('field_kwargs', {})
        super().__init__(**kwargs)

    def contribute_to_class(self, cls, name, private_only=False):
        for field in (
            self.field_class(name=name + '_real', **self.field_kwargs),
            self.field_class(name=name + '_imag', **self.field_kwargs),
        ):
            field.contribute_to_class(cls, field.name)

        setattr(cls, name, ComplexProperty(name))


class ComplexProperty:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if not instance:
            return self
        real = getattr(instance, self.name + '_real')
        imag = getattr(instance, self.name + '_imag')
        return complex(real, imag)

    def __set__(self, instance, value: complex):
        setattr(instance, self.name + '_real', value.real)
        setattr(instance, self.name + '_imag', value.imag)


class Test(models.Model):
    num1 = ComplexField()
    num2 = ComplexField()
    num3 = ComplexField()


donc comme vous pouvez le voir, les trois ComplexField Les sont décomposés en six FloatField .


8 commentaires

Pouvez-vous fournir une description un peu plus détaillée? Cela ressemble à mon option 2) mais je ne suis pas du tout familier avec les propriétés, les attributs dans le sens de leur lecture / écriture (en particulier la méthode contrib_to_class ). Je comprends pour la propriété que vous définissez simplement le setter et le getter. De plus, cette méthode de setter / getter s'étend-elle bien pour la validation de formulaire / entrée?


@Brian There - espérons que cela vous aidera.


Je veux apprendre, donc j'apprécie les détails supplémentaires. Je vois que la migration produit les colonnes _real et _imag , et l'interface d'administration a produit les deux champs de formulaire. Est-ce dû à la méthode contrib_to_class , ou à la propriété, ou aux deux?


La méthode contrib_to_class ajoute les deux champs. Pour être honnête, je n'ai même pas vraiment pensé à l'administration et aux autres interfaces utilisateur générées automatiquement (telles que ModelForms) se terminant avec les deux champs séparés, donc cette solution est définitivement un peu maladroite en ce sens.


Pour être honnête, il y aura peu de cas où les données seront sous forme d'affichage ou d'édition. La vue ou le modèle lui-même lit et écrit dans la base de données dans le cadre de l'enregistrement des paramètres ou des résultats dans la base de données. La saisie de données en tant que champs réels et imaginaires évite également beaucoup d'autres validations pour les espaces, les chiffres valides, etc., ce qui est une amélioration significative par rapport à la validation d'une chaîne entière dans un CharField ou quelque chose du genre.


Comment modifier les différentes propriétés du champ rendu / géré individuellement, comme ajouter une valeur par défaut à chaque FloatField par exemple? Les champs individuels fonctionnent très bien car ils gèrent nativement la notation scientifique sans aucun code supplémentaire.


Cela est déjà pris en charge par le biais du field_class / field_kwargs kwargs au ComplexField : x = ComplexField (field_kwargs = {'default': 8}) ferait 8 comme valeur par défaut pour x_real et x_imag . Ce serait une bonne idée d'ajouter le traitement à la déstructure ComplexField (default = 6 + 1j) dans les composants cependant :)


Merci d'avoir répondu. Je vais examiner ce que vous avez décrit. Ce que vous avez fourni jusqu'à présent fonctionne très bien. Je ne sais pas pourquoi quelqu'un vous a voté contre.