2010-03-23 6 views
9

No estoy seguro de si me gusta la dinámica de Python. A menudo resulta en que me olvide de verificar un tipo, tratando de llamar a un atributo y obtener el NoneType (o cualquier otro) no tiene atributo x error. Muchos de ellos son bastante inofensivos, pero si no se manejan correctamente, pueden hacer que baje toda tu aplicación/proceso/etc.¿Cuál es su estrategia para evitar errores dinámicos de tipado en Python (NoneType no tiene ningún atributo x)?

Con el tiempo mejoré la predicción de dónde podrían aparecer y añadí la verificación de tipos explícita, pero como soy humano, extraño uno ocasionalmente y luego un usuario final lo encuentra.

Así que estoy interesado en su estrategia para evitar esto. ¿Usas decoradores de verificación de tipo? ¿Tal vez envoltorios de objetos especiales?

favor comparta ...

+0

Además, consulte http://stackoverflow.com/questions/2014105/null-pattern-in-python-underused – voyager

+2

"NoneType no tiene ningún atributo x" es como una NullPointerException en Java. No es un problema de tipeo dinámico vs. estático. – FogleBird

+0

@FogleBird: pero Python permite más soluciones * imaginativas *) – voyager

Respuesta

7

olvidar para comprobar un tipo

Esto no tiene mucho sentido. Rara vez necesita "verificar" un tipo. Simplemente ejecuta pruebas unitarias y si ha proporcionado el objeto de tipo incorrecto, las cosas fallan. Nunca necesitas "verificar" mucho, en mi experiencia.

tratando de llamar a un atributo y conseguir el NoneType (o cualquier otra) tiene ningún atributo x error.

Inesperado None es un error simple. 80% de las veces, omití el return. Las pruebas unitarias siempre revelan esto.

De los que permanecen, el 80% del tiempo, son viejos bugs debido a una "salida temprana" que devuelve None porque alguien escribió una declaración return incompleta. Estas estructuras if foo: return son fáciles de detectar con pruebas unitarias. En algunos casos, deberían haber sido if foo: return somethingMeaningful, y en otros casos, deberían haber sido if foo: raise Exception("Foo").

El resto son errores tontos que malinterpretan las API. Generalmente, las funciones del mutador no devuelven nada. A veces lo olvido. Las pruebas unitarias las encuentran rápidamente, ya que básicamente, nada funciona bien.

Eso cubre los casos "inesperados None" bastante sólidamente. Fácil de probar para la unidad. La mayoría de los errores implican pruebas bastante triviales para escribir sobre algunas especies bastante obvias de errores: el retorno equivocado; no hacer una excepción

Otros errores de "no tiene ningún atributo X" son realmente desagradables cuando se utilizó un tipo totalmente erróneo. Esas son declaraciones de asignación realmente incorrectas o llamadas a función (o método) realmente incorrectas. Siempre fallan elaboradamente durante las pruebas unitarias, lo que requiere muy poco esfuerzo para solucionarlo.

Muchos de ellos son bastante inofensivos, pero si no se manejan correctamente, pueden hacer caer toda su aplicación/proceso/etc.

Um ... ¿Inofensivo? Si es un error, rezo para que baje toda mi aplicación lo más rápido posible para poder encontrarla. Un error que no bloquea mi aplicación es la situación más horrible que se pueda imaginar. "Inofensivo" no es una palabra que utilizaría para un error que no bloquea mi aplicación.

+1

+1 por "Un error que no bloquea mi aplicación es la situación más horrible que se pueda imaginar". –

+0

+1 Perspicaz, seguramente quise decir inofensivo ya que "sería inofensivo si se tratara de ellos, lo que a menudo es realmente fácil". –

4

Si escribir pruebas unitarias buenas para todo el código, usted debe encontrar los errores muy rápidamente al probar el código.

+1

En mi experiencia, eso es cierto para la mayoría de ellos, pero no dice el último 1% porque (para nosotros) no tiene sentido (o incluso es posible) probar la unidad _cada_posible escenario. –

+1

Las pruebas se vuelven aún más importantes en lenguajes muy dinámicos, que tienen el potencial de errores que ni siquiera son posibles en más idiomas estáticos. –

+0

@Mike: por el lado positivo, la prueba de unidad de escritura en lenguajes dinámicos es más fácil que en lenguajes estáticos. – voyager

-1

que tienden a utilizar

if x is None: 
    raise ValueError('x cannot be None') 

Pero esto sólo funcionará con el valor real None.

Un enfoque más general es probar los atributos necesarios antes de intentar usarlos. Por ejemplo:

def write_data(f): 
    # Here we expect f is a file-like object. But what if it's not? 
    if not hasattr(f, 'write'): 
     raise ValueError('write_data requires a file-like object') 
    # Now we can do stuff with f that assumes it is a file-like object 

El punto de este código es que en lugar de recibir un mensaje de error como "NoneType no tiene atributo de escritura", que se obtiene "write_data requiere un objeto de fichero". El error real no está en write_data(), y realmente no es un problema con NoneType en absoluto. El error real está en el código que llama alwrite_data(). La clave es comunicar esa información lo más directamente posible.

