¿Cómo manejar la “escritura de pato” en Python?

Generalmente quiero mantener mi código tan genérico como sea posible. Actualmente estoy escribiendo una biblioteca simple y esta vez puedo usar diferentes tipos con mi biblioteca.

Una forma de proceder es obligar a las personas a subclasificar una clase de “interfaz”. Para mí, esto se parece más a Java que a Python y el uso de issubclass en cada método tampoco suena muy tentador.

Mi forma preferida es usar el objeto de buena fe, pero esto boostá algunas AttributeErrors . Podría envolver cada llamada peligrosa en un bloque try / except. Esto, también, parece un poco engorroso:

 def foo(obj): ... # it should be able to sleep try: obj.sleep() except AttributeError: # handle error ... # it should be able to wag it's tail try: obj.wag_tail() except AttributeError: # handle this error as well 

¿Debería simplemente omitir el manejo de errores y esperar que las personas solo usen objetos con los métodos requeridos? Si hago algo estúpido como [x**2 for x in 1234] , en realidad obtengo un TypeError y no un AttributeError (los ints no son iterables), por lo que debe haber algún tipo de verificación de tipo en algún lugar. ¿Y si quiero hacer el ¿mismo?

Esta pregunta será abierta, pero ¿cuál es la mejor manera de manejar el problema anterior de manera limpia? ¿Existen buenas prácticas establecidas? ¿Cómo se implementa la “comprobación de tipo” iterable anterior, por ejemplo,?

Editar

Mientras que AttributeError s está bien, los TypeErrors generados por las funciones nativas generalmente brindan más información sobre cómo resolver los errores. Toma esto por ejemplo:

 >>> ['a', 1].sort() Traceback (most recent call last): File "", line 1, in  TypeError: unorderable types: int() < str() 

Me gustaría que mi biblioteca fuera lo más útil posible.

Si su código requiere una interfaz particular, y el usuario pasa un objeto sin esa interfaz, nueve de cada diez veces, es inadecuado detectar la excepción. La mayoría de las veces, un AttributeError no solo es razonable sino que también se espera cuando se trata de desajustes de interfaz.

Ocasionalmente, puede ser apropiado capturar un AttributeError por una de dos razones. O bien desea que algún aspecto de la interfaz sea opcional , o desea lanzar una excepción más específica, tal vez una subclase de excepción específica del paquete. Ciertamente, nunca debe evitar que se lance una excepción si no ha manejado honestamente el error y cualquier consecuencia.

Por lo tanto, me parece que la respuesta a esta pregunta debe ser específica del problema y del dominio. Es fundamentalmente una cuestión de si el uso de un objeto Cow lugar de un objeto Duck debería funcionar. Si es así, y manejas cualquier manipulación de interfaz necesaria, entonces está bien. Por otro lado, no hay razón para verificar explícitamente si alguien le ha pasado un objeto Frog , a menos que cause un error desastroso (es decir, algo mucho peor que un rastro de stack).

Dicho esto, siempre es una buena idea documentar su interfaz, para eso están las cadenas de documentación (entre otras cosas). Cuando lo piensa, es mucho más eficiente lanzar un error general para la mayoría de los casos y decirle a los usuarios la forma correcta de hacerlo en la cadena de documentos, que intentar prever todos los posibles errores que un usuario pueda cometer y crear un mensaje de error personalizado.

Una advertencia final: es posible que estés pensando en UI aquí. Creo que esa es otra historia. Es bueno verificar la entrada que un usuario final le brinda para asegurarse de que no sea malintencionada o que tenga una forma horrible, y proporcionar comentarios útiles en lugar de un seguimiento de la stack. Pero para bibliotecas o cosas así, realmente tienes que confiar en el progtwigdor que usa tu código para usarlo de manera inteligente y respetuosa, y para comprender los errores que genera Python.

