Week 3: Searching for Definition of a Model Attribute


Posted:

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 SortedDict? Searching that source file for 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 course.videocourse come from? I asked. There's no 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:

  1. A foreign key adds an attribute to the target model in addition to the model where it is defined.
  2. 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).
  3. 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_names I would have found this.

Footnote:

  1. 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 *

Comments powered by Disqus