Search code examples
pythondjangodjango-querysetlazy-evaluation

Not lazy Queryset calls outside methods


I use Django in work and I'm still learning about some of its features - recently I ran into a problem with a Queryset call in one of my Views. While the code ran perfectly for me, it broke on the CI pipeline set to initialize a project from scratch.

I got the answer that it's because of the non-lazy evaluation of said call - to be precise, it roughly looked like this:

class MyListView(ListView):
    queryset = (
        MyModel.objects.all()
        .exclude(some_type_field__in=[ContentType.objects.get_for_model(MyReferenceModel)])
        .distinct()
    )
    
    template_name = #...

In addition, I was told not to write Queryset calls that are not lazily evaluated outside of methods - and that the reason is that the Python interpreter executes code module by module.

I don't understand this part, however. How methods has anything to do with it? Why is it an issue if the code above, tries to actually hit the database?

To specify my question further, I show what was the solution I got:

class MyListView(ListView):
    model = MyModel

    def get_queryset(self):
        return (
            super()
                .get_queryset()
                .exclude(some_type_field__in=[ContentType.objects.get_for_model(MyReferenceModel)])
                .distinct()
        )

    template_name =  # ...

What's the actual difference between the two? I need to understand this problem better, and Django wiki was no help.


Solution

  • Difference is, code at module and class levels is executed when you import anything from this module.

    This is how python import system works, python just runs everything in .py file and puts it into global module "cache".

    Functions, on the other hand, are not executed unless called. The only thing that's executed is function definition signature. I.e.

    def f(arg=[][0]):
        return 1/0
    

    will give you IndexError at import time.

    You can check this by writing file like

    # my_module.py
    x = 4
    print(x)
    
    class A:
        y = input("Hey, I'm blocking!")
    
        def method(self):
            return input("Hey, I'm not blocking unless called!")
    

    And then from your code just try to from my_module import A.

    If you're working in environments with any i/o – web frameworks with databases – this is common practice to not have any i/o at import time. Usually, there's some machinery to help you with this. For example, in django that would be AppConfig hooks.