1

Currently I have the following Django model

class TestUser(models.Model):
    name = models.CharField(max_length=250)
    email = models.CharField(max_length=250)
    ...

    class Meta:
        db_table = 'temp_user'

and I've the following method.

def print_name(self):
    return self.name

I want to add this method as a property to TempUser model. I know this can be done by putting the method inside TempUser class and user @property. But I want to do it dynamically.

I tried this from python shell.

In [10]: TempUser.print_name = property(print_name)

In [11]: TempUser.print_name
Out[11]: <property at 0x7efc374e7c58>

In [12]: user = TempUser.objects.get(pk=1)

In [13]: user.print_name
Out[13]: u'Test User'

But once I exit the shell, I loose the property. Is there any way to add the property permanently.

Pattu
  • 2,591
  • 6
  • 29
  • 37
  • 1
    Why do you need to do this? What other properties do you need to dynamically add to your model? – Blender Jan 18 '18 at 05:30
  • https://stackoverflow.com/questions/1325673/how-to-add-property-to-a-class-dynamically A huge thread is this, I believe this must be useful in your case. – Anup Yadav Jan 18 '18 at 05:31
  • `print_name` was just an example. But we've some business requirements to collect and store user's data like income, education etc..and make them accessible via TempUser object. – Pattu Jan 18 '18 at 05:33
  • have you tried `setattr(TempUser,'print_name',property(print_name))` – Abdul Niyas P M Jan 18 '18 at 05:34
  • 1
    What exactly are you trying to accomplish? Properties are limited in usefulness because you cannot query against them (because they do not actually exist in the DB) -- SQLAlchemy has a `hybrid_property` that lets you do queries against properties. A way you can achieve something similar with Django that allows you to query against virtual fields is by [annotating](https://docs.djangoproject.com/en/2.0/ref/models/querysets/#annotate) the default queryset by adding an annotation to the model manager's `get_queryset`. That may fit your case. Performance implications do apply. – sytech Jan 18 '18 at 05:36
  • I understand that properties can't be used in sql queries. I'm planning to use them in Python code only. – Pattu Jan 18 '18 at 05:39
  • I also tried `setattr` thingy. Didn't seem to work. – Pattu Jan 18 '18 at 05:40
  • 1
    @Pattu: You'll have to be more clear about what you're actually trying to accomplish. Are these properties all simple expressions of `TestUser`'s columns or can they contain arbitrary Python code? Are they front-facing and will they be created/deleted by a (possibly malicious) user? Why can't this user just edit the `models.py` file? – Blender Jan 18 '18 at 05:41
  • 1
    But what problem does this solve? I'd strongly recommend avoiding this. Adding properties/attributes onto instances dynamically like this is next-to-useless and arguably hazardous to your application design. If you want to use properties, implement them with the `@property` method decorator. There are many cases where your models are serialized and otherwise reloaded from the db, in which case, any modifications you made to the object not in the DB are lost, even if it *seems* like you may be passing the object directly. – sytech Jan 18 '18 at 05:42
  • We have a separate table called `TempUserDetails` where we store various data of the user like income, education etc. Ex - check if the user has high income (>100k). In this case, I'll use the property to internally query `TempUserDetails` table and get proper value – Pattu Jan 18 '18 at 05:50
  • @Pattu: Are these just properties for you to personally use while querying the database in an interactive shell? Or will you be utilizing these properties in code? – Blender Jan 18 '18 at 05:58
  • I want to utilise these properties inside our django codebase. – Pattu Jan 18 '18 at 06:00
  • 1
    @Pattu: Why can't you place them into `models.py`? – Blender Jan 18 '18 at 06:20
  • @Pattu as already mentioned, you cannot query a table using attributes you set at runtime like that. Why can't you just do `Users.objects.filter(income__gt=hundred_thousand)`? Or using annotation/aggregation, which seems to be the best way to achieve this if you're trying to query against something other than fields that already exist in the table. – sytech Jan 18 '18 at 06:59

1 Answers1

2

But once I exit the shell, I loose the property. Is there any way to add the property permanently.

No, not through attributes you set at run-time. Data associated with Django model instances is persisted through the database. Your only option for persisting data is to create a field in the database with which to store the information. If you want to persist methods available on a class, edit the source code.

You've expressed a desire to do this dynamically at runtime, too. However, the benefit of doing so is dubious, at best, and likely hazardous to your code base. It's hard to imagine a demonstrable use-case where this solves a real problem that does not already have a better solution. There is almost certainly a better way to get at your goal. If your goal is to keep your code DRY, consider other patterns like inheriting from abstract models.

You can implement regular properties (using the @property method decorator) on your model class that can take existing fields and look at related model fields to compose information on-the-fly, but it is still not persisted in the database...

For example, if you have a model that has a start_time and end_time you could add a total_time property without necessarily needing to create a field for it.

class MyModel(models.Model):
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()

    @property
    def total_time(self):
        return self.end_time - self.start_time

However, doing this would not allow you to query against this property. MyModel.objects.filter(total_time__lt=delta) for example would not be possible with a property alone.

Some other options you have include annotation and aggregation which can be done dynamically at run-time and gives you the benefit of being able to query against your database.

Using the same example as above, rather than using @property one can annotate a queryset in a similar way, which also lets you query the database on this 'virtual field' and even pass that annotated queryset around. You can even do arithmetic and aggregations like sum, average, and more...

from django.db.models import F, ExpressionWrapper, fields

duration = ExpressionWrapper(F('end_time') - F('start_time'), output_field=fields.DurationField())

qs = MyModel.objects.annotate(duration=duration)
# query for objects with a delta of more than five minutes
results = qs.filter(duration__gt=five_minutes)

See also Query expressions

In short: no, and if you could, it's almost definitely a bad idea. Stick with the established methods of doing things. Django is a very opinionated framework, it is designed for you to do things 'the django way'. You will likely only hurt yourself by going against the grain.

sytech
  • 7,848
  • 2
  • 22
  • 49