¿Cuándo y cómo debo usar las excepciones?

20

El ajuste

A menudo tengo problemas para determinar cuándo y cómo usar excepciones. Consideremos un ejemplo simple: supongamos que estoy raspando una página web, digamos " http://www.abevigoda.com/ ", para determinar si Abe Vigoda todavía está vivo. Para hacer esto, todo lo que tenemos que hacer es descargar la página y buscar los momentos en que aparece la frase "Abe Vigoda". Devolvemos la primera aparición, ya que eso incluye el estado de Abe. Conceptualmente, se verá así:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Donde parse_abe_status(s)toma una cadena de la forma "Abe Vigoda es algo " y devuelve la parte " algo ".

Antes de argumentar que hay formas mucho mejores y más robustas de raspar esta página para el estado de Abe, recuerde que este es solo un ejemplo simple y artificial utilizado para resaltar una situación común en la que estoy.

Ahora, ¿dónde puede encontrar este código problemas? Entre otros errores, algunos "esperados" son:

  • download_pagees posible que no pueda descargar la página y arroje un archivo IOError.
  • Es posible que la URL no apunte a la página correcta o que la página se descargue incorrectamente y, por lo tanto, no haya resultados. hitses la lista vacía, entonces.
  • La página web ha sido alterada, posiblemente haciendo que nuestras suposiciones sobre la página sean incorrectas. Tal vez esperamos 4 menciones de Abe Vigoda, pero ahora encontramos 5.
  • Por alguna razón, hits[0]puede no ser una cadena de la forma "Abe Vigoda es algo ", por lo que no se puede analizar correctamente.

El primer caso no es realmente un problema para mí: IOErrorse lanza un y puede ser manejado por la persona que llama de mi función. Así que consideremos los otros casos y cómo podría manejarlos. Pero primero, supongamos que implementamos parse_abe_statusde la manera más estúpida posible:

def parse_abe_status(s):
    return s[13:]

Es decir, no realiza ninguna comprobación de errores. Ahora, a las opciones:

Opción 1: regreso None

Puedo decirle a la persona que llama que algo salió mal al regresar None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Si la persona que llama recibe Nonede mi función, debe suponer que no hubo menciones de Abe Vigoda, por lo que algo salió mal. Pero esto es bastante vago, ¿verdad? Y no ayuda el caso donde hits[0]no es lo que pensamos que era.

Por otro lado, podemos poner algunas excepciones:

Opción 2: uso de excepciones

Si hitsestá vacío, se IndexErrorlanzará un cuando lo intentemos hits[0]. Pero no debería esperarse que la persona que llama maneje un IndexErrorlanzamiento por mi función, ya que no tiene idea de dónde IndexErrorvino; podría haber sido arrojado find_all_mentions, por lo que él sabe. Entonces crearemos una clase de excepción personalizada para manejar esto:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Ahora, ¿qué pasa si la página ha cambiado y hay un número inesperado de visitas? Esto no es catastrófico, ya que el código aún puede funcionar, pero una persona que llama puede ser más cuidadosa o puede que desee registrar una advertencia. Entonces lanzaré una advertencia:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Por último, podríamos encontrar que statusno está vivo ni muerto. Tal vez, por alguna extraña razón, hoy resultó ser comatose. Entonces no quiero volver False, ya que eso implica que Abe está muerto. ¿Qué debo hacer aquí? Lanza una excepción, probablemente. ¿Pero de qué tipo? ¿Debo crear una clase de excepción personalizada?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Opción 3: en algún punto intermedio

Creo que el segundo método, con excepciones, es preferible, pero no estoy seguro si estoy usando excepciones correctamente dentro de él. Tengo curiosidad por ver cómo los programadores más experimentados manejarían esto.

jme
fuente

Respuestas:

17

La recomendación en Python es usar excepciones para indicar fallas. Esto es cierto incluso si espera un fracaso de forma regular.

