Как связать поиск атрибутов, который может вернуть None в Python?

Моя проблема является общей, как связать серию поиска атрибутов, когда один из промежуточных может вернуться None, но так как я столкнулся с этой проблемой, пытаясь использовать Beautiful Soup, я собираюсь спросить об этом в этом контексте.

Beautiful Soup анализирует HTML-документ и возвращает объект, который можно использовать для доступа к структурированному контенту этого документа. Например, если анализируемый документ находится в переменной soup, я могу получить его заголовок с помощью:

title = soup.head.title.string

Моя проблема в том, что если документ не имеет названия, то soup.head.title возвращает None, а последующий поиск string вызывает исключение. Я мог бы разбить цепочку как:

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

но это, на мой взгляд, многословно и трудно читается.

Я мог бы написать:

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

но это многословно и неэффективно.

Одним из решений, которое, по моему мнению, является возможным, было бы создание объекта (назовем его nil), который возвратит None для любого поиска атрибутов. Это позволило бы мне написать:

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

но это довольно уродливо. Есть ли лучший способ?

Ответ 1

Вы можете использовать reduce для этого:

>>> 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)
''

В python3.x я думаю, что reduce перемещается в functools, хотя: (


Я полагаю, вы могли бы также сделать это с помощью более простой функции:

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

Наконец, я полагаю, что самый лучший способ сделать это:

try:
   title = foo.bar.baz.qux
except AttributeError:
   title = None

Ответ 2

Самый простой способ - обернуть блок try... except.

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

На самом деле нет причин для тестирования на каждом уровне , когда удаление каждого теста приведет к тому же исключению в случае сбоя. Я бы рассмотрел эту идиоматику в Python.

Ответ 3

Одним из решений было бы обернуть внешний объект внутри прокси, который обрабатывает для вас значения None. Ниже приведена начальная реализация.

import unittest

class SafeProxy(object):

    def __init__(self, instance):
        self.__dict__["instance"] = instance

    def __eq__(self, other):
        return self.instance==other

    def __call__(self, *args, **kwargs):
        return self.instance(*args, **kwargs)

    # TODO: Implement other special members

    def __getattr__(self, name):
        if hasattr(self.__dict__["instance"], name):
            return SafeProxy(getattr(self.instance, name))

        if name=="val":
            return lambda: self.instance

        return SafeProxy(None)

    def __setattr__(self, name, value):
        setattr(self.instance, name, value)


# Simple stub for creating objects for testing
class Dynamic(object):
    def __init__(self, **kwargs):
        for name, value in kwargs.iteritems():
            self.__setattr__(name, value)

    def __setattr__(self, name, value):
        self.__dict__[name] = value


class Test(unittest.TestCase):

    def test_nestedObject(self):
        inner = Dynamic(value="value")
        middle = Dynamic(child=inner)
        outer = Dynamic(child=middle)
        wrapper = SafeProxy(outer)
        self.assertEqual("value", wrapper.child.child.value)
        self.assertEqual(None, wrapper.child.child.child.value)

    def test_NoneObject(self):
        self.assertEqual(None, SafeProxy(None))

    def test_stringOperations(self):
        s = SafeProxy("string")
        self.assertEqual("String", s.title())
        self.assertEqual(type(""), type(s.val()))
        self.assertEqual()

if __name__=="__main__":
    unittest.main()

ПРИМЕЧАНИЕ. Я лично не уверен, что я использовал бы это в реальном проекте, но это делает интересный эксперимент, и я помещаю его здесь, чтобы получить от людей мысли об этом.

Ответ 4

Вот еще один потенциальный метод, который скрывает назначение промежуточного значения в вызове метода. Сначала мы определяем класс для хранения промежуточного значения:

class DataHolder(object):
    def __init__(self, value = None):
            self.v = value

    def g(self):
            return self.v

    def s(self, value):
            self.v = value
            return value

x = DataHolder(None)

Затем мы используем его для хранения результата каждой ссылки в цепочке вызовов:

import bs4;

for html in ('<html><head></head><body></body></html>',
             '<html><head><title>Foo</title></head><body></body></html>'):
    soup = bs4.BeautifulSoup(html)
    print x.s(soup.head) and x.s(x.g().title) and x.s(x.g().string)
    # or
    print x.s(soup.head) and x.s(x.v.title) and x.v.string

Я не считаю это хорошим решением, но я включаю его здесь для полноты.

Ответ 5

Вот как я справился с этим с помощью @TAS и Есть ли библиотека (или шаблон) Python, например Ruby и?

class Andand(object):
    def __init__(self, item=None):
        self.item = item

    def __getattr__(self, name):
        try:
            item = getattr(self.item, name)
            return item if name is 'item' else Andand(item)
        except AttributeError:
            return Andand()     

    def __call__(self):
        return self.item


title = Andand(soup).head.title.string()