Link sitio oficial aquí
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.
(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 "
", line 3, in 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 "", line 1, in
File "", 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í.)
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 "", line 1, in File "", 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 "
", line 2, in 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]
Error común # 6: La Confusión de Cómo Python Enlaza las
Variables en los Cierres
Teniendo en cuenta el siguiente ejemplo:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>>
for multiplier in create_multipliers(): ... print multiplier(2) ...
Deberías esperar el siguiente resultado:
0 2 4 6 8
Pero en realidad obtienes:
8 8 8 8 8
¡Sorpresa!
Esto sucede debido al
comportamiento enlace tardío de Python, que dice que los valores de
las variables utilizadas en los cierres, se buscan en el momento en el que se
llama a la función interna. Así que en el
código anterior, cuando cualquiera de las funciones devueltas es llamada, el
valor de
i
se busca en el ámbito que lo rodea en el momento en que se
llama (y en ese
momento, el círculo se ha completado, por lo que a i
ya se le ha asignado su valor final de 4).
La solución a este problema común Python es un poco de hack:
>>> 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
¡Voilà! Estamos tomando ventaja de los argumentos por defecto para
generar funciones anónimas, con el 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 entender esto.
Error común # 7: Crear Dependencias de Módulos Circulares
Digamos que tienes 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 b.x print f()
Y en
b.py
:import a x = 1 def g(): print a.f()
En primer lugar, vamos a tratar de importar
a.py
:>>> import a 1
Funcionó muy bien. Tal vez te sorprendió. Después de todo, tenemos una
importación circular aquí que presumiblemente debería ser un problema, ¿no?
La respuesta es que
la mera presencia de una importación circular no es como
tal un problema en Python. Si un módulo ya se ha importado, Python es lo
suficientemente inteligente como para no intentar volverlo a importar. Sin
embargo, dependiendo del punto en el que cada módulo está intentando acceder a
las funciones o variables definidas en el otro, podrías tener problemas.
Así que volviendo a
nuestro ejemplo, cuando importamos
a.py
, no tenía ningún problema al importar b.py
, ya que b.py
no requiere nada de b.py
para definirse en el momento de su importación.
La única referencia en b.py
a a
, es el llamado a a.f()
. Pero ese llamado está en g()
y nada en a.py
o b.py
invoca g()
. Así que, la
vida es bella.
Pero ¿qué ocurre si se intenta importar
b.py
(claro, sin haber previamente importado a.py
):>>> import b Traceback (most recent call last): File "", line 1, in
File "b.py", line 1, in import a File "a.py", line 6, in
print f() File "a.py", line 4, in f return b.x AttributeError: 'module'
object has no attribute 'x'
Uh oh. ¡Eso no es
bueno! El problema aquí es que, en el proceso de importación
b.py
, intenta importar a.py
, lo que como resultado llama a f()
, que a su vez intenta acceder a b.x
. Pero b.x
aún no ha sido definida. De ahí la excepción AttributeError
.
Al menos una
solución a esto es bastante trivial. Simplemente modifica
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 a.f()
Cuando se importa, todo está bien:
>>> import b >>> b.g() 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 # 8: Choque de Nombres con Módulos de la
Biblioteca Estándar de Python
Una de las ventajas
de Python es la gran cantidad de módulos de biblioteca que trae desde el
principio. Pero como resultado, si no estás evitando
esto conscientemente, no es tan difícil encontrarse con un choque de nombres,
entre el nombre de uno de tus módulos y un módulo con el mismo nombre en la
biblioteca estándar que se incluye en Python (por ejemplo, es posible que
tengas un módulo denominado
email.py
en tu código, lo que estaría en conflicto con el módulo de
biblioteca estándar del mismo nombre).
Esto puede conducir a problemas muy agresivos, como la importación de
otra biblioteca que a su vez intenta importar la versión de bibliotecas
estándar Python de un módulo, pero como ya tienes un módulo con el mismo
nombre, el otro paquete importa erróneamente tu versión, en lugar de la que se
encuentra dentro de la biblioteca estándar Python, y es aquí es donde se
producen los errores más graves.
Por lo tanto, se
debe tener cuidado para evitar el uso de los mismos nombres que los de los
módulos de biblioteca estándar Python. Es mucho más fácil para ti cambiar el
nombre de un módulo dentro de tu paquete que presentar una propuesta de mejora
de Python (PEP) para solicitar un cambio de nombre upstream y
conseguir que lo aprueben.
Error común # 9: El No Poder Hacer Frente a las
Diferencias entre Python 2 y Python 3
Considera 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 muy bien:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
Pero ahora vamos a dar un giro en Python 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19,
in bad() File "foo.py", line 17, in bad print(e) UnboundLocalError:
local variable 'e' referenced before assignment
¿Qué acaba de
ocurrir aquí? El “problema” es que en Python 3 el objeto de excepción no es
accesible más allá del alcance del bloque
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
se ejecute el recolector de basura y purgue 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 except, de modo que siga siendo
accesible. Aquí hay una versión del ejemplo anterior
que utiliza esta técnica, por lo tanto, dosificando el código y haciéndole más
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()
La ejecución de éste es en Py3k:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
¡Yupi!
(Por cierto, nuestra Guía de contratación Python
(Python Hiring Guide) analiza
una serie de diferencias importantes, que se deben tener en cuenta al migrar el
código de Python 2 a Python 3.)
Error común # 10: El Mal Uso del
Método __del__
Digamos que tenías
esto en un archivo llamado
mod.py
:import foo class Bar(object): ... def __del__(self):
foo.cleanup(self.myhandle)
Y, luego, trataste de hacer esto desde
another_mod.py
:import mod mybar = mod.Bar()
Obtendrías una fea
excepción
AttributeError
.
¿Por qué? Debido a
que, como se informó aquí, al apagarse intérprete, las variables globales
del módulo se ajustan a
None
. Como resultado, en el ejemplo anterior, en el
punto que __del__
se invoca, el nombre foo ya se ha ajustado a None
.
Una solución a este
problema algo más avanzado que la programación Python, sería utilizar
atexit.register()
en su lugar. De esta manera, cuando el
programa se haya ejecutado (al salir normalmente, quiero decir), tus gestores
registrados son echados antes de que el intérprete se apague.
Con éste
conocimiento, una solución para el anterior código
mod.py
podría ser algo como esto:import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object):
def __init__(self): ... atexit.register(cleanup, self.myhandle)
Esta aplicación ofrece una manera limpia y confiable de llamar a
cualquier funcionalidad de limpieza necesaria para después de la terminación
normal del programa. Obviamente, le
toca a foo.cleanup decidir qué hacer con el objeto unido al nombre
self.myhandle, pero se entiende la idea.
Para Terminar
Python es un lenguaje potente y flexible con muchos mecanismos y
paradigmas que pueden mejorar considerablemente la productividad. Sin embargo,
al igual que con cualquier herramienta de software o lenguaje, el tener una
limitada comprensión o apreciación de sus capacidades a veces puede ser más un
impedimento que una ventaja, dejándonos en el estado proverbial de “saber lo
suficiente como para ser peligroso”.
Familiarizándose con los matices clave de Python, tales como (pero de
ninguna manera se limita a) los problemas de programación moderadamente
avanzados planteados en este artículo, ayudará a optimizar el uso de la lengua,
evitando algunos de sus errores más comunes.
Deberías revisar nuestra Guía a Entrevistas Python (Insider’s Guide
to Python Interviewing), para obtener sugerencias sobre preguntas de entrevistas que pueden
ayudar a identificar a los expertos en Python.
Esperamos que te sean útiles los consejos en este artículo y apreciamos
tus comentarios.
No hay comentarios:
Publicar un comentario
Te agradezco tus comentarios. Te esperamos de vuelta.