Míralo desde la perspectiva de la persona que llama tu código:

my_status = get_abe_status(my_url)

¿Qué pasa si devolvemos ninguno? Si la persona que llama no maneja específicamente el caso de que get_abe_status haya fallado, simplemente intentará continuar con my_stats siendo None. Eso puede producir un error difícil de diagnosticar más adelante. Incluso si marca Ninguno, este código no tiene idea de por qué falló get_abe_status ().

Pero, ¿y si planteamos una excepción? Si la persona que llama no maneja específicamente el caso, la excepción se propagará hacia arriba y finalmente alcanzará el controlador de excepción predeterminado. Puede que no sea lo que desea, pero es mejor que introducir un error sutil en otra parte del programa. Además, la excepción proporciona información sobre lo que salió mal y se perdió en la primera versión.

Desde la perspectiva de la persona que llama, es simplemente más conveniente obtener una excepción que un valor de retorno. Y ese es el estilo de Python, usar excepciones para indicar que las condiciones de falla no devuelven valores.

Algunos tomarán una perspectiva diferente y argumentarán que solo debe usar excepciones para casos que realmente nunca espera que sucedan. Argumentan que correr normalmente no debería generar ninguna excepción. Una razón que se da para esto es que las excepciones son extremadamente ineficientes, pero eso no es realmente cierto para Python.

Un par de puntos en su código:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Esa es una forma realmente confusa de buscar una lista vacía. No induzca una excepción solo para verificar algo. Use un if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

¿Te das cuenta de que la línea logger.warning nunca se ejecutará correctamente?

Winston Ewert
fuente
1
Gracias (tardíamente) por su respuesta. Esto, junto con mirar el código publicado, ha mejorado mi sensación sobre cuándo y cómo lanzar una excepción.
jme
4

La respuesta aceptada merece ser aceptada y responde la pregunta, escribo esto solo para proporcionar un poco de antecedentes adicionales.

Uno de los credos de Python es: es más fácil pedir perdón que permiso. Esto significa que normalmente solo haces cosas, y si esperas excepciones, las manejas. A diferencia de lo que hace antes, verifica para asegurarte de que no obtendrás una excepción.

Quiero proporcionar un ejemplo para mostrarle cuán dramática es la diferencia en la mentalidad de C ++ / Java. Un bucle for en C ++ generalmente se parece a:

for(int i = 0; i != myvector.size(); ++i) ...

Una forma de pensar en esto: acceder a myvector[k]donde k> = myvector.size () causará una excepción. Entonces, en principio, podría escribir esto (muy torpemente) como un try-catch.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

O algo similar. Ahora, considere lo que sucede en un bucle python for:

for i in range(1):
    ...

¿Cómo funciona esto? El ciclo for toma el resultado del rango (1) y lo llama iter (), agarrando un iterador.

b = range(1).__iter__()

Luego llama a continuación en cada iteración de bucle, hasta que ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

En otras palabras, un bucle for en python es en realidad un intento, excepto disfrazado.

En cuanto a la pregunta concreta, recuerde que las excepciones detienen la ejecución normal de la función y deben tratarse por separado. En Python, debe lanzarlos libremente siempre que no tenga sentido ejecutar el resto del código en su función, y / o ninguno de los retornos refleja correctamente lo que sucedió en la función. Tenga en cuenta que regresar temprano de una función es diferente: regresar temprano significa que ya descubrió la respuesta y no necesita el resto del código para encontrar la respuesta. Estoy diciendo que se deben lanzar excepciones cuando no se conoce la respuesta, y el resto del código para determinar la respuesta no se puede ejecutar razonablemente. Ahora, "reflejarse correctamente" en sí mismo, como las excepciones que elige lanzar, es todo una cuestión de documentación.

