Blog

Código Buggy Python: Los 10 Errores más Comunes que Cometen los Desarrolladores Python

Acerca de Python

Python es un lenguaje de programación interpretado y orientado a objetos de alto nivel con semántica dinámica. Su alto nivel integrado en las estructuras de datos, combinado con escritura y binding dinámicos lo hacen muy atractivo para el desarrollo rápido de aplicaciones, así como para su uso como lenguaje de script o glue para conectar componentes o servicios existentes. Python trabaja con módulos y paquetes, fomentando así la modularidad del programa y la reutilización de código.

Sobre este Artículo

La sintaxis simple y fácil de aprender de Python, puede enviar a los desarrolladores de Python en la dirección incorrecta - especialmente aquellos que están conociendo la lengua – perdiendo en el camino algunas de sus sutilezas y subestimando el poder del lenguaje diverso de Python.

Con esto en mente, este artículo presenta una lista “top 10” de errores sutiles, y difíciles de ver, que pueden tomar desprevenidos incluso a algunos de los desarrolladores de Python más avanzados.

python

(Nota: Este artículo está dirigido a un público más avanzado que el de Errores Comunes de Programadores Python, que se orienta más hacia aquellos que son nuevos en la lengua.)

Error común # 1: Un Mal Uso de Expresiones como Valores Predeterminados para los Argumentos de la Función

Python permite especificar que un argumento de la función es opcional, proporcionando un valor por defecto para ello. Si bien ésta es una gran característica de la lengua, puede dar lugar a cierta confusión cuando el valor por defecto es mutable. Por ejemplo, considera ésta definición de la función de Python:

>>> def foo(bar=[]):        # bar is optional and defaults to [] if not specified
...    bar.append("baz")    # but this line could be problematic, as we'll see...
...    return bar

Un error común es pensar que el argumento opcional se establecerá en la expresión por defecto específica, cada vez que se llama la función sin necesidad de suministrar un valor para el argumento opcional. En el código anterior, por ejemplo, se podría esperar que llamar a foo() varias veces (es decir, sin especificar un argumento bar) siempre daría de regreso baz, ya que la hipótesis sería que cada vez que foo() se llama (sin un argumento bar especificado) bar está ajustado a [] (es decir, una nueva lista vacía).

Pero vamos a ver lo que realmente sucede cuando se hace esto:

>>> foo()
["baz"]
>>> foo()
["baz", "baz"]
>>> foo()
["baz", "baz", "baz"]

¿Eh? ¿Por qué se siguió añadiendo el valor predeterminado de baz a una lista existente cada vez que foo()era llamado, en lugar de crear una nueva lista en cada oportunidad? La respuesta más avanzada de programación Python es que, el valor por defecto para un argumento de función se evalúa sólo una vez, en el momento en que se define la función. Por lo tanto, el argumento bar se inicializa con su valor por defecto (es decir, una lista vacía) sólo cuando foo() se ha definido por primera vez, pero luego los llamados a foo() (es decir, sin un argumento bar especificado) seguirán utilizando la misma lista a la que bar fue inicializado originalmente.

Por cierto, una solución común para esto es la siguiente:

>>> def foo(bar=None):
...    if bar is None:		# or if not bar:
...        bar = []
...    bar.append("baz")
...    return bar
...
>>> foo()
["baz"]
>>> foo()
["baz"]
>>> foo()
["baz"]

Error común # 2: Uso Incorrecto de las Variables de Clase

Consideremos el siguiente ejemplo:

>>> class A(object):
...     x = 1
...
>>> class B(A):
...     pass
...
>>> class C(A):
...     pass
...
>>> print A.x, B.x, C.x
1 1 1

Tiene sentido.

>>> B.x = 2
>>> print A.x, B.x, C.x
1 2 1

Sí, de nuevo como se esperaba.

>>> A.x = 3
>>> print A.x, B.x, C.x
3 2 3

¿Qué es esto? Sólo cambiamos A.x ¿Por qué C.x cambió también?

En Python, las variables de clase se manejan internamente como diccionarios y siguen lo que se refiere a menudo como Method Resolution Order (MRO). Así que en el código anterior, ya que el atributo x no se encuentra en la clase C, se buscará en sus clases base (únicamente A en el ejemplo anterior, aunque Python apoya herencia múltiple). En otras palabras, C no tiene su propia propiedad x, independiente de A. Por lo tanto, las referencias a C.x son de hecho referencias a A.x. Esto causa un problema Python, a menos que se maneje adecuadamente. Más información sobre atributos de clase en Python.

Error común # 3: Especificación de Parámetros de Forma Incorrecta para un Bloque de Excepción

Supongamos que tienes el siguiente código:

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except ValueError, IndexError:  # To catch both exceptions, right?
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
IndexError: list index out of range

