Как связать поиск атрибутов, который может вернуть 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()