En el caso de su código particular, diría que cualquier situación que haga que los hits sean una lista vacía debería arrojarse. ¿Por qué? Bueno, la forma en que se configura su función, no hay forma de determinar la respuesta sin analizar los resultados. Entonces, si los hits no se pueden analizar, ya sea porque la URL es incorrecta o porque los hits están vacíos, entonces la función no puede responder la pregunta y, de hecho, ni siquiera puede intentarlo.

En este caso particular, argumentaría que incluso si logras analizar y no obtienes una respuesta razonable (viva o muerta), entonces aún debes lanzar. ¿Por qué? Porque, la función devuelve un valor booleano. Devolver ninguno es muy peligroso para su cliente. Si hacen un check if en None, no habrá falla, solo se tratará en silencio como False. Por lo tanto, su cliente básicamente siempre tendrá que hacer una comprobación de si es Ninguno de todos modos si no quiere fallas silenciosas ... por lo que probablemente solo deba lanzar.

Nir Friedman
fuente
2

Debe usar excepciones cuando ocurra algo excepcional . Es decir, algo que no debería ocurrir dado el uso adecuado de la aplicación. Si es permisible y se espera que el consumidor de su método busque algo que no se encontrará, entonces "no encontrado" no es un caso excepcional. En este caso, debe devolver nulo o "Ninguno" o {}, o algo que indique un conjunto de retorno vacío.

Si, por otro lado, realmente espera que los consumidores de su método siempre encuentren lo que se está buscando (a menos que se equivoquen de alguna manera), entonces no encontrarlo sería una excepción y debería continuar con eso.

La clave es que el manejo de excepciones puede ser costoso: se supone que las excepciones recopilan información sobre el estado de su aplicación cuando ocurren, como un seguimiento de la pila, para ayudar a las personas a descifrar por qué ocurrieron. No creo que sea lo que intentas hacer.

Matthew Flynn
fuente
1
Si decide que no está permitido encontrar un valor, tenga cuidado con lo que usa para indicar que fue lo que sucedió. Si se supone que su método devuelve Stringay elige "Ninguno" como su indicador, esto significa que debe tener cuidado de que "Ninguno" nunca sea un valor válido. También tenga en cuenta que hay una diferencia entre mirar los datos y no encontrar un valor y no poder recuperar los datos, por lo tanto, no podemos encontrar los datos. Tener el mismo resultado para estos dos casos significa que no tiene visibilidad una vez que no obtiene ningún valor cuando espera que haya uno.
unholysampler el
Los bloques de código en línea están marcados con backticks (`), ¿tal vez eso es lo que querías hacer con" Ninguno "?
Izkata
3
Me temo que esto es absolutamente falso en Python. Está aplicando el razonamiento de estilo C ++ / Java a otro idioma. Python usa excepciones para indicar el final de un ciclo for; Eso es bastante excepcional.
Nir Friedman
2

Si estaba escribiendo una función

 def abe_is_alive():

Lo escribiría en return Trueo Falseen los casos en que estoy absolutamente seguro de uno u otro, y raiseun error en cualquier otro caso (por ejemplo raise ValueError("Status neither 'dead' nor 'alive'")). Esto se debe a que la función que llama a la mía espera un valor booleano, y si no puedo proporcionarlo con certeza, el flujo regular del programa no debería continuar.

Algo parecido a su ejemplo de obtener un número diferente de "hits" de lo esperado, probablemente lo ignoraría; mientras uno de los éxitos aún coincida con mi patrón "Abe Vigoda está {muerto | vivo}", está bien. Esto permite reorganizar la página, pero aún obtiene la información adecuada.

Más bien que

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Comprobaría explícitamente:

if not hits:
    raise NotFoundError

ya que esto tiende a ser "más barato" que configurar try.

Estoy de acuerdo contigo IOError; Tampoco trataría de manejar por error la conexión al sitio web; si no podemos, por alguna razón, este no es el lugar apropiado para manejarlo (ya que no nos ayuda a responder nuestra pregunta) y debería pasar fuera a la función de llamada.

jonrsharpe
fuente