views:

1316

answers:

9

I was having a debate on this with some colleagues. Is there a preferred way to retrieve an object in Django when you're expecting only one?

The two obvious ways are:

   try: 
      obj = MyModel.objects.get(id=1)
   except MyModel.DoesNotExist:
      # we have no object!  do something
      pass

and

   objs = MyModel.objects.filter(id=1)
   if len(objs) == 1:
      obj = objs[0]
   else: 
      # we have no object!  do something
      pass

The first method seems behaviorally more correct, but uses exceptions in control flow which may introduce some overhead. The second is more roundabout but won't ever raise an exception.

Any thoughts on which of these is preferable? Which is more efficient?

+1  A: 

Interesting question, but for me option #2 reeks of premature optimisation. I'm not sure which is more performant, but option #1 certainly looks and feels more pythonic to me.

John McCollum
+4  A: 

I can't speak with any experience of Django but option #1 clearly tells the system that you are asking for 1 object, whereas the second option does not. This means that option #1 could more easily take advantage of cache or database indexes, especially where the attribute you're filtering on is not guaranteed to be unique.

Also (again, speculating) the second option may have to create some sort of results collection or iterator object since the filter() call could normally return many rows. You'd bypass this with get().

Finally, the first option is both shorter and omits the extra temporary variable - only a minor difference but every little helps.

Kylotan
+12  A: 

get() is provided specifically for this case. Use it.

Option 2 is almost precisely how the get() method is actually implemented in Django, so there should be no "performance" difference (and the fact that you're thinking about it indicates you're violating one of the cardinal rules of programming, namely trying to optimize code before it's even been written and profiled -- until you have the code and can run it, you don't know how it will perform, and trying to optimize before then is a path of pain).

James Bennett
+3  A: 

1 is correct. In Python an exception has equal overhead to a return. For a simplified proof you can look at this.

2 This is what Django is doing in the backend. get calls filter and raises an exception if no item is found or if more than one than one object is found.

+1  A: 

Why do all that work? Replace 4 lines with 1 builtin shortcut. (This does its own try/except.)

from django.shortcuts import get_object_or_404

obj = get_object_or_404(MyModel, id=1)
This is great when it's the desired behavior, but sometimes, you might want to create the missing object, or the pull was optional information.
TokenMacGuy
+2  A: 

Some more info about exceptions. If they are not raised, they cost almost nothing. Thus if you know you are probably going to have a result, use the exception, since using a conditional expression you pay the cost of checking every time, no matter what. On the other hand, they cost a bit more than a conditional expression when they are raised, so if you expect not to have a result with some frequency (say, 30% of the time, if memory serves), the conditional check turns out to be a bit cheaper.

But this is Django's ORM, and probably the round-trip to the database, or even a cached result, is likely to dominate the performance characteristics, so favor readability, in this case, since you expect exactly one result, use get().

TokenMacGuy
+2  A: 

You can install a module called django-annoying and then do this:

from annoying.functions import get_object_or_None

obj = get_object_or_None(MyModel, id=1)

if not obj:
    #omg the object was not found do some error stuff
nbv4
A: 

Option 1 is more elegant, but be sure to use try..except.

From my own experience I can tell you that sometimes you're sure there cannot possibly be more than one matching object in the database, and yet there will be two... (except of course when getting the object by its primary key).

zooglash
A: 

Why on Earth would I want to wrap every .get() in a try/except? Why would the manager's get() not adopt behavior similar to the built-in dict's get() method? It's not Pythonic, I'll argue, to make the programmer wrap such a common idiom in try/except every time. As it is, I spent 20 minutes trying to find the proper way to do a get because I couldn't believe the try/except would need to be handled manually every time by design.

David