No soy un profesional de Python, pero creo que a menos que pueda probar una alternativa para cuando el parámetro no implemente un método determinado, no debe evitar que se generen excepciones. Deje que la persona que llama maneje estas excepciones. De esta manera, estarías ocultando problemas a los desarrolladores.

Como he leído en Clean Code , si desea buscar un elemento en una colección, no pruebe sus parámetros con ìssubclass (de una lista), sino que prefiera llamar a getattr(l, "__contains__") . Esto le dará a alguien que está usando su código la oportunidad de pasar un parámetro que no es una lista pero que tiene un método __contains__ definido y las cosas deberían funcionar igual de bien.

Por lo tanto, creo que deberías codificar de forma abstracta y genérica , imponiendo tan pocas restricciones como puedas. Para eso, tendrá que hacer el menor número posible de suposiciones. Sin embargo, cuando enfrente algo que no puede manejar, haga una excepción y deje que el progtwigdor sepa qué error cometió.

Si solo desea que los métodos no implementados no hagan nada, puede intentar algo como esto, en lugar de la construcción de varias líneas try/except :

 getattr(obj, "sleep", lambda: None)() 

Sin embargo, esto no es necesariamente obvio como una llamada a la función, así que quizás:

 hasattr(obj, "sleep") and obj.sleep() 

o si desea estar un poco más seguro antes de llamar a algo, puede llamarlo:

 hasattr(obj, "sleep") and callable(obj.sleep) and obj.sleep() 

Este patrón de “mirar antes de saltar” generalmente no es la forma preferida de hacerlo en Python, pero es perfectamente legible y cabe en una sola línea.

Otra opción, por supuesto, es abstraer el try/except en una función separada.

Buena pregunta, y bastante abierta. Creo que el estilo típico de Python no es para verificar, ya sea con instancias o capturando excepciones individuales. Cerainly, el uso de isinstance es un estilo bastante malo, ya que anula todo el punto de escritura de pato (aunque el uso de isinstance en primitivas puede ser correcto; asegúrese de verificar las entradas enteras int y long para las cadenas enteras, y la cadena de base) clase de str y unicode). Si lo marca, debería elevar un TypeError.

En general, no verificar es correcto, ya que normalmente genera un TypeError o AttributeError, que es lo que quieres. (Aunque puede retrasar esos errores, lo que dificulta la depuración del código del cliente).

La razón por la que ve TypeErrors es que el código primitivo lo eleva, efectivamente porque hace una instancia. El bucle for está codificado para generar un TypeError si algo no es iterable.

En primer lugar, el código en su pregunta no es ideal:

 try: obj.wag_tail() except AttributeError: ... 

No se sabe si AttributeError proviene de la búsqueda de wag_tail o si ocurrió dentro de la función. Lo que estás tratando de hacer es:

 try: f = getattr(obj, 'wag_tail') except AttributeError: ... finally: f() 

Edit: kindall señala acertadamente que si va a verificar esto, también debe verificar que f es reclamable.

En general, esto no es Pythonic. Simplemente llame y deje que la excepción se filtre hacia abajo, y el seguimiento de la stack es lo suficientemente informativo como para solucionar el problema. Creo que debería preguntarse si sus excepciones de devolución son lo suficientemente útiles para justificar todo este código de comprobación de errores.

El caso de ordenar una lista es un gran ejemplo.

  • La clasificación de listas es muy común,
  • pasar tipos no ordenados ocurre para una proporción significativa de ellos, y
  • lanzar AttributeError en ese caso es muy confuso.

Si esos tres criterios se aplican a su problema (especialmente el tercero), estoy de acuerdo con la creación de un emisor de excepciones bastante excepcional.

Tienes que mantener el equilibrio con el hecho de que lanzar estos bonitos errores hará que tu código sea más difícil de leer, lo que estadísticamente significa más errores en tu código. Es una cuestión de equilibrar los pros y los contras.

Si alguna vez necesita verificar comportamientos (como __real__ y __contains__ ), no olvide usar las clases base abstractas de Python que se encuentran en las collections , io y numbers .