Python "null coalescing" / cascading attribute access

Question:

Disclaimer: I’m relatively new to Python and Django.

While my problem is not specific to Django, I often encountered it in that context. Occasionnaly I add new features to my site that assume an authenticated user and access properties that only authenticated users have. Then when I visit the page in another browser where I’m not logged in I get an “AttributeNotFound” error, because the user is actually an AnonymousUser.

So what, no big deal. Just check if the is is authenticated:

def my_view(request):
    if request.user.is_authenticated():
        posts = user.post_set.filter(flag=True).all()
    else:
        posts = None

    return render(request, 'template.html', {posts: posts})

And then in the template:

<h1>Your Posts</h1>
<ul>
{% for post in posts %}
    <li>{{ post.name }}</li>
{%else%}
    <p>No posts found.</p>
{%endfor%}
</ul>

But I noticed this pattern a lot in my code where I want to do something iff a certain condition in a call chain is met (i.e. the attribute is not None) or just return None, an empty set or something similar otherwise.

So I immediately though of options (as found in Scala), but due to the cumbersome syntax of lambda expressions in python I didn’t like the result very much:

# create a user option somewhere in a RequestContext
request.user_option = Some(user) if user.is_authenticated() else Empty()

# access it like this in view
posts = request.user_option.map(lambda u: u.post_set.filter(flag=True).all()).get_or_else(None)

The code is obscured by the option syntax and almost hides the actual intent. Also we have to know that a User has to be authenticated to have the post_set property.

Instead what I wanted is a “Null coalescing”-like operator that allows me to write the code like this instead:

def my_view(request):
    user_ = Coalesce(user)
    posts = user_.post_set.filter(flag=True).all() # -> QuerySet | None-like
    return render(request, 'template.html', {posts: posts})

Then I proceeded to write a wrapper class that actually allows me to do this:

class Coalesce:
    def __init__(self, inst):
        self.inst = inst

    def __call__(self, *args, **kwargs):
        return Coalesce(None)

    def __iter__(self):
        while False:
            yield None

    def __str__(self):
        return ''

    def __getattr__(self, name):
        if hasattr(self.inst, name):
            return getattr(self.inst, name)
        else:
            return Coalesce(None)

    def __len__(self):
        return 0

Wrapping the user object with Coalesce allows me to write the code as I wanted to. If user does not have the attribute post_set defined it will behave like an empty set / list (with regard to iterables) and evaluates to False. So it works within Django templates.

So assuming that coalesce’d objects are marked via naming convention or explicit conversion, is this OK to do or would you say it should be avoided? If avoided, what is the best way to handle optional attributes without copious amounts of if-else-blocks?

Asked By: Bunkerbewohner

||

Answers:

Your solution is a variation on the Null object pattern. As long as it’s explicitly used, I would think it’s perfectly fine to use it. Especially when it improves your code’s readability.

You could also extend your Coalesce class with the following method to allow indexing access like this first_post = user_.post_set.filter(flag=True)[0]

def __getitem__(self, i):
    try:
        return self.inst[i]
    except (TypeError, IndexError, KeyError):
        return Coalesce(None)

And for the membership tests in and not in:

def __contains__(self, i):
    try:
        return i in self.inst
    except (TypeError, IndexError, KeyError):
        return False
Answered By: Steef
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.