[Python-ideas] PEP 505: None-aware operators

Clément Pit-Claudel cpitclaudel at gmail.com
Sun Jul 22 15:58:34 EDT 2018


On 2018-07-22 08:10, Steven D'Aprano wrote:
> Indeed. And I think we ought to think carefully about the benefits and 
> costs of all of those variants separately.
> 
> To me, the ?? operator seems like a clear and obvious win. The other 
> variants are more complex and the benefit is not as obvious to me, so I 
> haven't decided where I stand on them.

I like the ?? operator too; I like the short circuiting behavior a lot, and the semantics are simple.

I guess I'd use the other operators fairly often, too, mostly in quick-n-dirty scripts.  The one place where I miss them is when browsing through dictionaries that I get by querying a remote server and deserializing the resulting JSON.  I foten have situation where the value I'm interested in is e.g. either in response[0]["addresses"]["workplace"]["email"], or in response["records"][0]["contactInfo"]["emails"][0], and any of these subrecords may be missing.

Rewriting these using the ?[…] and ?. operators, I guess I would write something like this:

tmp = response?.get("records")
try:
    tmp = tmp?[0]
except IndexError:
    tmp = None
tmp = tmp?.get("contactInfo")?.get("emails")
try:
    tmp = tmp?[0]
except IndexError:
    tmp = None

Is there a shorter way to write these with the "?[…]" and "?." operators?  I guess the difficulty is that I need to swallow index and key errors, not just the type errors that come from indexing into None.

For cases like the one above, I usually use something like nget(response, ["records"], [0], ["contactInfo"], ["emails"], [0]), where nget is defined as shown below (in this use case, the lack of short-circuiting isn't an issue):

def nget(obj, *fields, default=None):
    for field in fields:
        if obj is None:
            return default
        if isinstance(field, str):
            obj = getattr(obj, field, None)
        elif isinstance(field, list):
            try:
                obj = obj.__getitem__(field[0])
            except (TypeError, KeyError, IndexError):
                obj = None
    return obj

class Test():
    def __init__(self):
        self.x = [{"y": 42, "z": ["aBc", "def"]}, [1]]

a = Test()
print(nget(a, "x", [0], ["z"], [0], [1]))              # B
print(nget(a, "x", [0], ["y"]))                        # 42
print(nget(a, "z", [0], ["y"], default="not found"))   # not found
print(nget(a, "z", [57], ["y"], default="not found"))  # not found

It would probably not be hard to wrap this into a special object, to be able to write something like wrap(response)["records"][0]["contactInfo"]["emails"][0].unwrap(). "wrap" would change its argument into a proxy returning a special indexable variant of None on key errors, and that dictionary would also call "wrap" on the results of __getitem__.  Something like this:

class wrap():
    SENTINEL = object()

    def __init__(self, obj):
        self.obj = obj

    def unwrap(self):
        return self.obj

    def __getitem__(self, key):
        try:
            return wrap(self.obj.__getitem__(key))
        except (TypeError, AttributeError, KeyError):
            return wrap(None)

a = [{"y": 42, "z": ["aBc", "def"]}, [1]]
print(wrap(a)[0]["z"][0][1].unwrap())

I think that's more or less what pymaybe does, in fact.

Cheers,
Clément.


More information about the Python-ideas mailing list