+2

Su enfoque tiene poco sentido. No es mejor que plantees tu propio error que pedirle a Python que levante el que de todos modos sería. –

+0

Quizás no lo explique con suficiente claridad. Mi punto es exactamente eso: plantear el error que Python plantearía de todos modos, solo fallaría rápidamente. Siempre que siempre falles rápido, podrás encontrar errores (suponiendo que la cobertura de tu prueba sea razonable). Algo más es simplemente tratar de construir un sistema de tipo estático encima del dinámico de Python. –

+1

¿Qué pasa con 'assert x is not None'? –

0

No he hecho mucha programación de Python, pero no he hecho ninguna programación en idiomas con tipado estático, así que no tiendo a pensar en cosas en términos de tipos de variables. Eso podría explicar por qué no he encontrado este problema mucho. (Aunque la pequeña cantidad de programación de Python que he hecho también podría explicar eso).

Disfruto el manejo revisado de Python 3 de cadenas (es decir, todas las cadenas son unicode, todo lo demás es solo una secuencia de bytes), porque Python 2 es posible que no observe TypeError s hasta que se ocupe de valores de cadena inusuales del mundo real.

+1

Python 2 tiene dos tipos de cadena-'unicode' y 'str'; el primero es una representación abstracta de texto y el último es una secuencia de bytes. Python 3 cambia el nombre de 'unicode' a' str' y realiza algunos pequeños cambios y cambia el nombre de 'str' a' bytes' y realiza algunos cambios medianos. Si está utilizando 'unicode' en Python 2, debería funcionar casi exactamente como Python 3. –

-1

Algo que puedes usar para simplificar tu código es usar el Null Object Design Pattern (al que me presentaron en Python Cookbook).

A grandes rasgos, el objetivo con objetos nulos es proporcionar una 'inteligente' reemplazo para el uso frecuente tipo de datos primitivo Ninguno en Python o nulo (o punteros nulos) en otros idiomas. Estos se usan para muchos propósitos de , incluido el caso importante en el que un miembro de un grupo de elementos similares es especial por alguna razón. La mayoría de los a menudo esto da como resultado declaraciones condicionales para distinguir entre elementos ordinarios y el valor nulo primitivo.

Este objeto simplemente come la falta de error de atributo, y puede evitar comprobar su existencia.

No es nada más que

class Null(object): 

    def __init__(self, *args, **kwargs): 
     "Ignore parameters." 
     return None 

    def __call__(self, *args, **kwargs): 
     "Ignore method calls." 
     return self 

    def __getattr__(self, mname): 
     "Ignore attribute requests." 
     return self 

    def __setattr__(self, name, value): 
     "Ignore attribute setting." 
     return self 

    def __delattr__(self, name): 
     "Ignore deleting attributes." 
     return self 

    def __repr__(self): 
     "Return a string representation." 
     return "<Null>" 

    def __str__(self): 
     "Convert to a string and return it." 
     return "Null" 

Con esto, si lo hace Null("any", "params", "you", "want").attribute_that_doesnt_exists() no va a explotar, pero sólo en silencio convertido en el equivalente de pass.

Lo normal sería hacer algo como

if obj.attr: 
    obj.attr() 

Con esto, usted acaba de hacer:

obj.attr() 

y olvidarse de él. Tenga en cuenta que el uso extensivo del objeto Null puede ocultar errores en su código.

+3

Eso me parece bastante peligroso. Va en contra del Zen de Python: "Los errores nunca deberían pasar en silencio". –

+1

Pero, por lo demás, la idea general del patrón de diseño Objeto nulo es buena. Por ejemplo, no use 'None' para representar un iterable vacío, utilice un iterable vacío real como la lista vacía (o una clase personalizada con un efecto similar). –

+0

'None' no es un tipo de datos primitivo. No es un tipo de datos en absoluto. –

1

Una de las ventajas de TDD es que terminas escribiendo un código que es más fácil de escribir pruebas.

Escribir primero el código y luego las pruebas pueden dar como resultado un código que superficialmente funciona igual, pero es mucho más difícil escribir pruebas de cobertura del 100%.

Cada caso es probable que sea diferente

Podría tener sentido para tener un decorador para comprobar si un determinado parámetro es None (o algún otro valor inesperado) si lo usa en un montón de lugares.

Quizás sea apropiado usar el Null pattern; si el código está explotando porque está estableciendo el valor inicial en Ninguno, podría establecer el valor inicial en una versión nula del objeto.

Cada vez son más las envolturas pueden añadir hasta un impacto en el rendimiento, sin embargo, lo que siempre es mejor escribir código desde el principio que evita los casos de esquina

2

Una herramienta para tratar de ayudarlo a mantener sus piezas bien ajustadas es interfaces. zope.interface es el paquete más notable en el mundo de Python para el uso de interfaces. Consulte http://wiki.zope.org/zope3/WhatAreInterfaces y http://glyph.twistedmatrix.com/2009/02/explaining-why-interfaces-are-great.html para comenzar a hacerse una idea de cómo funcionan las interfaces y z.i en particular. Las interfaces pueden resultar muy útiles en una gran base de código de Python.

