2013-03-07 9 views
10

Il mio problema è generale, come concatenare una serie di ricerche di attributi quando uno di quelli intermedi potrebbero restituire None, ma da quando mi sono imbattuto in questo problema cercando di utilizzare Beautiful Soup, ho intenzione di chiedere che in quel contesto .Come concatenare le ricerche di attributi che potrebbero restituire None in Python?

Beautiful Soup analizza un documento HTML e restituisce un oggetto che può essere utilizzato per accedere al contenuto di tale documento strutturato. Ad esempio, se il documento analizzato è nella variabile soup, posso ottenere il titolo con:

title = soup.head.title.string 

mio problema è che se il documento non ha un titolo, quindi soup.head.title rendimenti None e la successiva ricerca string lancia un'eccezione Potrei rompere la catena come:

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

ma questo, ai miei occhi, è prolisso e difficile da leggere.

Potrei scrivere:

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

ma che è prolisso e inefficiente.

Una soluzione se pensata, che penso sia possibile, sarebbe quella di creare un oggetto (chiamarlo nil) che restituirebbe None per qualsiasi ricerca di attributi. Questo mi permetterebbe di scrivere:

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

ma questo è piuttosto brutto. C'è un modo migliore?

+2

Forse mantenere il vostro codice e pescato + gestire il 'AttributeError' eccezione nei casi in cui viene restituito' none'. – crayzeewulf

+0

Cosa vuoi che ritorni? – mgilson

+1

[ 'Maybe'monad in python] (http://stackoverflow.com/questions/8507200/maybe-kind-of-monad-in-python). Vedere anche [Monadi in Python (con la sintassi bello!)] (Http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html) – jfs

risposta

4

Potreste essere in grado di utilizzare reduce per questo:

>>> 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, penso che reduce viene spostato functools però :(


Suppongo che si potrebbe anche fare questo con una funzione più semplice:

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

Infine, suppongo che la più bella modo per fare questo è qualcosa di simile:

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

'reduce' è disponibile come' functools.reduce' da 2,6 in poi - in modo da un import probabilmente non sarebbe male tanto comunque ... –

+0

@JonClements -- Buono a sapersi. Grazie per il testa a testa. – mgilson

+1

Trovo questa soluzione molto più brutta delle soluzioni "verbose" proposte nella domanda. –

8

Il modo più semplice è quello di avvolgere in un ... except blocco try.

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

Non c'è davvero alcun motivo per provare ad ogni livello durante la rimozione di ogni test avrebbe sollevato la stessa eccezione in caso fallimento. Considero questo idiomatico in Python.

1

Una soluzione sarebbe quella di avvolgere l'oggetto esterno all'interno di un Proxy che gestisce i valori Nessuno per te. Vedi sotto per un'implementazione iniziale.

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

NOTA: io non sono personalmente sicuro wether vorrei utilizzare questo in un progetto vero e proprio, ma rende un esperimento interessante e ho messo qui per convincere la gente pensiero su questo.

+0

Questa è una soluzione intelligente, e probabilmente quello che avevo in mente quando ho posto la domanda. Questa soluzione finisce per essere piuttosto pesante, e ha anche lo svantaggio che tutti gli accessi agli attributi vengono eseguiti anche quando uno di quelli intermedi restituisce Nessuno e potrebbe potenzialmente cortocircuitare la valutazione dell'espressione. –

0

Qui è un'altra tecnica di potenziale, che nasconde l'assegnazione del valore intermedio in una chiamata di metodo. In primo luogo si definisce una classe per contenere il valore intermedio:

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) 

allora otteniamo usarla per memorizzare il risultato di ogni anello della catena di chiamate:

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 

non ritengo questo un buona soluzione, ma la sto includendo qui per completezza.

0

Ecco come ho gestito con ispirazione da @TAS e Is there a Python library (or pattern) like Ruby's andand?

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() 
Problemi correlati