Welcome to Python Corner: Anvil scenarios with Pythonic solutions, inspired by the Python Cookbook.

Lazy-Loading Module Attributes with __getattr__

So, here’s a challenge for you:

I’ve got data I use in many places. I only want to load it once. And I don’t want to load it until I need it.

Let’s pick a concrete example: Say you’ve got a dropdown list, and you use it in several places in your app’s user interface – for example the article category selector in the News Aggregator Tutorial. To load the items for that DropDown, you’re making a server call to a function like this:

@anvil.server.callable
def get_dropdown_categories():
    return [(row['category'], row) for row in app_tables.categories.search()]

And for every form in your UI that includes one of these dropdowns, you have code like this:

class Form1(Form1Template):
    def __init__(self, **properties):
        ...
        self.category_dropwdown.items = anvil.server.call('get_dropdown_categories')
        # Every time a form loads I make a round trip to the server

This means you’re calling this function each time one of these forms loads. The data is the same every time, so that’s a lot of unnecessary round trips to the server!

Solution

There are a number of ways to tackle this problem. Let’s start with the simplest: a global variable in a client-side module:

The non-lazy module

We define a client-side module called Global:

# Global module

dropdown_categories = anvil.server.call('get_dropdown_categories')

Then we define a form, and in the drag-and-drop UI designer we create a DropDown called category_dropdown. We can initialise it like so:

# Inside Our Form

from .. import Global # use the autocomplete to ensure the correct .. path

class Form1(Form1Template):
    def __init__(self, **properties):
        self.init_components(**properties)
        self.category_dropdown.items = Global.dropdown_categories

This is a fine solution. We only do one call to the server, and it happens the first time the Global module is loaded. All subsequent times we use the cached value for dropdown_categories.

But we end up with a new problem. If we want more variables like this within our Global module, and each variable requires its own server call, we could end up making a lot of server calls when we first load this module. And we probably don’t need all that information to display our first screen!

Load data incrementally with lazy modules

I recently discovered you can lazy load-variables inside a module, using a custom __getattr__() function, as described in PEP 562.

If you access an attribute that isn’t in a module’s dictionary (eg Globals.this_attr_doesnt_exist), Python normally throws an AttributeError. But if the module has a __getattr__() function, Python calls that function instead, and uses its return value! This means we can run code at the moment someone accesses the dropdown_categories variable – and fetch its value just in time. Here’s how we might lazy load dropdown categories by taking advantage of this mechanism:

# Global Module

# A private variable to cache the value once we've fetched it
_dropdown_categories = None

def __getattr__(name):
    if name == 'dropdown_categories': 
        global _dropdown_categories
        # fetch the value if we haven't loaded it already:
        _dropdown_categories = _dropdown_categories or anvil.server.call('get_dropdown_categories')
        return _dropdown_categories
    raise AttributeError(name)
    # We must raise an AttributeError at the end of a custom __getattr__
# Inside Our Form

from .. import Global

class Form1(Form1Template):
    def __init__(self, **properties):
        self.init_components(**properties)
        self.category_dropdown.items = Global.dropdown_categories

Now loading the Global module is cheap. We only make the server call when we access Global.dropdown_categories. All subsequent times we use the cached value. What’s more, if we have several variables in our Global module, we can lazy load those too!

Discussion

Does the value ever change?

The above solution only works if you can cache the dropdown categories for the lifecycle of an app session. If the dropdown categories can change during an app session you will need to invalidate your cache, and that gets very sticky. As a wise man once said:

There are only two hard things in Computer Science: cache invalidation and naming things. Phil Carton

Does it scale?

What if you want to lazy load 20 possible variables? It would be annoyting to have to write 20 if statements!

A potential solution would be something like this, using the globals() dict:

# Global Module

_variable_1 = None
_variable_2 = None
...
_variable_20 = None

def __getattr__(name):
    global_dict = globals()
    private_name = '_' + name
    if private_name in global_dict:
        global_dict[private_name] = global_dict[private_name] or anvil.server.call('get_' + name)
        return global_dict[private_name]
    raise AttributeError(name)

I like this solution for its reusability - we simply add the private variable at the top of the module and then ensure we create a server function that matches our variable names. The problem is that globals() is not yet fully supported in Skulpt and so this code is doomed from the outset 😞 .

An easy work around would be to replace globals() with an actual dict defined within the module.

# Global Module

global_dict = {
    'variable_1' : None
    'variable_2' : None
    ...
    'variable_20' : None
}


def __getattr__(name):
    if name in global_dict:
        global_dict[name] = global_dict[name] or anvil.server.call('get_' + name)
        return global_dict[name]
    raise AttributeError(name)

Truth and falsity

All our code so far has made use of the or operator, which relies on None being falsy and the return value of the server call being truthy. If you might return an empty list, or some other empty data structure, you might want to make your code more robust.

def __getattr__(name):
    if name in global_dict:
        if global_dict[name] is None:
            global_dict[name] = anvil.server.call('get_' + name)
        return global_dict[name]
    raise AttributeError(name)

Finally, another product of PEP 562 that’s worth mentioning here is that you can customise your module’s __dir__. This can be useful for a lazy loading module that wants to advertise its contents to the end user.

# Inside Global Module
_dropdown_categories = None

__dir__ = ['dropdown_categories']
# inside some other module or form
from .. import Global

print(dir(Global)) # ['dropdown_categories']