Monday 29 August 2022

Safe Dereferencing in Python

There's a beautiful feature that I first came across with in Groovy many years ago and that over time has made it into C# first and TypeScript/JavaScript recently. It goes under different names: Safe Navigation, Save Dereferencing, Optional Chaining. Well, I'm refering to the "?" operator when accessing an attribute, that will return null (undefined in JavaScript) if the attribute is missing, and will not continue with ensuing attribute look ups if we were in a chain. I mean:

country?.capital?.location?.xCoordinate;

Notice that optional chaining also works for method invokation, using in JavaScript this odd syntax: ob.method?.()

It's one of those few features that I miss in Python. There is proposal PEP 505 to add it to the language, but for the moment it's in deferred state. So, same as I did 10 years ago when JavaScript was missing the feature and I sort of implemented a workaround, I've written a simple python function to sort of simulate it. We pass the chain as a List of strings (representing the attributes) or tuples (representing parameters for a method invokation):

UPDATE: The below implementation is quite a crap, for a better one check this follow-up post.


def safe_access(ob, chain):
    """
    sort of "safe accessor" (javascript's x?.attr?.attr2...)
    this version includes method invocation, so it's quite different from my getattr_chain.py
    """
    cur_value = ob
    for item in chain:
        if isinstance(item, str): #accessing attribute
            cur_value = getattr(cur_value, item, None)
        else: #invoking
            cur_value = cur_value(*item)
        if cur_value is None:
            return None
    return cur_value


class Person:
    def __init__(self, name, age, profession):
        self.name = name
        self.age = age
        self.profession = profession

class Profession:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def format_profession(self, st1, st2):
        return f"{st1}-{self.name}-{st2}"

p1 = Person("Francois", 4, Profession("programmer", 50000))

print(safe_access(p1, ["name"]))
print(safe_access(p1, ["city"]))
print(safe_access(p1, ["city", "name"]))
print(safe_access(p1, ["profession", "salary"]))
print(safe_access(p1, ["profession"
    , "format_profession", ("¡¡", "!!")
    , "upper", ()
]))

In JavaScript, if we try to invoke a method, but it exists as a data attribute, not as a function, the optional chaining will throw an error: TypeError: xxx is not a function


> ob = {name: "aa"};
> ob.name?.();
Uncaught TypeError: ob.name is not a function

I'm also doing that in my python code (you'll get a TypeError, xxx is not callable). I think it could make sense to return None rather than throwing an exception, for that we would update the invokation part in the above function with a callable check, like this:


            # cur_value = cur_value(*item)

            # if not callable, return None also
            if callable(cur_value):
                cur_value = cur_value(*item)
            else:
                return None


Safe dereferencing/Optional Chaining is related to other 2 features, also present in C# and recently in JavaScript (when I wrote this post it had not made it into JavaScript yet), the null coalescing operator and the null coalescing assignment. I think the aforementioned deferred PEP should also address these 2 things. For the moment, depending on the data that you are expecting, you could check with (as we used to do in JavaScript until recently) or. I mean a = b or c (rather than an hypothetycal: a = b ?? c). You can take a look at this discussion to see the possible issues with this approach.

No comments:

Post a Comment