Week 3: Searching for Definition of a Model Attribute
One of the things I really appreciate about Python is how easy it is to navigate from a line of code that mentions some class or function to the source where that name is defined. For example, given
headers = SortedDict()
You may ask What's a
Searching that source file for SortedDict
?SortedDict
, you find it's defined by
from django.utils.datastructures import SortedDict
From that you can expect with some confidence that it's in a file named django/utils/datastructures.py
. Granted, that file may be in a number of places (because sys.path
), but if you have any doubt it's quick to confirm:
>>> import django.utils.datastructures
>>> django.utils.datastructures.__file__
'/usr/lib/python2.7/dist-packages/django/utils/datastructures.py'
There are a number of variations that can make that slightly more complicated, but you should never have to guess or grep your entire hard drive. A competent IDE can navigate it for you in a keystroke. (Two of my most-used PyCharm shortcuts are Ctrl-q to open the docstring and Ctrl-b to take you to the definition of whatever your cursor is on.) Having fast and unambiguous access to definitions is an invaluable resource when reading code.1
I say this all by why of introduction as to why it drove me absolutely up the wall when I was reading code I could not find a definition for. It went something like this:
from django.db import models
class Course(models.Model):
def size(self):
if self.videocourse.duration > 90:
return "big"
else:
return "small"
Where's
I asked. There's no course.videocourse
come from?videocourse
attribute defined on the Course
class or its superclass. PyCharm said it couldn't find a definition either. I searched the module for anything doing videocourse =
and found no such assignments.
I knew the Django ORM does add attributes to models for backwards
relations, but there were no instances of ForeignKey(Course, related_name='videocourse')
anywhere, or any other ForeignKey(Course)
call that would result in a relation with that name.
What there was, however, was this:
class VideoCourse(Course):
duration = models.IntegerField()
But it didn't have any ForeignKey
(or other relationship fields) attributes to Course
, so surely it wouldn't implicitly add attributes to Course
instances, right?
Wrong. This is another instance where Kevin didn't read the documentation. Multi-table inheritance does create links between the parent model and its children.
Actually, in this case I do remember coming across the chapter on model inheritance in Two Scoops. I thought something along the lines of this seems complicated and not relevant to any database modelling problems I've had so far, why are they introducing it so early in the book?
and resolved not to use multi-table inheritance if I could avoid it. That plan doesn't work if you jump in to an existing project, though.
I think providing a way to look up foreign key relationships from both directions is a good one; it's convenient syntax for a very common use case. I also think the Python maxim of explicit is better than implicit
is very important for Python's reputation as a readable and clear language. Here we have three implicit decisions stacked on each other:
- A foreign key adds an attribute to the target model in addition to the model where it is defined.
- The name of that attribute (the
related_name
) has a default taken from the name of the source class, with some subtle transformation applied (just enough to confuse your normal use of search in a case-sensitive language). - Subclassing a model implicitly adds a foreign key to the parent model — sometimes, depending on options set in
Meta
on the parent (abstract
) or child (proxy
).
and I think the end result is not a good one for maintainability.
Fortunately, it looks like we can make some implicit things explicit.
class VideoCourse(Course):
course = models.OneToOneField(Course, parent_link=True,
related_name="video_course")
duration = models.IntegerField()
Using parent_link
with related_name
lets us make items #2 and #3 above both explicit, and also makes the attribute name follow our naming conventions (video course
is two words, not a compound word). With that, I still wouldn't have seen anything explicitly assigning to video_course
, but when I started looking for matching related_name
s I would have found this.
Footnote:
-
This is something I've sorely missed in my brief forays into Ruby on Rails, but that's another story.
This is also why we never use
import *