Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to chain attribute lookups that might return None in Python?

My problem is a general one, how to chain a series of attribute lookups when one of the intermediate ones might return None, but since I ran into this problem trying to use Beautiful Soup, I'm going to ask it in that context.

Beautiful Soup parses an HTML document and returns an object that can be used to access the structured content of that document. For example, if the parsed document is in the variable soup, I can get its title with:

title = soup.head.title.string

My problem is that if the document doesn't have a title, then soup.head.title returns None and the subsequent string lookup throws an exception. I could break up the chain as:

x = soup.head
x = x.title if x else None
title = x.string if x else None

but this, to my eye, is verbose and hard to read.

I could write:

title = soup.head and soup.head.title and soup.title.head.string

but that is verbose and inefficient.

One solution if thought of, which I think is possible, would be to create an object (call it nil) that would return None for any attribute lookup. This would allow me to write:

title = ((soup.head or nil).title or nil).string

but this is pretty ugly. Is there a better way?

like image 237
David Hull Avatar asked Mar 07 '13 19:03

David Hull


3 Answers

You might be able to use reduce for this:

>>> class Foo(object): pass
... 
>>> a = Foo()
>>> a.foo = Foo()
>>> a.foo.bar = Foo()
>>> a.foo.bar.baz = Foo()
>>> a.foo.bar.baz.qux = Foo()
>>> 
>>> reduce(lambda x,y:getattr(x,y,''),['foo','bar','baz','qux'],a)
<__main__.Foo object at 0xec2f0>
>>> reduce(lambda x,y:getattr(x,y,''),['foo','bar','baz','qux','quince'],a)
''

In python3.x, I think that reduce is moved to functools though :(


I suppose you could also do this with a simpler function:

def attr_getter(item,attributes)
    for a in attributes:
        try:
            item = getattr(item,a)
        except AttributeError:
            return None #or whatever on error
    return item

Finally, I suppose the nicest way to do this is something like:

try:
   title = foo.bar.baz.qux
except AttributeError:
   title = None
like image 28
mgilson Avatar answered Sep 21 '22 22:09

mgilson


The most straightforward way is to wrap in a try...except block.

try:
    title = soup.head.title.string
except AttributeError:
    print "Title doesn't exist!"

There's really no reason to test at each level when removing each test would raise the same exception in the failure case. I would consider this idiomatic in Python.

like image 84
jeffknupp Avatar answered Sep 20 '22 22:09

jeffknupp


I'm running Python 3.9

Python 3.9.2 (tags/v3.9.2:1a79785, Feb 19 2021, 13:44:55) [MSC v.1928 64 bit (AMD64)]

and the and key word solves my problem

memo[v] = short_combo and short_combo.copy()

From what I gather this is not pythonic and you should handle the exception.
However in my solution None ambiguity exists within the function, and in this scenario I would think it to be a poor practice to handle exceptions that occur ~50% of the time.
Where I outside of the function and calling it I would handle the exception.

like image 2
Bardia 'Luviz' Jedi Avatar answered Sep 22 '22 22:09

Bardia 'Luviz' Jedi