Capítulo 4. Escribir un gran código
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Este capítulo se centra en las buenas prácticas para escribir un gran código Python. Repasaremos las convenciones de estilo de codificación que se utilizarán en el Capítulo 5, y cubriremos brevemente las mejores prácticas de registro, además de enumerar algunas de las principales diferencias entre las licencias de código abierto disponibles. Todo esto pretende ayudarte a escribir un código que sea fácil de utilizar y ampliar para nosotros, tu comunidad.
Código Estilo
Los pythonistas (desarrolladores veteranos de Python) celebran tener un lenguaje tan accesible que personas que nunca han programado pueden entender lo que hace un programa Python cuando leen su código fuente. La legibilidad está en el corazón del diseño de Python, tras el reconocimiento de que el código se lee mucho más a menudo de lo que se escribe.
Una de las razones por las que el código Python puede entenderse fácilmente es su conjunto relativamente completo de directrices de estilo de código (recogidas en las dos Propuestas de Mejora de Python PEP 20 y PEP 8, que se describen en las páginas siguientes) y modismos "pitónicos". Cuando un pitonista señala partes de código y dice que no son "pitónicas", suele significar que esas líneas de código no siguen las directrices comunes y no expresan la intención de la forma que se considera más legible. Por supuesto, "una consistencia necia es el duende de las mentes pequeñas".1 La devoción pedante a la letra del PEP puede socavar la legibilidad y la comprensibilidad.
PEP 8
PEP 8 es la guía de estilo de código de facto para Python. Abarca las convenciones de nomenclatura, la disposición del código, los espacios en blanco (tabuladores frente a espacios) y otros temas de estilo similares.
Se trata de una lectura muy recomendable. Toda la comunidad Python hace todo lo posible por seguir las directrices establecidas en este documento. Algunos proyectos pueden desviarse de él de vez en cuando, mientras que otros (comoRequests) pueden modificar sus recomendaciones.
Conformar tu código Python a la PEP 8 es generalmente una buena idea y ayuda a que el código sea más coherente cuando se trabaja en proyectos con otros desarrolladores. Las directrices de la PEP 8 son lo suficientemente explícitas como para que puedan comprobarse mediante programación. Existe un programa de línea de comandos, pep8
que puede comprobar la conformidad de tu código. Instálalo ejecutando el siguiente comando en tu terminal:
$
pip3 install pep8
He aquí un ejemplo del tipo de cosas que puedes ver cuando ejecutas pep8
:
$ pep8 optparse.py
optparse.py:69:11: E401 multiple imports on one line optparse.py:77:1: E302 expected 2 blank lines, found 1 optparse.py:88:5: E301 expected 1 blank line, found 0 optparse.py:222:34: W602 deprecated form of raising exception optparse.py:347:31: E211 whitespace before '(' optparse.py:357:17: E201 whitespace after '{' optparse.py:472:29: E221 multiple spaces before operator optparse.py:544:21: W601 .has_key() is deprecated, use 'in'
Las soluciones a la mayoría de las quejas son sencillas y se indican directamente en la PEP 8. Laguía de estilo de código para las Peticionesda ejemplos de código bueno y malo y sólo está ligeramente modificada respecto al PEP 8 original.
Los linters a los que se hace referencia en "Editores de texto" suelen utilizar pep8
, por lo que también puedes instalar uno de ellos para ejecutar comprobaciones dentro de tu editor o IDE. O bien, puedes utilizar el programa autopep8
para reformatear automáticamente el código al estilo PEP 8. Instala el programa con:
$
pip3 install autopep8
Utilízalo para formatear un archivo in situ (sobrescribiendo el original) con:
$
autopep8 --in-place optparse.py
Excluir la bandera --in-place
hará que el programa envíe el código modificado directamente a la consola para su revisión (o canalizado a otro archivo). La bandera --aggressive
realizará cambios más sustanciales y puede aplicarse varias veces para un mayor efecto.
PEP 20 (también conocido como El Zen de Python)
PEP20, el conjunto de principios rectores para la toma de decisiones en Python, está siempre disponible a través de import this
en un shell de Python. A pesar de su nombre, PEP 20 sólo contiene 19 aforismos, no 20 (el último no se ha escrito...).
La verdadera historia del Zen de Python está inmortalizada en la entrada del blog de Barry Warsaw "importar esto y el Zen de Python".
Para ver un ejemplo de cada aforismo Zen en acción, consulta la presentación de Hunter Blanks"PEP 20 (El Zen de Python) por ejemplo".Raymond Hettinger también dio un uso fantástico a estos principios en su charla "Más allá de PEP 8: buenas prácticas para un código bello e inteligible".
Consejos generales
Esta sección contiene conceptos de estilo que esperamos que sean fáciles de aceptar sin debate, y a menudo aplicables a lenguajes distintos de Python. Algunos de ellos proceden directamente del Zen de Python, pero otros son simplemente de sentido común. Reafirman nuestra preferencia en Python por seleccionar la forma más obvia de presentar el código, cuando hay varias opciones posibles.
Lo explícito es mejor que lo implícito
Aunque cualquier tipo de magia negra es posible con Python, se prefiere la forma más sencilla y explícita de expresar algo:
Mal | Bien |
---|---|
|
|
En el código bueno, x
y y
se reciben explícitamente de la persona que llama, y se devuelve un diccionario explícito. Una buena regla general es que otro desarrollador pueda leer la primera y la última línea de tu función y entender lo que hace. No es el caso del mal ejemplo. (Por supuesto, también es bastante fácil cuando la función sólo tiene dos líneas).
Lo disperso es mejor que lo denso
Haz sólo una sentencia por línea. Algunas sentencias compuestas, como las comprensiones de listas, están permitidas y son apreciadas por su brevedad y su expresividad, pero es una buena práctica mantener las sentencias disjuntas en líneas de código separadas. También hace que las diferencias sean más comprensibles3 cuando se revisa una sentencia:
Mal | Bien |
---|---|
|
|
|
|
|
|
Las ganancias en legibilidad, para los Pythonistas, son más valiosas que unos pocos bytes de código total (para la sentencia dos-impresiones-en-una-línea) o unos pocos microsegundos de tiempo de cálculo (para la sentencia extra-condiciones-en-líneas-separadas). Además, cuando un grupo contribuye al código abierto, el historial de revisiones del código "bueno" será más fácil de descifrar, porque un cambio en una línea sólo puede afectar a una cosa.
Los errores nunca deben pasar en silencio / A menos que se silencien explícitamente
El tratamiento de errores en Python se realiza mediante la sentencia try
. Un ejemplo del paquete HowDoI de Ben Gleitzman (descrito con más detalle en "HowDoI") muestra cuándo está bien silenciar un error:
def
format_output
(
code
,
args
):
if
not
args
[
'color'
]:
return
code
lexer
=
None
# try to find a lexer using the Stack Overflow tags
# or the query arguments
for
keyword
in
args
[
'query'
]
.
split
()
+
args
[
'tags'
]:
try
:
lexer
=
get_lexer_by_name
(
keyword
)
break
except
ClassNotFound
:
pass
# no lexer found above, use the guesser
if
not
lexer
:
lexer
=
guess_lexer
(
code
)
return
highlight
(
code
,
lexer
,
TerminalFormatter
(
bg
=
'dark'
))
Forma parte de un paquete que proporciona un script de línea de comandos para consultar en Internet (Stack Overflow, por defecto) cómo realizar una determinada tarea de codificación, e imprimirlo en la pantalla. La funciónformat_output()
aplica el resaltado de sintaxis buscando primero entre las etiquetas de la pregunta una cadena que entienda el lexer (también llamado tokenizador; una etiqueta "python", "java" o "bash" identificará qué lexer utilizar para dividir y colorear el código), y luego, si eso falla, intentar deducir el lenguaje a partir del propio código. Hay tres caminos que el programa puede seguir cuando llega a la sentencia try
:
-
La ejecución entra en la cláusula
try
(todo lo que hay entretry
yexcept
), se encuentra con éxito un lexer, se rompe el bucle y la función devuelve el código resaltado con el lexer seleccionado. -
Si no se encuentra el lexer, se lanza la excepción
ClassNotFound
, se atrapa y no se hace nada. El bucle continúa hasta que termina de forma natural o se encuentra un lexer. -
Se produce alguna otra excepción (como un
KeyboardInterrupt
) que no se maneja, y se eleva al nivel superior, deteniendo la ejecución.
La parte "nunca debe pasar en silencio" del aforismo zen desaconseja el uso de trampas de error demasiado entusiastas. Aquí tienes un ejemplo que puedes probar en un terminal separado para que puedas eliminarlo más fácilmente una vez que le hayas pillado el punto:
>>>
while
True
:
...
try
:
...
(
"nyah"
,
end
=
" "
)
...
except
:
...
pass
O no lo intentes. La cláusula except
sin ninguna excepción especificada lo atrapará todo, incluido KeyboardInterrupt
(Ctrl+C en un terminal POSIX), y lo ignorará; así que se traga las docenas de interrupciones que intentes darle para apagar el cacharro. No es sólo el problema de las interrupciones: una cláusula except
amplia también puede ocultar errores, dejándolos para que causen algún problema más adelante, cuando será más difícil de diagnosticar. Repetimos, no dejes que los errores pasen silenciosamente: identifica siempre explícitamente por su nombre las excepciones que vas a atrapar, y maneja sólo esas excepciones. Si simplemente quieres registrar o reconocer de otro modo la excepción y volver a lanzarla, como en el siguiente fragmento, no pasa nada. Pero no dejes que el error pase silenciosamente (sin manejarlo ni volver a lanzarlo):
>>> while True: ... try: ... print("ni", end="-") ... except: ... print("An exception happened. Raising.") ... raise
Los argumentos de las funciones deben ser intuitivos
Tus elecciones en el diseño de la API determinarán la experiencia del desarrollador descendente al interactuar con una función. Los argumentos pueden pasarse a las funciones de cuatro formas distintas:
def
func
(
positional
,
keyword
=
value
,
*
args
,
*
*
kwargs
)
:
pass
Los argumentos posicionales son obligatorios y no tienen valores por defecto.
Los argumentos de las palabras clave son opcionales y tienen valores por defecto.
Una lista de argumentos arbitraria es opcional y no tiene valores por defecto.
Un diccionario de argumentos de palabra clave arbitraria es opcional y no tiene valores por defecto.
Aquí tienes consejos sobre cuándo utilizar cada método de paso de argumentos:
- Argumentos posicionales
-
Utilízalos cuando sólo haya unos pocos argumentos de función, que formen parte plenamente del significado de la función, con un orden natural. Por ejemplo, en
send(message, recipient)
opoint(x, y)
el usuario de la función no tiene dificultad en recordar que esas dos funciones requieren dos argumentos, y en qué orden.Antipatrón de uso: Es posible utilizar nombres de argumentos y cambiar el orden de los argumentos al llamar a funciones; por ejemplo, llamar a
send(recipient="World", message="The answer is 42.")
ypoint(y=2, x=1)
. Esto reduce la legibilidad y es innecesariamente verboso. Utiliza las llamadas más directas asend("The answer is 42", "World")
ypoint(1, 2)
. - Argumentos de las palabras clave
-
Cuando una función tiene más de dos o tres parámetros posicionales, su firma es más difícil de recordar, y resulta útil utilizar argumentos de palabra clave con valores por defecto. Por ejemplo, una función
send
más completa podría tener la firmasend(message, to, cc=None, bcc=None)
. Aquícc
ybcc
son opcionales y se evalúan aNone
cuando no se les pasa otro valor.Antipatrón de uso: Es posible seguir el orden de los argumentos en la definición sin nombrar explícitamente los argumentos, como en
send("42", "Frankie", "Benjy", "Trillian")
, enviando una copia oculta a Trillian. También es posible nombrar los argumentos en otro orden, como ensend("42", "Frankie", bcc="Trillian", cc="Benjy")
. A menos que haya una razón de peso para no hacerlo, es mejor utilizar la forma más parecida a la definición de la función:send("42", "Frankie", cc="Benjy", bcc="Trillian")
.
A menudo nunca es mejor que ahora mismo
A menudo es más difícil eliminar un argumento opcional (y su lógica dentro de la función) que se añadió "por si acaso" y aparentemente nunca se utiliza, que añadir un nuevo argumento opcional y su lógica cuando se necesita.
- Lista arbitraria de argumentos
-
Definido con la construcción
*args
, denota un número extensible de argumentos posicionales. En el cuerpo de la función,args
será una tupla de todos los argumentos posicionales restantes. Por ejemplo, también se puede llamar asend(message, *args)
con cada destinatario como argumento:send("42", "Frankie", "Benjy", "Trillian")
; y en el cuerpo de la función,args
será igual a("Frankie", "Benjy", "Trillian")
. Un buen ejemplo de cuándo funciona esto es la funciónprint
.Advertencia: Si una función recibe una lista de argumentos de la misma naturaleza, a menudo es más claro utilizar una lista o cualquier secuencia. Aquí, si
send
tiene varios destinatarios, podemos definirlo explícitamente:send(message, recipients)
y llamarlo consend("42", ["Benjy", "Frankie", "Trillian"])
. - Diccionario de argumentos de palabras clave arbitrarias
-
Definida mediante la construcción
**kwargs
, pasa una serie indeterminada de argumentos con nombre a la función. En el cuerpo de la función,kwargs
será un diccionario de todos los argumentos con nombre pasados que no hayan sido capturados por otros argumentos de palabra clave en la firma de la función. Un ejemplo de cuando esto es útil es en el registro; los formateadores de distintos niveles pueden tomar sin problemas la información que necesiten sin molestar al usuario.Advertencia: Es necesaria la misma precaución que en el caso de
*args
, por razones similares: estas potentes técnicas deben utilizarse cuando exista una necesidad probada de utilizarlas, y no deben emplearse si la construcción más sencilla y clara es suficiente para expresar la intención de la función.
Nota
Los nombres de las variables *args
y **kwargs
pueden (y deben) sustituirse por otros nombres, cuando éstos tengan más sentido.
Corresponde al programador que escribe la función determinar qué argumentos son argumentos posicionales y cuáles son argumentos opcionales de palabra clave, y decidir si utiliza las técnicas avanzadas de paso arbitrario de argumentos. Después de todo, debería haber una -y preferiblemente sólo una- forma obvia de hacerlo. Los demás usuarios apreciarán tu esfuerzo cuando tus funciones Python sean:
-
Fácil de leer (lo que significa que el nombre y los argumentos no necesitan explicación)
-
Fácil de cambiar (es decir, añadir un nuevo argumento de palabra clave no romperá otras partes del código)
Si la aplicación es difícil de explicar, es una mala idea
Python, una poderosa herramienta para hackers, viene con un conjunto muy rico de ganchos y herramientas que te permiten hacer casi cualquier tipo de trucos complicados. Por ejemplo, es posible:
-
Cambiar cómo se crean e instancian los objetos
-
Cambiar cómo importa módulos el intérprete de Python
-
Incrustar rutinas C en Python
Todas estas opciones tienen inconvenientes, y siempre es mejor utilizar la forma más sencilla de lograr tu objetivo. El principal inconveniente es que la legibilidad se resiente al utilizar estas construcciones, por lo que lo que ganes debe ser más importante que la pérdida de legibilidad. Muchas herramientas de análisis de código, como pylint o pyflakes, serán incapaces de analizar este código "mágico".
Un desarrollador de Python debe conocer estas posibilidades casi infinitas, porque infunde confianza en que no habrá ningún problema infranqueable en el camino. Sin embargo, es muy importante saber cómo y, sobre todo, cuándo no utilizarlas.
Como un maestro de kung fu, un pitonista sabe cómo matar con un solo dedo, y nunca hacerlo realmente.
Todos somos usuarios responsables
Como ya se ha demostrado, Python permite muchos trucos, y algunos de ellos son potencialmente peligrosos. Un buen ejemplo es que cualquier código cliente puede anular las propiedades y métodos de un objeto: en Python no existe la palabra clave "private". Esta filosofía es muy diferente de la de lenguajes altamente defensivos como Java, que proporcionan muchos mecanismos para evitar cualquier uso indebido, y se expresa con el dicho: "Todos somos usuarios responsables".
Esto no significa que, por ejemplo, ninguna propiedad se considere privada, ni que sea imposible una encapsulación adecuada en Python. Más bien, en lugar de confiar en muros de hormigón erigidos por los desarrolladores entre su código y el de los demás, la comunidad Python prefiere basarse en un conjunto de convenciones que indican que no se debe acceder directamente a estos elementos.
La convención principal para las propiedades privadas y los detalles de implementación es anteponer un guión bajo a todos los elementos "internos" (por ejemplo, sys._getframe
). Si el código cliente se salta esta regla y accede a estos elementos marcados, cualquier mal comportamiento o problema que surja si se modifica el código es responsabilidad del código cliente.
Se recomienda utilizar generosamente esta convención: cualquier método o propiedad que no esté destinado a ser utilizado por el código cliente debe ir precedido de un guión bajo. Esto garantizará una mejor separación de funciones y una modificación más fácil del código existente; siempre será posible hacer pública una propiedad privada, pero hacer privada una propiedad pública puede ser una operación mucho más difícil.
Devuelve los valores de un lugar
Cuando una función crece en complejidad, no es infrecuente utilizar varias sentencias return dentro del cuerpo de la función. Sin embargo, para mantener una intención clara y mantener la legibilidad, es mejor devolver valores significativos desde el menor número posible de puntos del cuerpo.
Las dos formas de salir de una función son por error o con un valor de retorno después de que la función se haya procesado con normalidad. En los casos en que la función no pueda funcionar correctamente, puede ser conveniente devolver un valor None
o False
. En este caso, es mejor retornar de la función tan pronto como se haya detectado el contexto incorrecto, para aplanar la estructura de la función: todo el código posterior a la declaración de retorno por error puede asumir que se cumple la condición para seguir calculando el resultado principal de la función. A menudo es necesario disponer de varias declaraciones de retorno de este tipo.
Aun así, siempre que sea posible, mantén un único punto de salida: es difícil depurar funciones cuando primero tienes que identificar qué declaración de retorno es responsable de su resultado. Forzar a la función a salir en un único lugar también ayuda a factorizar algunas rutas de código, ya que los múltiples puntos de salida probablemente sean una pista de que es necesaria dicha refactorización. Este ejemplo no es un código malo, pero posiblemente podría ser más claro, como se indica en los comentarios :
def
select_ad
(
third_party_ads
,
user_preferences
):
if
not
third_party_ads
:
return
None
# Raising an exception might be better
if
not
user_preferences
:
return
None
# Raising an exception might be better
# Some complex code to pick the best_ad given the
# available ads and the individual's preferences...
# Resist the temptation to return best_ad if succeeded...
if
not
best_ad
:
# Some Plan-B computation of best_ad
return
best_ad
# A single exit point for the returned value
# will help when maintaining the code
Convenios
Las convenciones tienen sentido para todos, pero pueden no ser la única forma de hacer las cosas. Las convenciones que mostramos aquí son las opciones más utilizadas, y las recomendamos como la opción más legible.
Alternativas a la comprobación de la igualdad
Cuando no necesites comparar explícitamente un valor conTrue
, o None
, o 0
, puedes simplemente añadirlo a la declaración if
, como en los siguientes ejemplos. (Consulta"Comprobación del valor verdadero"para obtener una lista de lo que se considera falso).
Mal | Bien |
---|---|
|
|
|
|
Acceder a los elementos del diccionario
Utiliza la sintaxis x in d
en lugar del método dict.has_key
, o pasa un argumento por defecto a dict.get()
:
Mal | Bien |
---|---|
|
|
Manipular listas
Las comprensiones de listas proporcionan una forma potente y concisa de trabajar con listas (para más información, consulta la entrada El tutorial de Python). Además, las funciones map()
y filter()
pueden realizar operaciones con listas utilizando una sintaxis diferente y más concisa:
Bucle estándar | Comprensión de la lista |
---|---|
|
|
|
|
Utiliza enumerate()
para mantener un recuento de tu lugar en la lista. Es más legible que crear manualmente un contador, y está mejor optimizado para los iteradores:
>>>
a
=
[
"icky"
,
"icky"
,
"icky"
,
"p-tang"
]
>>>
for
i
,
item
in
enumerate
(
a
):
...
(
"{i}: {item}"
.
format
(
i
=
i
,
item
=
item
))
...
0
:
icky
1
:
icky
2
:
icky
3
:
p
-
tang
Continuación de una larga línea de código
Cuando una línea lógica de código es más larga que el límite aceptado,4 necesitas dividirla en varias líneas físicas. El intérprete de Python unirá líneas consecutivas si el último carácter de la línea es una barra invertida. Esto es útil en algunos casos, pero normalmente debe evitarse debido a su fragilidad: un carácter de espacio en blanco añadido al final de la línea, después de la barra invertida, romperá el código y puede tener resultados inesperados.
Una solución mejor es utilizar paréntesis alrededor de tus elementos. Si se deja un paréntesis sin cerrar al final de una línea, el intérprete de Python se unirá a la línea siguiente hasta que se cierre el paréntesis. El mismo comportamiento se aplica a las llaves rizadas y cuadradas:
Mal | Bien |
---|---|
|
|
|
|
Sin embargo, la mayoría de las veces, tener que dividir una línea lógica larga es señal de que estás intentando hacer demasiadas cosas a la vez, lo que puede dificultar la legibilidad.
Idiomas
Aunque normalmente hay una -y preferiblemente sólo una- forma obvia de hacerlo, la forma de escribir código idiomático (o pitónico) puede no ser obvia para los principiantes en Python al principio (a menos que sean holandeses5), por lo que los buenos modismos deben adquirirse conscientemente.
Desembalaje
Si conoces la longitud de una lista o tupla, puedes asignar nombres a sus elementos con la descompresión. Por ejemplo, como es posible especificar el número de veces que hay que dividir una cadena en split()
y rsplit()
, se puede hacer que el lado derecho de una asignación se divida sólo una vez (por ejemplo, en un nombre de archivo y una extensión), y que el lado izquierdo contenga ambos destinos simultáneamente, en el orden correcto, así:
>>>
filename
,
ext
=
"my_photo.orig.png"
.
rsplit
(
"."
,
1
)
>>>
(
filename
,
"is a"
,
ext
,
"file."
)
my_photo
.
orig
is
a
png
file
.
También puedes utilizar el desempaquetado para intercambiar variables:
a
,
b
=
b
,
a
El desempaquetado anidado también funciona:
a
,
(
b
,
c
)
=
1
,
(
2
,
3
)
En Python 3,la PEP 3132 introdujo un nuevo método de desempaquetado ampliado:
a
,
*
rest
=
[
1
,
2
,
3
]
# a = 1, rest = [2, 3]
a
,
*
middle
,
c
=
[
1
,
2
,
3
,
4
]
# a = 1, middle = [2, 3], c = 4
Ignorar un valor
Si necesitas asignar algo mientras descomprimes, pero no vas a necesitar esa variable, utiliza un guión bajo doble (__
):
filename
=
'foobar.txt'
basename
,
__
,
ext
=
filename
.
rpartition
(
'.'
)
Nota
Muchas guías de estilo de Python recomiendan un guión bajo simple (_
) para las variables desechables, en lugar del guión bajo doble (__
) recomendado aquí. La cuestión es que un guión bajo simple se utiliza habitualmente como alias de la función gettext.gettext()
, y también se utiliza en el indicador interactivo para mantener el valor de la última operación. Utilizar en su lugar un guión bajo doble es igual de claro y casi igual de cómodo, y elimina el riesgo de sobrescribir accidentalmente la variable de guión bajo simple, en cualquiera de estos otros casos de uso.
Crear una lista de longitud-N de lo mismo
Utiliza el operador de lista de Python *
para hacer una lista del mismo elemento inmutable:
>>>
four_nones
=
[
None
]
*
4
>>>
(
four_nones
)
[
None
,
None
,
None
,
None
]
Pero ten cuidado con los objetos mutables: como las listas son mutables, el operador *
creará una lista de N referencias a la misma lista, que no es probablemente lo que quieres. En lugar de eso, utiliza una comprensión de lista:
Mal | Bien |
---|---|
|
|
Una forma habitual de crear cadenas es utilizar str.join()
en una cadena vacía. Este modismo puede aplicarse a listas y tuplas:
>>>
letters
=
[
's'
,
'p'
,
'a'
,
'm'
]
>>>
word
=
''
.
join
(
letters
)
>>>
(
word
)
spam
A veces necesitamos buscar en una colección de cosas. Veamos dos opciones: listas y conjuntos.
Toma como ejemplo el código siguiente
>>>
x
=
list
((
'foo'
,
'foo'
,
'bar'
,
'baz'
))
>>>
y
=
set
((
'foo'
,
'foo'
,
'bar'
,
'baz'
))
>>>
>>>
(
x
)
[
'foo'
,
'foo'
,
'bar'
,
'baz'
]
>>>
(
y
)
{
'foo'
,
'bar'
,
'baz'
}
>>>
>>>
'foo'
in
x
True
>>>
'foo'
in
y
True
Aunque ambas pruebas booleanas de pertenencia a listas y conjuntos parecen idénticas, foo in y
está utilizando el hecho de que los conjuntos (y diccionarios) en Python son tablas hash,6 el rendimiento de la búsqueda entre los dos ejemplos es diferente. Python tendrá que recorrer cada elemento de la lista para encontrar un caso coincidente, lo que lleva mucho tiempo (la diferencia de tiempo se hace significativa para colecciones más grandes). Pero encontrar claves en el conjunto puede hacerse rápidamente, utilizando la búsqueda hash. Además, los conjuntos y losdiccionarios descartan las entradas duplicadas, razón por la cual los diccionarios no pueden tener dos claves idénticas. Para más información, consulta estedebate de Stack Overflowsobre lista frente a diccionario.
Contextos a prueba de excepciones
Es habitual utilizar cláusulas try/finally
para gestionar recursos como archivos o bloqueos de hilos cuando pueden producirse excepciones.La PEP 343introdujo la sentencia with
y un protocolo de gestión de contextos en Python (en la versión 2.5 y posteriores), un modismo para sustituir estas cláusulas try/finally
por código más legible. El protocolo consiste en dos métodos, __enter__()
y __exit__()
, que cuando se implementan para un objeto permiten utilizarlo a través de la nueva sentencia with
, de la siguiente manera:
>>>
import
threading
>>>
some_lock
=
threading
.
Lock
()
>>>
>>>
with
some_lock
:
...
# Make Earth Mark One, run it for 10 million years ...
...
(
...
"Look at me: I design coastlines.
\n
"
...
"I got an award for Norway."
...
)
...
que antes hubiera sido:
>>>
import
threading
>>>
some_lock
=
threading
.
Lock
()
>>>
>>>
some_lock
.
acquire
()
>>>
try
:
...
# Make Earth Mark One, run it for 10 million years ...
...
(
...
"Look at me: I design coastlines.
\n
"
...
"I got an award for Norway."
...
)
...
finally
:
...
some_lock
.
release
()
El módulo de la biblioteca estándar contextlib
proporciona herramientas adicionales que ayudan a convertir funciones en gestores de contexto, imponer la llamada al método close()
de un objeto, suprimir excepciones (Python 3.4 y superior) y redirigir flujos de salida y error estándar (Python 3.4 o 3.5 y superior). He aquí un ejemplo de uso de contextlib.closing()
:
>>>
from
contextlib
import
closing
>>>
with
closing
(
open
(
"outfile.txt"
,
"w"
))
as
output
:
...
output
.
write
(
"Well, he's...he's, ah...probably pining for the fjords."
)
...
56
pero como los métodos __enter__()
y __exit__()
están definidos para el objeto que gestiona la E/S de archivos,7
podemos utilizar la declaración with
directamente, sin la closing
:
>>>
with
open
(
"outfile.txt"
,
"w"
)
as
output
:
output
.
write
(
"PININ' for the FJORDS?!?!?!? "
"What kind of talk is that?, look, why did he fall "
"flat on his back the moment I got 'im home?
\n
"
)
...
123
Errores comunes
En su mayor parte, Python pretende ser un lenguaje limpio y coherente que evite las sorpresas. Sin embargo, hay algunos casos que pueden resultar confusos para los recién llegados.
Algunos de estos casos son intencionados, pero pueden ser potencialmente sorprendentes. Algunos podrían considerarse verrugas del lenguaje. En general, sin embargo, lo que sigue es una colección de comportamientos potencialmente engañosos que pueden parecer extraños a primera vista, pero que suelen ser sensatos una vez que eres consciente de la causa subyacente de la sorpresa.
Argumentos por defecto mutables
Parece que la sorpresa más común con la que se encuentran los nuevos programadores de Python es el tratamiento que da Python a los argumentos por defecto mutables en las definiciones de funciones.
- Lo que escribiste:
-
def
append_to
(
element
,
to
=
[]):
to
.
append
(
element
)
return
to
- Lo que podías esperar que ocurriera:
-
my_list
=
append_to
(
12
)
print
(
my_list
)
my_other_list
=
append_to
(
42
)
print
(
my_other_list
)
Se crea una nueva lista cada vez que se llama a la función si no se proporciona un segundo argumento, para que la salida sí lo sea:
[
12
]
[
42
]
- Lo que ocurre en realidad:
-
[
12
]
[
12
,
42
]
Se crea una nueva lista una vez cuando se define la función, y se utiliza la misma lista en cada llamada sucesiva: Los argumentos por defecto de Python se evalúan una vez cuando se define la función, no cada vez que se llama a la función (como ocurre, por ejemplo, en Ruby). Esto significa que si utilizas un argumento por defecto mutable y lo mutas , también habrás mutado ese objeto para todas las llamadas futuras a la función.
- Lo que debes hacer en su lugar:
-
Crea un nuevo objeto cada vez que se llame a la función, utilizando un arg por defecto para señalar que no se ha proporcionado ningún argumento (
None
suele ser una buena opción):def
append_to
(
element
,
to
=
None
):
if
to
is
None
:
to
=
[]
to
.
append
(
element
)
return
to
- Cuando este gotcha no es un gotcha:
-
A veces puedes "explotar" específicamente (es decir, utilizar según lo previsto) este comportamiento para mantener el estado entre las llamadas de una función. Esto se hace a menudo al escribir una función de caché (que almacena resultados en memoria), por ejemplo:
def
time_consuming_function
(
x
,
y
,
cache
=
{}):
args
=
(
x
,
y
)
if
args
in
cache
:
return
cache
[
args
]
# Otherwise this is the first time with these arguments.
# Do the time-consuming operation...
cache
[
args
]
=
result
return
result
Cierres de encuadernación tardíos
Otra fuente habitual de confusión es la forma en que Python vincula sus variables en los cierres (o en el ámbito global circundante).
- Lo que escribiste:
-
def
create_multipliers
():
return
[
lambda
x
:
i
*
x
for
i
in
range
(
5
)]
- Lo que podías esperar que ocurriera:
-
for
multiplier
in
create_multipliers
():
print
(
multiplier
(
2
),
end
=
" ... "
)
print
()
Una lista que contiene cinco funciones que tienen cada una su propia variable cerrada
i
que multiplica su argumento, produciendo:0
...
2
...
4
...
6
...
8
...
- Lo que ocurre en realidad:
-
8
...
8
...
8
...
8
...
8
...
Se crean cinco funciones; en cambio, todas ellas se limitan a multiplicar
x
por 4. ¿Por qué? Los cierres de Python son de enlace tardío. Esto significa que los valores de las variables utilizadas en los cierres se buscan en el momento en que se llama a la función interna.Aquí, cada vez que se llama a cualquiera de las funciones devueltas, se busca el valor de
i
en el ámbito circundante en el momento de la llamada. Para entonces, el bucle ya se ha completado, yi
se queda con su valor final de 4.Lo más desagradable de este problema es la desinformación aparentemente generalizada de que tiene algo que ver conlas expresiones lambdade Python. Las funciones creadas con una expresión lambda no tienen nada de especial y, de hecho, se comporta exactamente igual si se utiliza un
def
normal:def
create_multipliers
():
multipliers
=
[]
for
i
in
range
(
5
):
def
multiplier
(
x
):
return
i
*
x
multipliers
.
append
(
multiplier
)
return
multipliers
- Lo que debes hacer en su lugar:
-
Podría decirse que la solución más general es un pequeño truco. Debido al mencionado comportamiento de Python respecto a la evaluación de los argumentos por defecto de las funciones (ver "Argumentos por defecto mutables"), puedes crear un cierre que se vincule inmediatamente a sus argumentos utilizando un argumento por defecto:
def
create_multipliers
():
return
[
lambda
x
,
i
=
i
:
i
*
x
for
i
in
range
(
5
)]
También puedes utilizar la función
functools.partial()
:from
functools
import
partial
from
operator
import
mul
def
create_multipliers
():
return
[
partial
(
mul
,
i
)
for
i
in
range
(
5
)]
- Cuando este gotcha no es un gotcha:
-
A veces quieres que tus cierres se comporten así. La vinculación tardía es buena en muchas situaciones (por ejemplo, en el proyecto Diamante, "Ejemplo de uso de un cierre (cuando el gotcha no es un gotcha)"). El bucle para crear funciones únicas es, por desgracia, un caso en el que puede causar hipo.
Estructurar tu proyecto
Por estructura nos referimos a las decisiones que tomas sobre cómo tu proyecto cumple mejor su objetivo. El objetivo es aprovechar al máximo las características de Python para crear un código limpio y eficaz. En términos prácticos, eso significa que la lógica y las dependencias tanto en tu código como en tu estructura de archivos y carpetas están claras.
¿Qué funciones deben ir en qué módulos? ¿Cómo fluyen los datos a través del proyecto? ¿Qué características y funciones pueden agruparse y aislarse? Respondiendo a preguntas como éstas, puedes empezar a planificar, en sentido amplio, cómo será tu producto final.
El Libro de cocina de Python tiene un capítulo sobremódulos y paquetesque describe en detalle cómo funcionan las declaraciones y los paquetes de __import__
. El objetivo de esta sección es esbozar aspectos de los sistemas de módulos e importaciones de Python que son fundamentales para reforzar la estructura de tu proyecto. A continuación, discutiremos diversas perspectivas sobre cómo construir código que pueda ampliarse y probarse de forma fiable.
Gracias a la forma en que se gestionan las importaciones y los módulos en Python, es relativamente fácil estructurar un proyecto Python: hay pocas restricciones y el modelo de importación de módulos es fácil de comprender. Por tanto, sólo te queda la tarea puramente arquitectónica de diseñar las distintas partes de tu proyecto y sus interacciones.
Módulos
Los módulos son una de las principales capas de abstracción de Python, y probablemente la más natural. Las capas de abstracción permiten a un programador separar el código en partes que contienen datos y funcionalidades relacionados.
Por ejemplo, si una capa de un proyecto se ocupa de la interfaz con las acciones del usuario, mientras que otra se ocupa de la manipulación de datos a bajo nivel, la forma más natural de separar estas dos capas es reagrupar toda la funcionalidad de interfaz en un archivo, y todas las operaciones de bajo nivel en otro archivo. Esta agrupación las coloca en dos módulos separados. El archivo de interfaz importaría entonces el archivo de bajo nivel con la propiedadimport module
o from module import attribute
.
En cuanto utilizas sentencias import
, también utilizas módulos. Éstos pueden ser módulos incorporados (como os
y sys
), paquetes de terceros que tengas instalados en tu entorno (como Requests o NumPy), o módulos internos de tu proyecto. El código siguiente muestra algunos ejemplos de sentencias import
y confirma que un módulo importado es un objeto Python con su propio tipo de datos:
>>>
import
sys
# built-in module
>>>
import
matplotlib.pyplot
as
plt
# third-party module
>>>
>>>
import
mymodule
as
mod
# internal project module
>>>
>>>
(
type
(
sys
),
type
(
plt
),
type
(
mod
))
<
class
'
module
'> <class '
module
'> <class '
module
'>
Para mantenerte en línea con la guía de estilo, mantén los nombres de los módulos cortos y en minúsculas. Y asegúrate de evitar el uso de símbolos especiales como el punto (.) o el signo de interrogación (?), que interferirían con la forma en que Python busca los módulos. Así que un nombre de archivo como mi.spam.py8 es uno de los que debes evitar; Python esperaría encontrar un archivo spam.py en una carpeta llamada my
, que no es el caso. Ladocumentación de Pythonofrece más detalles sobre el uso de la notación por puntos.
Importar módulos
Aparte de algunas restricciones de nombres, no se requiere nada especial para utilizar un archivo de Python como módulo, pero ayuda a comprender el mecanismo de importación. En primer lugar, la sentencia import modu
buscará la definición de modu
en un archivo llamadomodu.py en el mismo directorio que el llamante, si existe un archivo con ese nombre.
Si no se encuentra, el intérprete de Python buscará recursivamente modu.py enlaruta debúsqueda de Pythony lanzará una excepción ImportError
si no se encuentra. El valor de la ruta de búsqueda depende de la plataforma e incluye cualquier directorio definido por el usuario o por el sistema en el entorno $PYTHONPATH
(o %PYTHONPATH%
en Windows). Puede manipularse o inspeccionarse en una sesión de Python:
import
sys
>>>
sys
.
path
[
''
,
'/current/absolute/path'
,
'etc'
]
# The actual list contains every path that is searched
# when you import libraries into Python, in the order
# that they'll be searched.
Una vez encontrado modu.py, el intérprete de Python ejecutará el módulo en un ámbito aislado. Se ejecutará cualquier sentencia de nivel superior de modu. py, incluidas otras importaciones, si existen. Las definiciones de funciones y clases se almacenan en el diccionario del módulo.
Por último, las variables, funciones y clases del módulo estarán a disposición de quien lo llame a través del espacio de nombres del módulo, un concepto central en programación que resulta especialmente útil y potente en Python. Los espacios de nombres proporcionan un ámbito que contiene atributos con nombre que son visibles entre sí, pero a los que no se puede acceder directamente fuera del espacio de nombres.
En muchos lenguajes, una directiva de inclusión de archivos hace que el preprocesador copie el contenido del archivo incluido en el código de la persona que llama. En Python es distinto: el código incluido se aísla en un espacio de nombres de módulo. El resultado de la sentencia import modu
será un objeto de módulo llamado modu
en el espacio de nombres global, con los atributos definidos en el módulo accesibles mediante la notación de puntos:modu.sqrt
sería el objeto sqrt
definido dentro de modu.py, por ejemplo. Esto significa que, por lo general, no tienes que preocuparte de que el código incluido pueda tener efectos no deseados -por ejemplo, anular una función existente con el mismo nombre.
Es posible simular el comportamiento más estándar utilizando una sintaxis especial de la sentencia import
: from modu import *
. Sin embargo, esto se considera generalmente una mala práctica: utilizar import *
dificulta la lectura del código, hace que las dependencias estén menos compartimentadas y puede emborronar (sobrescribir) los objetos definidos existentes con las nuevas definiciones dentro del módulo importado.
Utilizar from modu import func
es una forma de importar sólo el atributo que quieras en el espacio de nombres global. Aunque es mucho menos perjudicial que from modu import *
porque muestra explícitamente lo que se importa en el espacio de nombres global. Su única ventaja sobre un import modu
más sencillo es que te ahorrará un poco de mecanografía.
La Tabla 4-1 compara las distintas formas de importar definiciones de otros módulos.
Muy mal (confuso para un lector) |
Mejor (obvia qué nuevos nombres están en el espacio de nombres global) |
Mejor (inmediatamente obvio de dónde el atributo) |
---|---|---|
|
|
|
¿Es |
¿Se ha modificado o redefinido |
Ahora |
Como se menciona en "Estilo del código", la legibilidad es una de las principales características de Python. El código legible evita el texto repetitivo inútil y el desorden. Pero la prolijidad y la oscuridad son los límites donde debe detenerse la brevedad. Indicar explícitamente de dónde procede una clase o función, como en el lenguaje modu.func()
, mejora enormemente la legibilidad y comprensibilidad del código en todos los proyectos, salvo en los más sencillos de un solo archivo.
Paquetes
Python proporciona un sistema de empaquetado muy sencillo, que extiende el mecanismo de módulos a un directorio.
Cualquier directorio con un archivo __init__ . py se considera un paquete Python. El directorio de nivel superior con un __init__. py es el paquete raíz.9 Los distintos módulos del paquete se importan de forma similar a los módulos normales, pero con un comportamiento especial para el archivo __init__.py, que se utiliza para reunir todas las definiciones del paquete.
Un archivo modu. py en el directorio pack/ se importa con la sentencia import pack.modu
. El intérprete buscará un archivo__init__.py en pack
y ejecutará todas sus sentencias de nivel superior. A continuación, buscará un archivo llamado pack/modu.py y ejecutará todas sus sentencias de nivel superior. Tras estas operaciones, cualquier variable, función o clase definida enmodu. py estará disponible en el espacio de nombres pack.modu
.
Un problema frecuente es el exceso de código en los archivos __init__. py. Cuando crece la complejidad del proyecto, puede haber subpaquetes y sub-subpaquetes en una estructura de directorios profunda. En este caso, importar un único elemento de un subpaquete requerirá ejecutar todos los archivos __init__. py encontrados al recorrer el árbol.
Es normal, incluso una buena práctica, dejar un__init__ .py vacío cuando los módulos y subpaquetes del paquete no necesitan compartir ningún código: los proyectos HowDoI y Diamond, que se utilizan como ejemplo en la siguiente sección, no tienen ningún código excepto los números de versión en sus archivos__init__.py. Los proyectos Tablib, Peticiones y Flask contienen una cadena de documentación de nivel superior y declaraciones import
que exponen la API prevista para cada proyecto, y el proyecto Werkzeug también expone su API de nivel superior, pero lo hace utilizando lazy loading (código adicional que sólo añade contenido al espacio de nombres a medida que se utiliza, lo que acelera la declaración inicial import
).
Por último, existe una cómoda sintaxis para importar paquetes profundamente anidados: import very.deep.module as mod
. Esto te permite utilizar mod
en lugar de la repetición verbosa de very.deep.module
.
Programación Orientada a Objetos
A veces se describe Python como un lenguaje de programación orientado a objetos, lo cual puede resultar algo engañoso y debe aclararse.
En Python, todo es un objeto, y puede manejarse como tal. A esto nos referimos cuando decimos que las funciones son objetos de primera clase. Las funciones, las clases, las cadenas e incluso los tipos son objetos en Python: todos tienen un tipo, pueden pasarse como argumentos de función y pueden tener métodos y propiedades. En este sentido, Python es un lenguaje orientado a objetos.
Sin embargo, a diferencia de Java, Python no impone la programación orientada a objetos como paradigma principal de programación. Es perfectamente viable que un proyecto Python no esté orientado a objetos, es decir, que no utilice (o utilice muy pocas) definiciones de clases, herencia de clases o cualquier otro mecanismo propio de la programación orientada a objetos. Estas características están disponibles, pero no son obligatorias, para nosotros los Pythonistas. Además, como se vio en "Módulos", la forma en que Python maneja los módulos y los espacios de nombres proporciona al desarrollador una forma natural de garantizar la encapsulación y la separación de las capas de abstracción -las razones más comunes para utilizar la orientación a objetos- sin clases.
Los defensores de la programación funcional (un paradigma que, en su forma más pura, no tiene operador de asignación, ni efectos secundarios, y básicamente encadena funciones para realizar tareas), dicen que los errores y la confusión se producen cuando una función hace cosas diferentes dependiendo del estado externo del sistema -por ejemplo, una variable global que indica si una persona ha iniciado sesión o no-. Python, aunque no es un lenguaje puramente funcional, tiene herramientas que hacen posible la programación funcional,y entonces podemos restringir nuestro uso de clases personalizadas a situaciones en las que queramos pegar un estado y una funcionalidad.
En algunas arquitecturas, normalmente aplicaciones web, se generan múltiples instancias de procesos Python para responder a peticiones externas que pueden producirse al mismo tiempo. En este caso, mantener cierto estado en los objetos instanciados, lo que significa conservar cierta información estática sobre el mundo, es propenso alas condiciones de carrera, término utilizado para describir la situación en la que, en algún momento entre la inicialización del estado de un objeto (normalmente realizada con el método Class.__init__()
en Python) y el uso real del estado del objeto a través de uno de sus métodos, el estado del mundo ha cambiado.
Por ejemplo, una petición puede cargar un artículo en memoria y marcarlo posteriormente como añadido al carrito de la compra de un usuario. Si otra petición vende el artículo a otra persona al mismo tiempo, puede ocurrir que la venta se produzca realmente después de que la primera sesión cargara el artículo, y entonces estemos intentando vender inventario ya marcado como vendido. Éste y otros problemas llevaron a preferir las funciones sin estado.
Nuestra recomendación es la siguiente: cuando trabajes con código que dependa de algún contexto persistente o estado global (como la mayoría de las aplicaciones web), utiliza funciones y procedimientos con el menor número posible de contextos implícitos y efectos secundarios. El contexto implícito de una función está formado por cualquiera de las variables globales o elementos de la capa de persistencia a los que se accede desde dentro de la función.Los efectos secundariosson los cambios que una función realiza en su contexto implícito. Si una función guarda o borra datos en una variable global o en la capa de persistencia, se dice que tiene un efecto secundario.
Las clases personalizadas en Python deben utilizarse para aislar cuidadosamente las funciones con contexto y efectos secundarios de las funciones con lógica (llamadas funciones puras).Las funciones puras son deterministas: dada una entrada fija, la salida siempre será la misma. Esto se debe a que no dependen del contexto y no tienen efectos secundarios. La función print()
, por ejemplo, es impura porque no devuelve nada, pero escribe en la salida estándar como efecto secundario. He aquí algunas ventajas de tener funciones puras y separadas:
-
Las funciones puras son mucho más fáciles de cambiar o sustituir si hay que refactorizarlas u optimizarlas.
-
Las funciones puras son más fáciles de probar con pruebas unitarias, hay menos necesidad de configurar contextos complejos y de limpiar los datos después.
-
Las funciones puras son más fáciles de manipular, decorar (más sobre decoradores en un momento) y pasar de un lado a otro.
En resumen, para algunas arquitecturas, las funciones puras son bloques de construcción más eficientes que las clases y los objetos, porque no tienen contexto ni efectos secundarios. Como ejemplo, las funciones de E/S relacionadas con cada uno de los formatos de archivo de la biblioteca Tablib(tablib/formats/*.py-veremosTablib en el próximo capítulo) son funciones puras, y no forman parte de una clase, porque todo lo que hacen es leer datos en un objeto Dataset
separado que persiste los datos, o escribir elDataset
en un archivo. Pero el objeto Session
de la biblioteca Solicitudes (que también veremos en el próximo capítulo) es una clase, porque tiene que persistir la información de cookies y autenticación que puede intercambiarse en una sesión HTTP.
Nota
La orientación a objetos es útil e incluso necesaria en muchos casos; por ejemplo, cuando se desarrollan aplicaciones gráficas de escritorio o juegos, en los que las cosas que se manipulan (ventanas, botones, avatares, vehículos) tienen una vida propia relativamente larga en la memoria del ordenador. Éste es también uno de los motivos que subyacen al mapeo objeto-relacional, que mapea filas en las bases de datos a objetos en el código, del que se habla más adelante en "Bibliotecas de bases de datos".
Decoradores
Los decoradores se añadieron a Python en la versión 2.4 y se definen y discuten en la PEP 318. Un decorador es una función o un método de clase que envuelve (o decora) otra función o método. La función o método decorado sustituirá a la función o método original. Como las funciones son objetos de primera clase en Python, esto se puede hacer manualmente, pero utilizar la sintaxis @decorator
es más claro y preferible. He aquí un ejemplo de cómo utilizar un decorador:
>>>
def
foo
():
...
(
"I am inside foo."
)
...
...
...
>>>
import
logging
>>>
logging
.
basicConfig
()
>>>
>>>
def
logged
(
func
,
*
args
,
**
kwargs
):
...
logger
=
logging
.
getLogger
()
...
def
new_func
(
*
args
,
**
kwargs
):
...
logger
.
debug
(
"calling {} with args {} and kwargs {}"
.
format
(
...
func
.
__name__
,
args
,
kwargs
))
...
return
func
(
*
args
,
**
kwargs
)
...
return
new_func
...
>>>
>>>
...
@logged
...
def
bar
():
...
(
"I am inside bar."
)
...
>>>
logging
.
getLogger
()
.
setLevel
(
logging
.
DEBUG
)
>>>
bar
()
DEBUG
:
root
:
calling
bar
with
args
()
and
kwargs
{}
I
am
inside
bar
.
>>>
foo
()
I
am
inside
foo
.
Este mecanismo es útil para aislar la lógica central de la función o método. Un buen ejemplo de una tarea que se maneja mejor con decoradores es la memoización o almacenamiento en caché: quieres almacenar los resultados de una función costosa en una tabla y utilizarlos directamente en lugar de volver a calcularlos cuando ya se han calculado. Evidentemente, esto no forma parte de la lógica de la función. A partir de la PEP 3129, en Python 3, los decoradores también pueden aplicarse a las clases.
Tipificación dinámica
Python es de tipado dinámico (a diferencia del tipado estático), lo que significa que las variables no tienen un tipo fijo. Las variables se implementan como punteros a un objeto, lo que hace posible que la variable a
se establezca con el valor 42, luego con el valor "gracias por todo el pescado", y luego con una función.
El tipado dinámico utilizado en Python se considera a menudo un punto débil, porque puede dar lugar a complejidades y a código difícil de depurar: si algo llamado a
puede establecerse a muchas cosas diferentes, el desarrollador o el mantenedor deben rastrear este nombre en el código para asegurarse de que no se ha establecido a un objeto completamente no relacionado.La Tabla 4-2 ilustra las buenas y malas prácticas al utilizar nombres.
Consejo | Mal | Bien |
---|---|---|
Utiliza funciones o métodos cortos para reducir el riesgo de utilizar el mismo nombre para dos cosas no relacionadas. |
|
|
Utiliza nombres diferentes para los elementos relacionados cuando tengan un tipo diferente. |
|
|
Al reutilizar nombres no se gana en eficiencia: la asignación seguirá creando un nuevo objeto. Y cuando la complejidad crece y cada asignación está separada por otras líneas de código, incluidas ramas y bucles, se hace más difícil determinar el tipo de una variable determinada.
Algunas prácticas de codificación, como la programación funcional, recomiendan no reasignar variables. En Java, se puede forzar que una variable contenga siempre el mismo valor después de la asignación utilizando la palabra clave final. Python no tiene una palabra clave final, y sería contrario a su filosofía. Pero asignar una varible sólo una vez puede ser una buena disciplina; ayuda a reforzar el concepto de tipos mutables frente a inmutables.
Consejo
Pylintte avisará si reasignas una variable a dos tipos diferentes.
Tipos mutables e inmutables
Python tiene dos tipos de tipos incorporados o definidos por el usuario10 definidos por el usuario:
# Lists are mutable
my_list
=
[
1
,
2
,
3
]
my_list
[
0
]
=
4
my_list
# [4, 2, 3] <- The same list, changed.
# Integers are immutable
x
=
6
x
=
x
+
1
# The new x occupies a different location in memory.
- Tipos mutables
-
Permiten modificar in situ el contenido del objeto. Ejemplos de ello son las listas y los diccionarios, que tienen métodos de mutación como
list.append()
odict.pop()
y pueden modificarse in situ. - Tipos inmutables
-
Estos tipos no proporcionan ningún método para cambiar su contenido. Por ejemplo, la variable
x
establecida en el entero 6 no tiene ningún método de "incremento". Para calcularx + 1
, tienes que crear otro entero y darle un nombre.
Una consecuencia de esta diferencia de comportamiento es que los tipos mutables no pueden utilizarse como claves de diccionario, porque si el valor cambia alguna vez, no hará hash al mismo valor, y los diccionarios utilizan hashing11 para almacenar claves. El equivalente inmutable de una lista es la tupla, creada con paréntesis; por ejemplo, (1, 2)
. No puede modificarse en su lugar y, por tanto, puede utilizarse como clave de diccionario.
Utilizar adecuadamente tipos mutables para objetos que se pretende que sean mutables (por ejemplo, my_list = [1, 2, 3]
) y tipos inmutables para objetos que se pretende que tengan un valor fijo (por ejemplo, islington_phone = ("220", "7946", "0347")
) aclara la intención del código para otros desarrolladores.
Una peculiaridad de Python que puede sorprender a los recién llegados es que las cadenas son inmutables; intentar cambiar una dará un error de tipo:
>>>
s
=
"I'm not mutable"
>>>
s
[
1
:
7
]
=
" am"
Traceback
(
most
recent
call
last
):
File
"<stdin>"
,
line
1
,
in
<
module
>
TypeError
:
'str'
object
does
not
support
item
assignment
Esto significa que, al construir una cadena a partir de sus partes, es mucho más eficaz acumular las partes en una lista, porque es mutable, y luego unir las partes para formar la cadena completa.Además, una comprensión de lista de Python, que es una sintaxis abreviada para iterar sobre una entrada para crear una lista, es mejor y más rápida que construir una lista a partir de llamadas a append()
dentro de un bucle.La Tabla 4-3 muestra distintas formas de crear una cadena a partir de un iterable.
Mal | Bien | Mejor |
---|---|---|
|
|
|
La página principal de Python tiene unbuen debate sobre este tipo de optimización.
Por último, si se conoce el número de elementos de una concatenación, la suma pura de cadenas es más rápida (y directa) que crear una lista de elementos sólo para hacer una "".join()
. Todas las opciones de formato siguientes para definir cheese
hacen lo mismo :12
>>>
adj
=
"Red"
>>>
noun
=
"Leicester"
>>>
>>>
cheese
=
"
%s
%s
"
%
(
adj
,
noun
)
# This style was deprecated (PEP 3101)
>>>
cheese
=
"{} {}"
.
format
(
adj
,
noun
)
# Possible since Python 3.1
>>>
cheese
=
"{0} {1}"
.
format
(
adj
,
noun
)
# Numbers can also be reused
>>>
cheese
=
"{adj} {noun}"
.
format
(
adj
=
adj
,
noun
=
noun
)
# This style is best
>>>
(
cheese
)
Red
Leicester
Venderizar dependencias
Un paquete que vendoriza dependenciasincluye dependencias externas (bibliotecas de terceros) dentro de su código fuente, a menudo dentro de una carpeta llamada vendor, o paquetes. Hayuna entrada de blog muy buena sobre el tema que enumera las principales razones por las que el propietario de un paquete puede hacer esto (básicamente, para evitar diversos problemas de dependencia), y analiza alternativas.
El consenso es que, en casi todos los casos, es mejor mantener la dependencia separada, ya que añade contenido innecesario (a menudo megabytes de código adicional) al repositorio; los entornos virtuales utilizados en combinación con setup. py (preferible, especialmente cuando tu paquete es una biblioteca) o un requirements.txt (que, cuando se utiliza, anulará las dependencias ensetup.py en caso de conflicto) pueden restringir las dependencias a un conjunto conocido de versiones de trabajo.
Si esas opciones no son suficientes, puede ser útil ponerse en contacto con el propietario de la dependencia para que tal vez resuelva el problema actualizando su paquete (p. ej, tu biblioteca puede depender de una próxima versión de su paquete, o puede necesitar que se añada una nueva función específica), ya que esos cambios probablemente beneficiarían a toda la comunidad. La advertencia es que, si envías pull requests para grandes cambios, es posible que se espere que mantengas esos cambios cuando lleguen más sugerencias y peticiones; por esta razón, tanto Tablib como Requests venden al menos algunas dependencias. A medida que la comunidad avance hacia la adopción completa de Python 3, es de esperar que queden menos problemas de los más acuciantes.
Probar tu código
Probar tu código es muy importante. Es mucho más probable que la gente utilice un proyecto que realmente funciona.
Python incluyó por primera vez doctest
y unittest
en Python 2.1, publicado en 2001, adoptando desarrollo dirigido por pruebas (TDD), en el que el desarrollador escribe primero pruebas que definen el funcionamiento principal y los casos de perímetro de una función, y luego escribe la función para que supere esas pruebas. Desde entonces, el TDD se ha aceptado y adoptado ampliamente en las empresas y en los proyectos de código abierto: es una buena idea practicar escribiendo en paralelo el código de pruebas y el código en ejecución. Utilizado sabiamente, este método te ayuda a definir con precisión la intención de tu código y a tener una arquitectura más modular.
Consejos para las pruebas
Una prueba es el código más útil que puede escribir un autoestopista. Hemos resumido aquí algunos de nuestros consejos.
Sólo una cosa por prueba
Una unidad de pruebas debe centrarse en una pequeña parte de la funcionalidad y demostrar que es correcta.
La independencia es imprescindible
Cada unidad de prueba debe ser totalmente independiente: capaz de ejecutarse por sí sola, y también dentro del conjunto de pruebas, independientemente del orden en que sean llamadas. La implicación de esta regla es que cada prueba debe cargarse con un conjunto de datos nuevo y puede tener que hacer alguna limpieza posterior. De esto se encargan normalmente los métodos setUp()
y tearDown()
.
La precisión es mejor que la parsimonia
Utiliza nombres largos y descriptivos para las funciones de prueba. Esta pauta es ligeramente diferente a la del código en ejecución, donde suelen preferirse los nombres cortos. La razón es que las funciones de prueba nunca se llaman explícitamente.square()
o incluso sqr()
están bien en el código en ejecución, pero en el código de prueba, deberías tener nombres como test_square_of_number_2()
otest_square_negative_number()
. Estos nombres de función se muestran cuando falla una prueba y deben ser lo más descriptivos posible.
La velocidad cuenta
Esfuérzate por hacer pruebas que sean rápidas. Si una prueba necesita más de unos milisegundos para ejecutarse, el desarrollo se ralentizará, o las pruebas no se ejecutarán con la frecuencia deseable. En algunos casos, las pruebas no pueden ser rápidas porque necesitan una estructura de datos compleja sobre la que trabajar, y esta estructura de datos debe cargarse cada vez que se ejecuta la prueba. Mantén estas pruebas más pesadas en un conjunto de pruebas separado que se ejecute mediante alguna tarea programada, y ejecuta todas las demás pruebas con la frecuencia necesaria.
RTMF (¡Lee el manual, amigo!)
Conoce tus herramientas y aprende a ejecutar una sola prueba o un caso de prueba. Luego, cuando desarrolles una función dentro de un módulo, ejecuta a menudo las pruebas de esta función, idealmente de forma automática cuando guardes el código.
Pruébalo todo cuando empieces-y de nuevo cuando termines
Ejecuta siempre el conjunto completo de pruebas antes de una sesión de codificación, y ejecútalo de nuevo después. Esto te dará más seguridad de que no has roto nada en el resto del código.
Los ganchos de automatización del control de versiones son fantásticos
Es una buena idea implementar un gancho que ejecute todas las pruebas antes de enviar el código a un repositorio compartido. Puedes añadir directamente ganchos a tu sistema de control de versiones, y algunos IDE proporcionan formas de hacerlo de forma más sencilla en sus propios entornos. Aquí tienes los enlaces a la documentación de los sistemas más populares, que te explicarán paso a paso cómo hacerlo:
Escribe una prueba de ruptura si quieres tomarte un descanso
Si estás en medio de una sesión de desarrollo y tienes que interrumpir tu trabajo, es una buena idea escribir una prueba unitaria rota sobre lo que quieres desarrollar a continuación. Cuando vuelvas al trabajo, tendrás un indicador de dónde estabas y podrás retomar el camino más rápidamente.
Ante la ambigüedad, depura utilizando una prueba
El primer paso cuando estás depurando tu código es escribir una nueva prueba que localice el fallo. Aunque no siempre es posible hacerlo, esas pruebas de detección de fallos son de las piezas de código más valiosas de tu proyecto.
Si la prueba es difícil de explicar, buena suerte encontrando colaboradores
Cuando algo va mal o hay que cambiarlo, si tu código tiene un buen conjunto de pruebas, tú u otros mantenedores confiaréis en gran medida en el conjunto de pruebas para solucionar el problema o modificar un comportamiento determinado. Por tanto, el código de pruebas se leerá tanto -o incluso más- que el código en ejecución. Una prueba unitaria cuyo propósito no esté claro no es muy útil en este caso.
Si la prueba es fácil de explicar, casi siempre es una buena idea
Otro uso del código de pruebas es como introducción para los nuevos desarrolladores. Cuando otras personas van a tener que trabajar en la base de código, ejecutar y leer el código de pruebas relacionado suele ser lo mejor que pueden hacer. Descubrirán (o deberían descubrir) los puntos calientes, donde surgen la mayoría de las dificultades, y los casos de esquina. Si tienen que añadir alguna funcionalidad, el primer paso debería ser añadir una prueba y, por este medio, asegurarse de que la nueva funcionalidad no es ya una vía de trabajo que no se ha conectado a la interfaz.
Sobre todo, que no cunda el pánico
¡Es código abierto! El mundo entero te cubre las espaldas.
Pruebas básicas
Esta sección enumera los aspectos básicos de las pruebas -para que te hagas una idea de las opciones disponibles- y da algunos ejemplos tomados de los proyectos de Python en los que nos sumergiremos a continuación, en el Capítulo 5. Hay un libro entero sobre TDD en Python, y no queremos reescribirlo. Echa un vistazo a Test-Driven Development with Python (O'Reilly) (¡obedece a la cabra de las pruebas!).
unittest
unittest
es el módulo de pruebas incluido en la biblioteca estándar de Python. Su API resultará familiar a cualquiera que haya utilizado alguna de las series de herramientas JUnit (Java)/nUnit (.NET)/CppUnit (C/C++).
La creación de casos de prueba se consigue subclasificando unittest.TestCase
. En este código de ejemplo, la función de prueba se define simplemente como un nuevo método en MyTest
:
# test_example.py
import
unittest
def
fun
(
x
):
return
x
+
1
class
MyTest
(
unittest
.
TestCase
):
def
test_that_fun_adds_one
(
self
):
self
.
assertEqual
(
fun
(
3
),
4
)
class
MySecondTest
(
unittest
.
TestCase
):
def
test_that_fun_fails_when_not_adding_number
(
self
):
self
.
assertRaises
(
TypeError
,
fun
,
"multiply six by nine"
)
Nota
Los métodos de prueba deben empezar por la cadena test
o no se ejecutarán. Se espera que los módulos de prueba (archivos) coincidan con el patrón test*.py
por defecto, pero pueden coincidir con cualquier patrón dado al argumento de la palabra clave --pattern
en la línea de comandos.
Para ejecutar todas las pruebas en ese TestClass
, abre un intérprete de comandos de terminal; y en el mismo directorio que el archivo, invoca el módulo unittest
de Python en la línea de comandos, de la siguiente manera:
$
python -m unittest test_example.MyTest . ---------------------------------------------------------------------- Ran1
test
in 0.000s OK
O para ejecutar todas las pruebas en un archivo, dale un nombre al archivo:
$
python -m unittest test_example . ---------------------------------------------------------------------- Ran2
tests in 0.000s OK
Simulacro (en unittest)
A partir de Python 3.3,unittest.mock
está disponible en la biblioteca estándar. Te permite sustituir partes de tu sistema bajo prueba por objetos simulados y hacer afirmaciones sobre cómo se han utilizado.
Por ejemplo, puedes parchear un método como en el siguiente ejemplo (un parche es un código que modifica o sustituye a otro código existente en tiempo de ejecución). En este código, el método existente llamadoProductionClass.method
, para la instancia que creamos llamada instance
, se sustituye por un nuevo objeto, MagicMock
, que siempre devolverá el valor 3
cuando sea llamado, y que cuenta el número de llamadas a métodos que recibe, registra la firma con la que fue llamado y contiene métodos de aserción para realizar pruebas:
from
unittest.mock
import
MagicMock
instance
=
ProductionClass
()
instance
.
method
=
MagicMock
(
return_value
=
3
)
instance
.
method
(
3
,
4
,
5
,
key
=
'value'
)
instance
.
method
.
assert_called_with
(
3
,
4
,
5
,
key
=
'value'
)
Para burlarse de clases u objetos de un módulo bajo prueba, utiliza el decorador patch
. En el siguiente ejemplo, se sustituye un sistema de búsqueda externo por un simulacro que siempre devuelve el mismo resultado (tal y como se utiliza en este ejemplo, el parche es sólo para la duración de la prueba):
import
unittest.mock
as
mock
def
mock_search
(
self
):
class
MockSearchQuerySet
(
SearchQuerySet
):
def
__iter__
(
self
):
return
iter
([
"foo"
,
"bar"
,
"baz"
])
return
MockSearchQuerySet
()
# SearchForm here refers to the imported class reference
# myapp.SearchForm, and modifies this instance, not the
# code where the SearchForm class itself is initially
# defined.
@mock.patch
(
'myapp.SearchForm.search'
,
mock_search
)
def
test_new_watchlist_activities
(
self
):
# get_search_results runs a search and iterates over the result
self
.
assertEqual
(
len
(
myapp
.
get_search_results
(
q
=
"fish"
)),
3
)
Mock tiene muchas otras formas de configurarlo y controlar su comportamiento, que se detallan en la documentación de Python paraunittest.mock
.
doctest
El módulo doctest busca trozos de texto que parezcan sesiones interactivas de Python en docstrings, y luego ejecuta esas sesiones para verificar que funcionan exactamente como se muestra.
Las pruebas documentales tienen una finalidad distinta a las pruebas unitarias propiamente dichas. Suelen ser menos detallados y no detectan casos especiales ni oscuros errores de regresión, sino que son útiles como documentación expresiva de los principales casos de uso de un módulo y sus componentes (un ejemplo decamino feliz). Sin embargo, los doctests deben ejecutarse automáticamente cada vez que se ejecute el conjunto completo de pruebas.
Aquí tienes un sencillo doctest en una función:
def
square
(
x
):
"""Squares x.
>>> square(2)
4
>>> square(-2)
4
"""
return
x
*
x
if
__name__
==
'__main__'
:
import
doctest
doctest
.
testmod
()
Cuando ejecutes este módulo desde la línea de comandos (es decir, python module.py
), los doctests se ejecutarán y se quejarán si algo no se comporta como se describe en los docstrings.
Ejemplos
En esta sección, tomaremos extractos de nuestros paquetes favoritos para destacar las buenas prácticas de pruebas utilizando código real. Los conjuntos de pruebas requieren bibliotecas adicionales no incluidas en los paquetes (por ejemplo, Requests utiliza Flask para simular un servidor HTTP) que se incluyen en el archivo requirements.txt de sus proyectos.
Para todos estos ejemplos, los primeros pasos esperados son abrir un intérprete de comandos de terminal, cambiar de directorio a un lugar donde trabajes en proyectos de código abierto, clonar el repositorio de fuentes y configurar un entorno virtual, como éste:
$
git clone https://github.com/username/projectname.git$
cd
projectname$
virtualenv -p python3 venv$
source
venv/bin/activate(
venv)
$
pip install -r requirements.txt
Ejemplo: Pruebas en Tablib
Tablib utiliza el módulo unittest
de la biblioteca estándar de Python para sus pruebas. El conjunto de pruebas no viene con el paquete; debes clonar el repositorio de GitHub para obtener los archivos. Aquí tienes un extracto, con las partes importantes anotadas:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Tests for Tablib."""
import
json
import
unittest
import
sys
import
os
import
tablib
from
tablib.compat
import
markup
,
unicode
,
is_py3
from
tablib.core
import
Row
class
TablibTestCase
(
unittest
.
TestCase
)
:
"""Tablib test cases."""
def
setUp
(
self
)
:
"""Create simple data set with headers."""
global
data
,
book
data
=
tablib
.
Dataset
(
)
book
=
tablib
.
Databook
(
)
#
# ... skip additional setup not used here ...
#
def
tearDown
(
self
)
:
"""Teardown."""
pass
def
test_empty_append
(
self
)
:
"""Verify append() correctly adds tuple with no headers."""
new_row
=
(
1
,
2
,
3
)
data
.
append
(
new_row
)
# Verify width/data
self
.
assertTrue
(
data
.
width
==
len
(
new_row
)
)
self
.
assertTrue
(
data
[
0
]
==
new_row
)
def
test_empty_append_with_headers
(
self
)
:
"""Verify append() correctly detects mismatch of number of headers and data. """
data
.
headers
=
[
'
first
'
,
'
second
'
]
new_row
=
(
1
,
2
,
3
,
4
)
self
.
assertRaises
(
tablib
.
InvalidDimensions
,
data
.
append
,
new_row
)
Para utilizar
unittest
, subclaseunittest.TestCase
, y escribe métodos de prueba cuyos nombres empiecen portest
.TestCase
proporciona métodos assert que comprueban la igualdad, la verdad, el tipo de datos, la pertenencia a un conjunto y si se producen excepciones; consultala documentación para obtener más detalles.TestCase.setUp()
se ejecuta antes de cada método de prueba enTestCase
.TestCase.tearDown()
se ejecuta después de cada método de prueba enTestCase
.13Todos los métodos de prueba deben empezar por
test
, o no se ejecutarán.Puede haber varias pruebas dentro de un mismo
TestCase
, pero cada una debe probar una sola cosa.
Si estuvieras contribuyendo a Tablib, lo primero que harías después de clonarlo es ejecutar el conjunto de pruebas y confirmar que no se rompe nada. Así:
(
venv)
$
### inside the top-level directory, tablib/
(
venv)
$
python -m unittest test_tablib.py .............................................................. ---------------------------------------------------------------------- Ran62
tests in 0.289s OK
A partir de Python 2.7, unittest
también incluye sus propios mecanismos de descubrimiento de pruebas, utilizando la opción discover
en la línea de comandos:
(
venv)
$
### *above* the top-level directory, tablib/
(
venv)
$
python -m unittest discover tablib/ .............................................................. ---------------------------------------------------------------------- Ran62
tests in 0.234s OK
Después de confirmar que todas las pruebas pasan, (a) busca el caso de prueba relacionado con la parte que estás cambiando y ejecútalo a menudo mientras modificas el código, o (b) escribe un nuevo caso de prueba para la función que estás añadiendo o el error que estás localizando y ejecútalo a menudo mientras modificas el código. El siguiente fragmento es un ejemplo:
(
venv)
$
### inside the top-level directory, tablib/
(
venv)
$
python -m unittest test_tablib.TablibTestCase.test_empty_append . ---------------------------------------------------------------------- Ran1
test
in 0.001s OK
Una vez que tu código funcione, volverías a ejecutar todo el conjunto de pruebas antes de enviarlo al repositorio. Dado que ejecutas estas pruebas tan a menudo, tiene sentido que sean lo más rápidas posible. Hay muchos más detalles sobre el uso de unittest enla documentación de unittest de la biblioteca estándar.
Ejemplo: Pruebas en las peticiones
Requests utiliza py.test
. Para verlo en acción, abre un intérprete de comandos de terminal, cambia a un directorio temporal, clona Solicitudes, instala las dependencias y ejecuta py.test
, como se muestra aquí:
$
git clone -q https://github.com/kennethreitz/requests.git$
$
virtualenv venv -q -p python3# dash -q for 'quiet'
$
source
venv/bin/activate(
venv)
$
(
venv)
$
pip install -q -r requests/requirements.txt# 'quiet' again...
(
venv)
$
cd
requests(
venv)
$
py.test=========================
test
sessionstarts
=================================
platform darwin -- Python 3.4.3, pytest-2.8.1, py-1.4.30, pluggy-0.3.1 rootdir: /tmp/requests, inifile: plugins: cov-2.1.0, httpbin-0.0.7 collected219
items tests/test_requests.py ........................................................ X............................................ tests/test_utils.py ..s....................................................=========
217
passed,1
skipped,1
xpassed in 25.75seconds
===================
Otras herramientas populares
Las herramientas de comprobación que se enumeran aquí se utilizan con menos frecuencia, pero siguen siendo lo suficientemente populares como para mencionarlas.
pytest
pytest es una alternativa sin boilerplate al módulo unittest estándar de Python, lo que significa que no requiere el andamiaje de clases de prueba, y quizá ni siquiera métodos de configuración y desmontaje. Para instalarlo, utiliza pip
como de costumbre:
$
pip install pytest
A pesar de ser una herramienta de pruebas totalmente equipada y extensible, presume de una sintaxis sencilla. Crear un conjunto de pruebas es tan fácil como escribir un módulo con un par de funciones:
# content of test_sample.py
def
func
(
x
):
return
x
+
1
def
test_answer
():
assert
func
(
3
)
==
5
y luego ejecutar el comando py.test
es mucho menos trabajo del que se necesitaría para la funcionalidad equivalente con el módulo unittest
:
$
py.test===========================
test
sessionstarts
============================
platform darwin -- Python 2.7.1 -- pytest-2.2.1 collecting ... collected1
items test_sample.pyF
=================================
FAILURES
=================================
_______________________________ test_answer ________________________________ def test_answer()
: > assert func(
3)
==
5 E assert4
==
5 E + where4
=
func(
3)
test_sample.py:5:AssertionError
=========================
1
failed in 0.02seconds
=========================
Nariz
Lanarizse extiende unittest
para facilitar las pruebas:
$
pip install nose
Nose proporciona descubrimiento automático de pruebas para ahorrarte la molestia de crear manualmente conjuntos de pruebas. También proporciona numerosos complementos para funciones como la salida de pruebas compatibles con xUnit, informes de cobertura y selección de pruebas.
tox
tox es una herramienta para automatizar la gestión del entorno de pruebas y realizar pruebas con múltiples configuraciones de intérpretes:
$
pip install tox
tox te permite configurar complicadas matrices de prueba multiparamétricas mediante un sencillo archivo de configuración de estilo ini.
Opciones para versiones antiguas de Python
Si no controlas tu versión de Python, pero aún así quieres utilizar estas herramientas de prueba, aquí tienes algunas opciones.
unitest2
unittest2es un backport del módulo unittest de Python 2.7 que tiene una API mejorada y mejores aserciones que las disponibles en versiones anteriores de Python.
Si utilizas Python 2.6 o inferior (lo que significa que probablemente trabajas en un gran banco o en una empresa de Fortune 500), puedes instalarlo con pip
:
$
pip install unittest2
Puede que quieras importar el módulo con el nombre unittest para facilitar la portabilidad del código a versiones más recientes del módulo en el futuro:
import
unittest2
as
unittest
class
MyTest
(
unittest
.
TestCase
):
...
De este modo, si alguna vez cambias a una versión más reciente de Python y ya no necesitas el módulounittest2, puedes simplemente cambiar la importación en tu módulo de pruebas sin necesidad de cambiar ningún otro código.
Simulacro
Si te gustó "Mock (en unittest)" pero utilizas una versión de Python inferior a la 3.3, puedes seguir utilizando unittest.mock
importándola como una biblioteca independiente:
$
pip install mock
accesorio
fixture puede proporcionar herramientas que faciliten la configuración y desmontaje de backends de bases de datos para pruebas. Puede cargar conjuntos de datos simulados para utilizarlos con SQLAlchemy, SQLObject, Google Datastore, Django ORM y Storm. Aún hay nuevas versiones, pero sólo se ha probado con Python 2.4 a Python 2.6.
Lechuga y Comportamiento
Lettuce y Behave son paquetes para hacer desarrollo dirigido por el comportamiento (BDD) en Python. BDD es un proceso que surgió de TDD (¡obedece a la cabra que prueba!) a principios de la década de 2000, con el deseo de sustituir la palabra "prueba" en el desarrollo dirigido por pruebas por "comportamiento" para superar los problemas iniciales de los novatos para comprender TDD. El nombre fue acuñado por primera vez por Dan North en 2003 y presentado al mundo junto con la herramienta Java JBehave en un artículo de 2006 para la revista Better Software que se reproduce en la entrada del blog de Dan North,"Introducing BDD".
BDD se hizo muy popular tras la publicación en 2011 de The Cucumber Book (Pragmatic Bookshelf), que documenta un paquete Behave para Ruby.Esto inspiró Lettuce, de Gabriel Falco, y Behave, de Peter Parente, en nuestra comunidad.
Los comportamientos se describen en texto plano utilizando una sintaxis llamada Gherkin que es legible por humanos y procesable por máquinas. Los siguientes tutoriales pueden serte útiles:
Documentación
La legibilidad es un objetivo primordial para los desarrolladores de Python, tanto en la documentación del proyecto como en la del código. Las buenas prácticas descritas en esta sección pueden ahorrarte mucho tiempo, tanto a ti como a los demás.
Documentación del proyecto
Hay documentación de la API para los usuarios del proyecto, y luego hay documentación adicional del proyecto para quienes quieran contribuir a él. Esta sección trata de la documentación adicional del proyecto.
Un archivo README en el directorio raíz debería dar información general tanto a los usuarios como a los mantenedores de un proyecto. Debe ser texto sin formato o estar escrito en algún formato de marcado fácil de leer, como Texto Reestructurado (recomendado porque ahora mismo es el único formato que PyPI entiende) o Markdown.14Debecontener unas líneas que expliquen el propósito del proyecto o biblioteca (sin asumir que el usuario sabe algo sobre el proyecto), la URL de la fuente principal del software y alguna información básica sobre los créditos. Este archivo es el principal punto de entrada para los lectores del código.
Un archivo INSTALL es menos necesario con Python (pero puede ser útil para cumplir requisitos de licencia como la GPL). Las instrucciones de instalación suelen reducirse a un comando, como pip install
module
o python setup.py install
, y se añaden al archivo README.
Siempre debe estar presente un archivo LICENSE que especifique la licencia bajo la cual el software se pone a disposición del público. (Para más información, consulta "Elegir una licencia" ).
Un archivo TODO o una sección TODO en README debe enumerar el desarrollo previsto para el código.
Un archivo o sección CHANGELOG en README debería recopilar un breve resumen de los cambios en la base de código de las últimas versiones.
Publicación del proyecto
Dependiendo del proyecto, tu documentación puede incluir algunos o todos los componentes siguientes:
-
Una introducción debe proporcionar una visión general muy breve de lo que se puede hacer con el producto, utilizando uno o dos casos de uso extremadamente simplificados. Es la presentación de 30 segundos de tu proyecto.
-
Un tutorial debe mostrar con más detalle algunos casos de uso principales. El lector seguirá un procedimiento paso a paso para configurar un prototipo que funcione.
-
Una referencia a la API suele generarse a partir del código (consulta "Docstring frente a comentarios en bloque"). Enumerará todas las interfaces, parámetros y valores de retorno disponibles públicamente.
-
La documentación para desarrolladores está destinada a los colaboradores potenciales. Puede incluir las convenciones del código y la estrategia general de diseño del proyecto.
Esfinge
Sphinxes, de lejos, la herramienta de documentación de15 herramienta de documentación de Python. Utilízala. Convierte el lenguaje de marcado Texto Reestructurado en una serie de formatos de salida, como HTML, LaTeX (para versiones imprimibles en PDF), páginas de manual y texto sin formato.
También hay un gran alojamiento gratuito para tu documentación de Sphinx:Read the Docs. Utilízalo también. Puedes configurarlo con commit hooks a tu repositorio fuente para que la reconstrucción de tu documentación se haga automáticamente.
Nota
Sphinx es famoso por su generación de API, pero también funciona bien para la documentación general de proyectos. LaGuía del Autoestopista de Pythonen línea se ha creado con Sphinx y está alojada en Read the Docs.
Texto reestructurado
Sphinx utiliza TextoReestructurado, y casi toda la documentación de Python está escrita con él. Si el contenido de tu argumento long_description
asetuptools.setup()
está escrito en Texto Reestructurado, se mostrará como HTML en PyPI -otros formatos sólo se presentarán como texto-. Es como Markdown con todas las extensiones opcionales incorporadas. Buenos recursos para la sintaxis son:
O simplemente empieza a contribuir a la documentación de tu paquete favorito y aprende leyendo.
Docstring Versus Block Comentarios
Los docstrings y los comentarios de bloque no son intercambiables. Ambos pueden utilizarse para una función o una clase. Aquí tienes un ejemplo en el que se utilizan ambos:
# This function slows down program execution for some reason.
def
square_and_rooter
(
x
)
:
"""Return the square root of self times self."""
.
.
.
El bloque de comentario inicial es una nota del programador.
El docstring describe elfuncionamiento de la función o clase y se mostrará en una sesión interactiva de Python cuando el usuario escriba
help(square_and_rooter)
.
Las docstrings colocadas al principio de un módulo o en la parte superior de un archivo __init__.py también aparecerán en help()
. La función autodoc deSphinx también puede generar documentación automáticamente utilizando docstrings con el formato adecuado. Las instrucciones sobre cómo hacerlo, y cómo formatear tus docstrings para autodoc, se encuentran en eltutorial de Sphinx. Para más detalles sobre docstrings, consultala PEP 257.
Registro
El módulo de registro forma parte de la Biblioteca Estándar de Python desde la versión 2.3. Se describe sucintamente en la PEP 282. La documentación es notoriamente difícil de leer, salvo un tutorial básico de registro. Está sucintamente descrito enla PEP 282. La documentación es notoriamente difícil de leer, excepto eltutorial básico sobre registro.
El registro tiene dos finalidades:
- Registro de diagnóstico
-
El registro de diagnóstico registra los eventos relacionados con el funcionamiento de la aplicación. Si un usuario llama para informar de un error, por ejemplo, se puede buscar el contexto en los registros.
- Registro de auditoría
-
El registro de auditoría registra eventos para el análisis empresarial. Las transacciones de un usuario (como un flujo de clics) pueden extraerse y combinarse con otros detalles del usuario (como compras eventuales) para informes o para optimizar un objetivo empresarial.
Entrar en una biblioteca
Las notas para configurar el registro de una biblioteca se encuentran en eltutorial de registro. Otro buen recurso para ver ejemplos de uso del registro son las bibliotecas que mencionamos en el capítulo siguiente. Puesto que es el usuario, y no la biblioteca, quien debe dictar lo que ocurre cuando se produce un evento de registro, conviene repetir una advertencia:
Se recomienda encarecidamente que no añadas ningún manejador distinto de
NullHandler
a los registradores de tu biblioteca.
El NullHandler
hace lo que dice su nombre: nada. Por lo demás, el usuario tendrá que desactivar expresamente tu registro si no lo desea.
La mejor práctica a la hora de instanciar registradores en una biblioteca es crearlos únicamente utilizando la variable global __name__
: el módulo logging
crea una jerarquía de registradores utilizando la notación por puntos, por lo que utilizar __name__
garantiza que no se produzcan colisiones de nombres.
Aquí tienes un ejemplo de buenas prácticas delcódigo fuente de Solicitudes:colócalo en el __init__.py de nivel superior de tu proyecto:
# Set default logging handler to avoid "No handler found" warnings.
import
logging
try
:
# Python 2.7+
from
logging
import
NullHandler
except
ImportError
:
class
NullHandler
(
logging
.
Handler
):
def
emit
(
self
,
record
):
pass
logging
.
getLogger
(
__name__
)
.
addHandler
(
NullHandler
())
Iniciar sesión en una aplicación
La App de los Doce Factores, una referencia autorizada sobre buenas prácticas en el desarrollo de aplicaciones, contiene una sección sobrebuenas prácticas de registro. Aboga enfáticamente por tratar los eventos de registro como un flujo de eventos, y por enviar ese flujo de eventos a la salida estándar para que lo gestione el entorno de la aplicación.
Hay al menos tres formas de configurar un registrador:
Pros | Contras | |
---|---|---|
Utilizar un archivo con formato INI |
Es posible actualizar la configuración mientras se ejecuta utilizando la función |
Tienes menos control (por ejemplo, filtros o registradores subclase personalizados) que el posible al configurar un registrador en código. |
Utilizar un diccionario o un archivo con formato JSON |
Además de actualizar mientras se ejecuta, también es posible cargar desde un archivo utilizando el módulo json, en la biblioteca estándar desde Python 2.6. |
Tienes menos control que cuando configuras un registrador en código. |
Utilizar código |
Tienes un control total sobre la configuración. |
Cualquier modificación requiere un cambio en el código fuente. |
Ejemplo de configuración mediante un archivo INI
Encontrarás más detalles sobre el formato del archivo INI en la sección de configuración del registro deltutorial sobre el registro. Un archivo de configuración mínimo tendría este aspecto:
[loggers]
keys
=
root
[handlers]
keys
=
stream_handler
[formatters]
keys
=
formatter
[logger_root]
level
=
DEBUG
handlers
=
stream_handler
[handler_stream_handler]
class
=
StreamHandler
level
=
DEBUG
formatter
=
formatter
args
=
(sys.stderr,)
[formatter_formatter]
format
=
%(asctime)s %(name)-12s %(levelname)-8s %(message)s
asctime
, name
, levelname
, y message
son atributos opcionales disponibles en la biblioteca de registro. La lista completa de opciones y sus definiciones está disponible en la documentación de Python. Digamos que nuestro archivo de configuración de registro se llama logging_config.ini. Entonces, para configurar el registrador utilizando esta configuración en el código, utilizaríamos logging.config.fileConfig()
:
import
logging
from
logging.config
import
fileConfig
fileConfig
(
'logging_config.ini'
)
logger
=
logging
.
getLogger
()
logger
.
debug
(
'often makes a very good meal of
%s
'
,
'visiting tourists'
)
Ejemplo de configuración mediante un diccionario
A partir de Python 2.7, puedes utilizar un diccionario con detalles de configuración.PEP 391contiene una lista de los elementos obligatorios y opcionales del diccionario de configuración. Aquí tienes una implementación mínima:
import
logging
from
logging.config
import
dictConfig
logging_config
=
dict
(
version
=
1
,
formatters
=
{
'f'
:
{
'format'
:
'
%(asctime)s
%(name)-12s
%(levelname)-8s
%(message)s
'
}
},
handlers
=
{
'h'
:
{
'class'
:
'logging.StreamHandler'
,
'formatter'
:
'f'
,
'level'
:
logging
.
DEBUG
}
},
loggers
=
{
'root'
:
{
'handlers'
:
[
'h'
],
'level'
:
logging
.
DEBUG
}
}
)
dictConfig
(
logging_config
)
logger
=
logging
.
getLogger
()
logger
.
debug
(
'often makes a very good meal of
%s
'
,
'visiting tourists'
)
Ejemplo de configuración directamente en código
Y por último, aquí tienes una configuración mínima de registro directamente en código:
import
logging
logger
=
logging
.
getLogger
()
handler
=
logging
.
StreamHandler
()
formatter
=
logging
.
Formatter
(
'
%(asctime)s
%(name)-12s
%(levelname)-8s
%(message)s
'
)
handler
.
setFormatter
(
formatter
)
logger
.
addHandler
(
handler
)
logger
.
setLevel
(
logging
.
DEBUG
)
logger
.
debug
(
'often makes a very good meal of
%s
'
,
'visiting tourists'
)
Elegir una licencia
En Estados Unidos, cuando no se especifica una licencia con tu publicación fuente, los usuarios no tienen derecho legal a descargarla, modificarla o distribuirla. Además, la gente no puede contribuir a tu proyecto a menos que les digas con qué reglas deben jugar. Necesitas una licencia.
Licencias Upstream
Si estás derivando de otro proyecto, tu elección puede estar determinada por las licencias upstream. Por ejemplo, la Python Software Foundation (PSF) pide a todos los contribuyentes al código fuente de Python que firmen un acuerdo de contribuyente que licencia formalmente su código a la PSF (conservando sus propios derechos de autor) bajo una de dos licencias.16
Dado que ambas licencias permiten a los usuarios conceder sublicencias en condiciones diferentes, la PSF es libre de distribuir Python bajo su propia licencia, la Licencia de la Fundación para el Software de Python (Python Software Foundation License). Una FAQ de la Licencia de la PSFdetalla lo que los usuarios pueden y no pueden hacer en lenguaje sencillo (no legal). No está pensada para un uso posterior más allá de la licencia de distribución de Python de la PSF.
Opciones
Hay muchas licencias disponibles entre las que elegir. El PSF recomienda utilizar una de laslicencias aprobadas por el Instituto de Código Abierto (OSI). Si con el tiempo deseas contribuir con tu código al PSF, el proceso será mucho más fácil si empiezas con una de las licencias especificadas en lapágina de contribuciones.
Nota
Recuerda cambiar el texto del marcador de posición en las plantillas de licencia para que refleje realmente tu información. Por ejemplo, la plantilla de licencia MIT contieneCopyright (c) <year> <copyright holders>
en su segunda línea. La Licencia Apache, Versión 2.0 no requiere ninguna modificación.
Las licencias de código abierto suelen pertenecer a una de estas dos categorías:17
- Licencias permisivas
-
Las licencias permisivas, a menudo llamadas también licencias del estilo de la Distribución de Software Berkeley (BSD), se centran más en la libertad del usuario para hacer con el software lo que quiera. Algunos ejemplos:
-
La licencia Apache versión2.0 es la actual, modificada para que la gente pueda incluirla sin modificaciones en cualquier proyecto, pueda incluir la licencia por referencia en lugar de listarla en cada archivo, y pueda utilizar código con licencia Apache 2.0 con la Licencia Pública General de GNU versión 3.0 (GPLv3).
-
Tanto la licencia BSD de 2 cláusulas como la de 3 cláusulas-lalicencia de tres cláusulas es la licencia de dos cláusulas más una restricción adicional sobre el uso de las marcas registradas del emisor.
-
Las licencias del Instituto Tecnológico de Massachusetts (MIT) -ambas las versiones Expat y X11 llevan el nombre de productos populares que utilizan las respectivas licencias.
-
La licencia del Consorcio de Software de Internet (ISC) -es casi idéntica a la licencia MIT excepto por unas pocas líneas que ahora se consideran superfluas.
-
- Licencias copyleft
-
Las licencias con copyleft, o licencias menos permisivas, se centran más en garantizar que el propio código fuente -incluidos los cambios realizados en él- esté disponible. La más conocida es la familia GPL, cuya versión actual es laGPLv3.
Nota
La licencia GPLv2 no es compatible con Apache 2.0; por tanto, el código licenciado con GPLv2 no puede mezclarse con proyectos con licencia Apache 2.0. Pero los proyectos con licencia Apache 2.0pueden utilizarse en proyectos GPLv3 (que posteriormente deben ser todos GPLv3).
Todas las licencias que cumplen los criterios de la OSI permiten el uso comercial, la modificación del software y la distribución posterior, con diferentes restricciones y requisitos. Todas las que figuran en la Tabla 4-4también limitan la responsabilidad del emisor y exigen que el usuario conserve los derechos de autor y la licencia originales en cualquier distribución posterior.
Familia de licencias | Restricciones | Indemnizaciones | Requisitos |
---|---|---|---|
BSD |
Protege la marca del emisor (cláusula 3 de BSD) |
Permite una garantía (cláusula 2 y cláusula 3 de BSD) |
- |
MIT (X11 o Expat), ISC |
Protege la marca del emisor (ISC y MIT/X11) |
Permite sublicenciar con una licencia diferente |
- |
Apache versión 2.0 |
Protege la marca del emisor |
Permite la sublicencia, el uso en patentes |
Debe indicar los cambios realizados en la fuente |
GPL |
Prohíbe conceder sublicencias con una licencia diferente |
Permite una garantía, y (sólo GPLv3) el uso en patentes |
Debe indicar los cambios en la fuente e incluir el código fuente |
Recursos para licencias
El libro de Van LindbergIntellectual Property and Open Source (O'Reilly) es un gran recurso sobre los aspectos legales del software de código abierto. Te ayudará a comprender no sólo las licencias, sino también los aspectos legales de otros temas de propiedad intelectual, como las marcas, las patentes y los derechos de autor, en su relación con el código abierto. Si no te preocupan tanto los asuntos legales y sólo quieres elegir algo rápidamente, estos sitios pueden ayudarte:
-
GitHub ofrece una práctica guía que resume y compara las licencias en unas pocas frases.
-
TLDRLegal18 enumera lo que se puede, no se puede y se debe hacer según los términos de cada licencia en viñetas rápidas.
-
La lista de licencias aprobadasde la OSI contiene el texto completo de todas las licencias que han superado su proceso de revisión de licencias para cumplir la Definición de Código Abierto (permitir que el software se utilice, modifique y comparta libremente).
1 Enunciado originalmente por Ralph Waldo Emerson en Self-Reliance, se cita en PEP 8 para afirmar que el mejor criterio del codificador debe prevalecer sobre la guía de estilo. Por ejemplo, la conformidad con el código circundante y las convenciones existentes es más importante que la coherencia con PEP 8.
2 Tim Peters es un antiguo usuario de Python que con el tiempo se convirtió en uno de sus desarrolladores principales más prolíficos y tenaces (creó el algoritmo de ordenación de Python, Timsort), y una presencia frecuente en la Red. En un momento dado se rumoreó que era un portador de Python del programa de IA stallman.el de Richard Stallman. La teoría conspirativa original apareció en un listserv a finales de los 90.
3 diff es una utilidad del shell que identifica y muestra las líneas que difieren entre dos archivos.
4 Un máximo de 80 caracteres según PEP 8, 100 según muchos otros, y para ti, lo que diga tu jefe. ¡Ja! Pero, sinceramente, cualquiera que haya tenido que utilizar alguna vez un terminal para depurar código de pie junto a una estantería apreciará rápidamente el límite de 80 caracteres (a partir del cual el código no se enrolla en un terminal) y, de hecho, prefiere entre 75 y 77 caracteres para permitir la numeración de líneas en Vi.
5 Véase Zen 14. Guido, nuestro BDFL, es holandés.
6 Por cierto, ésta es la razón por la que sólo los objetos hashables pueden almacenarse en conjuntos o utilizarse como claves de diccionario. Para que tus propios objetos Python sean hashables, define una función miembro object.__hash__(self)
que devuelva un número entero. Los objetos que se comparan igual deben tener el mismo valor hash. La documentación de Python tiene más información.
7 En este caso, el método __exit__()
sólo llama al método close()
de la envoltura de E/S, para cerrar el descriptor de archivo. En muchos sistemas, hay un número máximo permitido de descriptores de archivo abiertos, y es una buena práctica liberarlos cuando hayan terminado.
8 Si quieres, puedes llamar a tu módulo mi_spam.py, pero nuestro amigo el guión bajo no debería verse a menudo en los nombres de los módulos (los guiones bajos dan la impresión de ser un nombre de variable).
9 Gracias a la PEP 420, que se implementó en Python 3.3, ahora existe una alternativa al paquete raíz, denominada paquete de espacio de nombres. Los paquetes de espacio de nombres no deben tener un __init__.py y pueden estar dispersos en varios directorios en sys.path
. Python reunirá todas las piezas y las presentará juntas al usuario como un único paquete.
10 En la documentación sobre extensiones de Python encontrarás instrucciones para definir tus propios tipos en C .
11 Un ejemplo de algoritmo hash sencillo consiste en convertir los bytes de un elemento en un número entero, y tomar su valor módulo de algún número. Así es como memcached distribuye las claves entre varios ordenadores.
12 Debemos admitir que aunque, según PEP 3101, el formato estilo porcentaje(%s, %d, %f) está obsoleto desde hace más de una década, la mayoría de los veteranos siguen utilizándolo, y PEP 460 acaba de introducir este mismo método para dar formato a los objetos bytes
o bytearray
.
13 Ten en cuenta que unittest.TestCase.tearDown
no se ejecutará si el código da error. Esto puede sorprenderte si has utilizado funciones de unittest.mock
para alterar el comportamiento real del código. En Python 3.1, se añadió el método unittest.TestCase.addCleanup()
que coloca una función de limpieza y sus argumentos en una pila que se llamará una a una después de unittest.TestCase.tearDown()
o se llamará de todos modos, independientemente de si se ha llamado a tearDown()
. Para más información, consulta la documentación sobre unittest.TestCase.addCleanup()
.
14 Para los interesados, se está debatiendo la posibilidad de añadir soporte Markdown a los archivos README en PyPI.
15 Otras herramientas que puedes ver son Pycco, Ronn, Epydoc (ya descatalogada) y MkDocs. Casi todo el mundo utiliza Sphinx y te recomendamos que tú también lo hagas.
16 En el momento de escribir esto, eran la Licencia Libre Académica v. 2.1 o la Licencia Apache, Versión 2.0. La descripción completa de cómo funciona esto se encuentra en la página de contribuciones del PSF.
17 Todas las licencias descritas aquí están aprobadas por OSI, y puedes obtener más información sobre ellas en la página principal de licencias de OSI.
18 tl;dr significa "Demasiado largo; no lo leí", y al parecer existía como taquigrafía para editores antes de su popularización en Internet.
Get La guía del autoestopista pitón now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.