Las interfaces no pueden sustituir las pruebas. Las pruebas razonablemente completas son especialmente importantes en lenguajes altamente dinámicos como Python, donde existen tipos de errores que no podrían existir en un lenguaje de tipo estático. Las pruebas también lo ayudarán a detectar los tipos de errores que no son exclusivos de los lenguajes dinámicos. Afortunadamente, desarrollar en Python significa que las pruebas son fáciles (debido a la flexibilidad) y tienes mucho tiempo para escribirlas que guardaste porque estás usando Python.

+0

Probablemente la mejor respuesta para analizar los lenguajes dinámicamente frente a los tipados estáticamente y la importancia de las pruebas unitarias en lenguajes tipados dinámicamente. –

3

También puede usar decorators to enforce the type of attributes.

>>> @accepts(int, int, int) 
... @returns(float) 
... def average(x, y, z): 
...  return (x + y + z)/2 
... 
>>> average(5.5, 10, 15.0) 
TypeWarning: 'average' method accepts (int, int, int), but was given 
(float, int, float) 
15.25 
>>> average(5, 10, 15) 
TypeWarning: 'average' method returns (float), but result is (int) 
15 

No soy realmente un fan de ellos, pero puedo ver su utilidad.

+0

Esta no es una buena estrategia para hacer un código que no tenga las desventajas OP temores. –

1

olvidar para comprobar un tipo

Con la tipificación de pato, no debería ser necesario comprobar un tipo. Pero esa es la teoría, en realidad a menudo querrás validar los parámetros de entrada (por ejemplo, verificar un UUID con una expresión regular). Para ese propósito, me creé algunos decoradores útiles para el tipo y el retorno sencilla comprobación de tipos que se llaman así:

@decorators.params(0, int, 2, str) # first parameter must be integer/third a string 
@decorators.returnsOrNone(int, long) # must return an int/long value or None 
def doSomething(integerParam, noMatterWhatParam, stringParam): 
    ... 

para todo lo demás que todo utilizar afirmaciones. Por supuesto, uno a menudo se olvida de verificar un parámetro, por lo que es necesario realizar pruebas y pruebas con frecuencia.

tratando de llamar a un atributo

me sucede muy rara vez. En realidad, a menudo uso métodos en lugar de acceso directo a los atributos (a veces, el "buen" viejo enfoque getter/setter).

porque yo soy humano me olvido de uno de vez en cuando y algo más para el usuario final le resulta

"Software siempre se efectuará a los clientes." - Un antipatrón que debe resolver con pruebas unitarias que manejan todos los casos posibles en una función. Es más fácil decirlo que hacerlo, pero ayuda ...

En cuanto a otros errores comunes de Python (nombres mal escritos, importaciones incorrectas, ...), estoy usando Eclipse con PyDev para proyectos (no para pequeños scripts). PyDev te advierte sobre la mayoría de los tipos simples de errores.

+3

Comprobar que un UUID realmente es, o validar datos de otro modo, no está probando el tipo * per se *, está mirando directamente a lo que le importa. A menudo es algo bueno. La comprobación de tipos real para un tipo literal (como lo hacen los decoradores) es bastante diferente y hace que su código sea menos flexible sin nada que ganar la mayor parte del tiempo. –

+0

Usar métodos en lugar de acceso directo a otro atributo no ayuda en nada. Un método todavía * es * un atributo, y uno que podría escribir mal o usar incorrectamente. Sin embargo, el uso de getters y setters hace que tengas que escribir más repeticiones y saturar tu API para que no ganes nada. (En idiomas como C++, hay algo que ganar: tendrás que cambiar tu API si alguna vez necesitas cambiar de un miembro a un método. En Python, puedes usar propiedades y por lo tanto no tienes que cambiar tu API, entonces hay realmente no hay una razón obvia para usar getters y setters.) –

+0

@Mike Graham: Correcto. Es por eso que a menudo la tipificación de pato es suficiente, como para iteradores que (casi) nunca harías 'assert isinstance (param, tuple)'.Pero si define interfaces estrictas (como clases base abstractas), a veces es una buena idea hacer una verificación estricta de tipos. – AndiDog

0

Puede indicar su IDE mediante el documento de función, por ejemplo: http://www.pydev.org/manual_adv_type_hints.html, en JavaScript el jsDoc ayuda de forma similar.

Pero en algún momento se enfrentarán errores que un lenguaje escrito podría evitar inmediatamente sin pruebas unitarias (a través de la compilación IDE y los tipos/inferencia).

Por supuesto, esto no elimina el beneficio de las pruebas unitarias, el análisis estático y las aserciones. Para proyectos más grandes tiendo a usar lenguajes tipados estáticamente porque tienen un soporte IDE muy bueno (excelente autocompletado, refactorización pesada ...). Todavía puede usar secuencias de comandos o DSL para alguna parte secundaria del proyecto.

Cuestiones relacionadas