Buggy Python Code: los 10 errores más comunes que cometen los desarrolladores de Python
Publicado: 2022-03-11Acerca de Python
Python es un lenguaje de programación de alto nivel interpretado, orientado a objetos y con semántica dinámica. Sus estructuras de datos integradas de alto nivel, combinadas con la escritura dinámica y el enlace dinámico, lo hacen muy atractivo para el desarrollo rápido de aplicaciones, así como para su uso como lenguaje de secuencias de comandos o pegamento para conectar componentes o servicios existentes. Python admite módulos y paquetes, lo que fomenta la modularidad del programa y la reutilización del código.
Acerca de este artículo
La sintaxis simple y fácil de aprender de Python puede inducir a error a los desarrolladores de Python, especialmente a aquellos que son nuevos en el lenguaje, para que pasen por alto algunas de sus sutilezas y subestimen el poder del diverso lenguaje Python.
Con eso en mente, este artículo presenta una lista de los "10 principales" de errores un tanto sutiles y más difíciles de detectar que pueden afectar incluso a algunos desarrolladores de Python más avanzados en la retaguardia.
(Nota: este artículo está destinado a una audiencia más avanzada que Errores comunes de los programadores de Python, que está más orientado a aquellos que son nuevos en el lenguaje).
Error común n.º 1: uso indebido de expresiones como valores predeterminados para argumentos de función
Python le permite especificar que el argumento de una función es opcional al proporcionarle un valor predeterminado . Si bien esta es una excelente característica del lenguaje, puede generar cierta confusión cuando el valor predeterminado es mutable . Por ejemplo, considere esta definición de 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 predeterminada especificada cada vez que se llame a la función sin proporcionar un valor para el argumento opcional. En el código anterior, por ejemplo, uno podría esperar que llamar a foo() repetidamente (es decir, sin especificar un argumento de bar ) siempre devolvería 'baz' , ya que se supondría que cada vez que se llama a foo() (sin una bar argumento especificado) la bar se establece en [] (es decir, una nueva lista vacía).
Pero veamos lo que realmente sucede cuando haces esto:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"] ¿Eh? ¿Por qué seguía agregando el valor predeterminado de "baz" a una lista existente cada vez que se llamaba a foo() , en lugar de crear una nueva lista cada vez?
La respuesta de programación de Python más avanzada es que el valor predeterminado para un argumento de función solo se evalúa una vez, en el momento en que se define la función. Por lo tanto, el argumento de bar se inicializa a su valor predeterminado (es decir, una lista vacía) solo cuando foo() se define por primera vez, pero luego las llamadas a foo() (es decir, sin un argumento de bar especificado) continuarán usando la misma lista para qué bar se inicializó originalmente.
FYI, 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 n.º 2: usar variables de clase de forma incorrecta
Considere el siguiente ejemplo:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1Tiene sentido.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1Sí, de nuevo como se esperaba.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3 ¿Qué diablos $%#!& ?? Solo cambiamos Ax . ¿Por qué Cx también cambió?
En Python, las variables de clase se manejan internamente como diccionarios y siguen lo que a menudo se denomina Orden de resolución de métodos (MRO). Entonces, en el código anterior, dado que el atributo x no se encuentra en la clase C , se buscará en sus clases base (solo A en el ejemplo anterior, aunque Python admite herencias múltiples). En otras palabras, C no tiene su propia propiedad x , independiente de A Por lo tanto, las referencias a Cx son de hecho referencias a Ax . Esto causa un problema de Python a menos que se maneje correctamente. Obtenga más información sobre los atributos de clase en Python.
Error común n.º 3: especificar parámetros incorrectamente para un bloque de excepción
Supongamos que tiene 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 la declaración de except no toma una lista de excepciones especificadas de esta manera. Más bien, en Python 2.x, la sintaxis except Exception, e se usa para vincular la excepción al segundo parámetro opcional especificado (en este caso e ), a fin de que esté disponible para una inspección adicional. Como resultado, en el código anterior, la declaración de excepción no detecta la except IndexError ; más bien, la excepción termina siendo vinculada a un parámetro llamado IndexError .
La forma adecuada de capturar múltiples excepciones en una declaración de except es especificar el primer parámetro como una tupla que contiene todas las excepciones que se van a capturar. Además, para una máxima portabilidad, use la palabra clave as , ya que esa sintaxis es compatible tanto con Python 2 como con Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>Error común n.º 4: entender mal las reglas de alcance de Python
La resolución de alcance de Python se basa en lo que se conoce como la regla LEGB, que es la abreviatura de L ocal, Encerrando , G lobal, B uilt-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 de programación de Python más avanzado 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 ocurre porque, cuando realiza una asignación a una variable en un ámbito, Python considera automáticamente que esa variable es local para ese ámbito y sombrea cualquier variable con un nombre similar en cualquier ámbito externo.
Por lo tanto, muchos se sorprenden al obtener un UnboundLocalError en un código que funcionaba anteriormente cuando se modifica agregando una declaración de asignación en algún lugar del cuerpo de una función. (Puedes leer más sobre esto aquí.)
Es particularmente común que esto haga tropezar a los desarrolladores cuando usan listas. Considere 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 bombardeó mientras foo1 funcionaba bien?
La respuesta es la misma que en el problema del ejemplo anterior, pero se admite que es más sutil. foo1 no está haciendo una asignación a lst , mientras que foo2 . Recordando que lst += [5] es realmente solo una abreviatura de lst = lst + [5] , vemos que estamos intentando asignar un valor a lst (por lo tanto, Python supone que está en el ámbito local). Sin embargo, el valor que buscamos asignar a lst se basa en el propio lst (nuevamente, ahora se supone que está en el ámbito local), que aún no se ha definido. Auge.
Error común #5: Modificar una lista mientras se itera 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 rangeEliminar un elemento de una lista o matriz mientras se itera sobre él es un problema de Python que es bien conocido por cualquier desarrollador de software experimentado. Pero mientras que el ejemplo anterior puede ser bastante obvio, incluso los desarrolladores avanzados pueden ser mordidos involuntariamente por este código que es mucho más complejo.
Afortunadamente, Python incorpora una serie de paradigmas de programación elegantes que, cuando se usan correctamente, pueden resultar en un código significativamente simplificado y optimizado. Un beneficio adicional de esto es que es menos probable que el código más simple sea mordido por el error de eliminación accidental de un elemento de lista mientras se itera sobre él. Uno de esos paradigmas es el de las listas de comprensión. Además, las listas por comprensión son particularmente útiles para evitar este problema específico, como se muestra en esta implementación alternativa del código anterior 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]Error común #6: Confundir cómo Python vincula variables en los cierres
Considerando el siguiente ejemplo:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...Es posible que espere el siguiente resultado:
0 2 4 6 8Pero en realidad obtienes:
8 8 8 8 8¡Sorpresa!
Esto sucede debido al comportamiento de enlace tardío de Python que dice que los valores de las variables utilizadas en los cierres se buscan en el momento en que se llama a la función interna. Entonces, en el código anterior, cada vez que se llama a cualquiera de las funciones devueltas, el valor de i se busca en el ámbito circundante en el momento en que se llama (y para entonces, el bucle se ha completado, por lo que ya se i ha asignado su final). valor de 4).
La solución a este problema común de Python es un truco:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8¡Voila! Estamos aprovechando los argumentos predeterminados aquí para generar funciones anónimas a fin de lograr el comportamiento deseado. Algunos llamarían a esto elegante. Algunos lo llamarían sutil. Algunos lo odian. Pero si eres un desarrollador de Python, es importante entenderlo en cualquier caso.
Error común #7: crear dependencias de módulos circulares
Supongamos que tiene dos archivos, a.py y b.py , cada uno de los cuales importa al otro, de la siguiente manera:
En a.py :
import b def f(): return bx print f() Y en b.py :
import a x = 1 def g(): print af() Primero, intentemos importar a.py :
>>> import a 1Funcionó muy bien. Quizás eso te sorprenda. Después de todo, aquí tenemos una importación circular que presumiblemente debería ser un problema, ¿no?
La respuesta es que la mera presencia de una importación circular no es en sí misma un problema en Python. Si ya se ha importado un módulo, Python es lo suficientemente inteligente como para no intentar volver a importarlo. Sin embargo, dependiendo del punto en el que cada módulo intente acceder a funciones o variables definidas en el otro, es posible que tenga problemas.
Entonces, volviendo a nuestro ejemplo, cuando a.py , no hubo problemas para importar b.py , ya que b.py no requiere que se defina nada de a.py en el momento en que se importa . La única referencia en b.py a a es la llamada a af() . Pero esa llamada está en g() y nada en a.py o b.py invoca g() . Así que la vida es buena.
Pero qué sucede si intentamos importar b.py (sin haber importado previamente a.py , es decir):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x' UH oh. ¡Eso no es bueno! El problema aquí es que, en el proceso de importar b.py , intenta importar a.py , que a su vez llama a f() , que intenta acceder a bx . Pero bx aún no se ha definido. De ahí la excepción AttributeError .
Al menos una solución a esto es bastante trivial. Simplemente modifique b.py para importar a.py dentro de g() :
x = 1 def g(): import a # This will be evaluated only when g() is called print af()No, cuando lo importamos, todo está bien:
>>> import b >>> bg() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g'Error común n.º 8: el nombre choca con los módulos de la biblioteca estándar de Python
Una de las bellezas de Python es la gran cantidad de módulos de biblioteca que viene con "listo para usar". Pero como resultado, si no lo está evitando conscientemente, no es tan difícil encontrarse con un conflicto de nombres entre el nombre de uno de sus módulos y un módulo con el mismo nombre en la biblioteca estándar que se envía con Python (por ejemplo , es posible que tenga un módulo llamado email.py en su código, que estaría en conflicto con el módulo de biblioteca estándar del mismo nombre).
Esto puede generar problemas complicados, como importar otra biblioteca que a su vez intente importar la versión de la biblioteca estándar de Python de un módulo pero, como tiene un módulo con el mismo nombre, el otro paquete importa por error su versión en lugar de la que está dentro. la biblioteca estándar de Python. Aquí es donde ocurren los errores de Python.
Por lo tanto, se debe tener cuidado para evitar el uso de los mismos nombres que los de los módulos de la biblioteca estándar de Python. Es mucho más fácil para usted cambiar el nombre de un módulo dentro de su paquete que presentar una Propuesta de mejora de Python (PEP) para solicitar un cambio de nombre en sentido ascendente e intentar que se apruebe.
Error común n.º 9: no abordar las diferencias entre Python 2 y Python 3
Considere el siguiente archivo foo.py :
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()En Python 2, esto funciona bien:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2Pero ahora vamos a darle un giro en Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment ¿Qué acaba de pasar aquí? El "problema" es que, en Python 3, el objeto de excepción no es accesible más allá del alcance del bloque de except . (La razón de esto es que, de lo contrario, mantendría un ciclo de referencia con el marco de pila en la memoria hasta que el recolector de basura se ejecute y elimine las referencias de la memoria. Más detalles técnicos sobre esto están disponibles aquí).
Una forma de evitar este problema es mantener una referencia al objeto de excepción fuera del alcance del bloque de except para que permanezca accesible. Aquí hay una versión del ejemplo anterior que usa esta técnica, lo que genera un código compatible con Python 2 y Python 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()Ejecutando esto en Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2¡Yippe!
(Por cierto, nuestra Guía de contratación de Python analiza otras diferencias importantes que se deben tener en cuenta al migrar código de Python 2 a Python 3).
Error común #10: Mal uso del método __del__
Digamos que tienes esto en un archivo llamado mod.py :
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle) Y luego intentaste hacer esto desde another_mod.py :
import mod mybar = mod.Bar() Obtendría una fea excepción de AttributeError .
¿Por qué? Porque, como se informó aquí, cuando el intérprete se apaga, todas las variables globales del módulo están configuradas en None . Como resultado, en el ejemplo anterior, en el momento en que se invoca __del__ , el nombre foo ya se ha establecido en None .
Una solución a este problema de programación de Python algo más avanzado sería usar atexit.register() en su lugar. De esa manera, cuando su programa termine de ejecutarse (es decir, cuando salga normalmente), sus controladores registrados se iniciarán antes de que se apague el intérprete.
Con ese entendimiento, una solución para el código mod.py anterior podría verse así:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle) Esta implementación proporciona una forma limpia y confiable de llamar a cualquier función de limpieza necesaria al finalizar el programa normalmente. Obviamente, depende de foo.cleanup decidir qué hacer con el objeto vinculado al nombre self.myhandle , pero entiendes la idea.
Envolver
Python es un lenguaje poderoso y flexible con muchos mecanismos y paradigmas que pueden mejorar enormemente la productividad. Sin embargo, al igual que con cualquier herramienta de software o lenguaje, tener una comprensión o apreciación limitada de sus capacidades a veces puede ser más un impedimento que un beneficio, lo que lo deja a uno en el estado proverbial de "saber lo suficiente como para ser peligroso".
Familiarizarse con los matices clave de Python, como (entre otros) los problemas de programación moderadamente avanzados planteados en este artículo, ayudará a optimizar el uso del lenguaje y evitará algunos de sus errores más comunes.
También puede consultar nuestra Guía de información privilegiada para entrevistas de Python para obtener sugerencias sobre las preguntas de la entrevista que pueden ayudar a identificar a los expertos en Python.
Esperamos que haya encontrado útiles los consejos de este artículo y agradecemos sus comentarios.