El problema aquí es que el informe except no toma una lista de excepciones que se especifiquen de esta forma. Por el contrario, Python 2.x la sintaxis except Exception, e, se utiliza para enlazar la excepción al segundo parámetro opcional especificado (en éste caso e), con el fin de que esté disponible para una inspección adicional. Como resultado, en el código anterior, la excepción IndexError no está siendo capturada por el informe except; más bien, la excepción termina siendo enlazada a un parámetro llamado IndexError.

La forma correcta de capturar varias excepciones en un informe except, es especificar el primer parámetro como una tupla que contiene todas las excepciones a ser capturadas. Además, para la máxima portabilidad, utiliza la palabra clave as, ya que la sintaxis es apoyada por Python 2 y Python 3:

>>> try:
...     l = ["a", "b"]
...     int(l[2])
... except (ValueError, IndexError) as e:  
...     pass
...
>>>

Error común # 4: No Entender las Reglas de Ámbito de Python

La resolución de ámbito de Python se basa en lo que se conoce como la regla LEGB, que es la abreviatura de Local, Enclosing, Global, Built-in. Parece bastante sencillo, ¿verdad? Bueno, en realidad, hay algunas sutilezas en la forma en que esto funciona en Python, lo que nos lleva al problema común, más avanzado, de programación Python, a continuación.

Considera lo siguiente:

>>> x = 10
>>> def foo():
...     x += 1
...     print x
...
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

¿Cuál es el problema?

El error anterior se debe a que, cuando se hace una asignación a una variable en un ámbito, esa variable es considerada, automáticamente por Python, como local en ese ámbito y sigue cualquier variable de nombre similar, en cualquier ámbito exterior.

Muchos de ellos son, por lo tanto, se sorprenden al conseguir un UnboundLocalError en el código de trabajo anterior, cuando éste se modifica al añadir una instrucción de informe, en alguna parte del cuerpo de una función. (Puedes leer más sobre esto aquí.)

Es particularmente común que esto confunda a los desarrolladores cuando usan listas. Considera el siguiente ejemplo:

>>> lst = [1, 2, 3]
>>> def foo1():
...     lst.append(5)   # This works ok...
...
>>> foo1()
>>> lst
[1, 2, 3, 5]

>>> lst = [1, 2, 3]
>>> def foo2():
...     lst += [5]      # ... but this bombs!
...
>>> foo2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment

¿Eh? ¿Por qué foo2 falló, mientras que foo1 funcionó muy bien?

La respuesta es la misma que en el problema del ejemplo anterior, pero es sin duda más sutil. foo1 No está haciendo una asignación a lst, mientras que foo2 sí lo está. Recordando que lst += [5] es en realidad la abreviatura de lst = lst + [5], vemos que estamos tratando de asignar un valor a lst (por lo tanto, Python presume que está en el ámbito local). Sin embargo, el valor que estamos tratando de asignar a lst se basa en el mismo lst (de nuevo, ahora presume estar en el ámbito local), que aún no ha sido definido. Boom.

Error común # 5: Modificar una Lista al Iterar Sobre Ella

El problema con el siguiente código debería ser bastante obvio:

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> for i in range(len(numbers)):
...     if odd(numbers[i]):
...         del numbers[i]  # BAD: Deleting item from a list while iterating over it
...
Traceback (most recent call last):
  	  File "<stdin>", line 2, in <module>
IndexError: list index out of range

La eliminación de un elemento de una lista o matriz, mientras que se da iteración sobre ella, es un problema de Python que es bien conocido por cualquier desarrollador de software con experiencia. Sin embargo, aunque el ejemplo anterior puede ser bastante obvio, incluso los desarrolladores avanzados pueden involuntariamente, ser tomados por sorpresa por éste código, el cual es mucho más complejo.

Afortunadamente, Python incorpora una serie de paradigmas de programación elegantes que cuando se utilizan correctamente, pueden resultar en un código significativamente simplificado y racionalizado. Un beneficio secundario de esto es que el código más simple es menos probable que sea sorprendido por el bug eliminación-accidental-de-un-item-de-lista-mientras-iterando-sobre-ella. Uno de estos paradigmas es el de [comprensiones de lista]((https://docs.python.org/2/tutorial/datastructures.html#tut-listcomps). Por otra parte, las comprensiones de lista son particularmente útiles para evitar este problema específico, como se muestra en ésta implementación alternativa del código mostrado arriba, que funciona perfectamente:

>>> odd = lambda x : bool(x % 2)
>>> numbers = [n for n in range(10)]
>>> numbers[:] = [n for n in numbers if not odd(n)]  # ahh, the beauty of it all
>>> numbers
[0, 2, 4, 6, 8]

Si te gusta lo que has leido hasta ahora, pues no te pierdes el final:
ingresa aqui: https://www.toptal.com/python/c%C3%B3digo-buggy-python-los-10-errores-m%C3%A1s-comunes-que-cometen-los-desarrolladores-python/es

Comentarios potenciados por CComment

  • 0Clientes Satisfechos
  • 0Proyectos Culminados
  • 0Tazas de Café
  • 0Visitas
LOGIN