esvdev_logo

25 hábitos de novato en Python - Parte 1

Publicado: 08/04/24

Escribí código más pythonico que demuestre que ya no sos un principiante

Por algo se empieza

¿Recordás tus primeros pasos con la programación? En mi caso fue C++ en la secundaria. Nunca me aburrió tanto una materia. Es loco decirlo, ya que ahora, poco más de una década después de terminada la secundaria, no solo trabajo como programador (conseguí mi primer trabajo en febrero de 2024), sino hasta le tomé el gusto a escribir artículos técnicos y me la paso investigando maneras de cómo hacer mejor lo que ya sé hacer y como puedo lograr eso que aun no sé.

Volviendo a la pregunta, te invito a pensar ese primer lenguaje que aprendiste, pero mas que nada en tus primeros pasos en él. Seamos honestos, el primer código que escribimos en el lenguaje que sea, y más si somos trainee/junior, suele ser bastante feo. ¿Quièn no escribió una montaña de “if-else” o se quedó atrapado infinitamente en los oscuros recovecos de un “while True”?

Eso pasa hasta que empezás a empaparte más con el lenguaje que estás estudiando. Empezás a ver que hay otras formas hacer lo mismo pero de maneras que te permiten dejar tu código más legible, mantenible y hasta incluso puede que con una performance superior.

Bueno, a eso vamos con este artículo. Como ya he hecho en otros posts, hoy te traigo la traducción de un video que me resultó bastante interesante: “25 hábitos de novato en Python que necesitas dejar”. Será una serie de posts donde cubriré 5 hábitos por post para facilitar la lectura.

Porque por algo se empieza. No te sientas mal por la forma en que escribiste (o estás escribiendo) código en Python. Sé más amable con vos mismo/a, estás aprendiendo. Pero que eso no se convierta en una excusa para seguir haciendo las cosas de la misma manera siendo que hay formas mejores de hacerlo. Arranquemos.

1. Formateo manual de strings

Creo que todos pasamos por acá. En cualquier curso de Python se suele enseñar que para imprimir strings por pantalla podemos utilizar el operador de suma para concatenar strings y otros valores, como lo puede ser un entero o el contenido de una variable. Por ejemplo:

def saludo(apellido, nombre):
	print("Hola" + apellido + nombre + ", bienvenido a la aplicación."
	
saludo("Roronoa", "Zoro") #llamamos a la función

"Hola Roronoa Zoro, bienvenido a la aplicación" #resultado

Mirá la diferencia ahora si usamos lo que se conoce como f-strings. Esta mejora, introducida en la versión 3.6 de Python, nos permite escribir código más legible gracias a que podemos formatear nuestros strings pasando los valores que necesitamos dentro de llaves { }, lo que se conoce como campos de reemplazo.

def saludo(apellido, nombre):
	print(f"Hola {apellido} {nombre}, bienvenido a la aplicación."
	
saludo("Roronoa", "Zoro") #llamamos a la función

"Hola Roronoa Zoro, bienvenido a la aplicación" #resultado

¿Ves la diferencia? Ambas formas producen el mismo resultado, pero las f-strings tienen como ventaja que dejan el código mucho más legible y tienen menos tendencia a errores. Si te interesa saber más te dejo algunos recursos:

2. Cerrar archivos manualmente

Este es un hábito que realmente necesitas dejar de lado si es que lo estás haciendo. En el caso anterior, de las f-strings, el único problema que tenés si no las usas es que tu código es más difícil de leer, pero no hay mucha más gravedad que eso. En el caso de los archivos, SÍ hay un problema si vas por la vía de cerrarlos (es decir, dejar de usarlos) de manera manual. Examinemos el siguiente código:

def escribir_en_archivo(nombre_de_archivo):
	archivo = open(nombre_de_archivo, "w")
	archivo.write("hola") #si ocurre un error acá, la linea de abajo nunca se va a ejecutar, dejando el archivo abierto en memoria
	archivo.close()

Tenemos una función que recibe el nombre de un archivo. Con el método “open ( )” intentará abrirlo en modo escritura (por eso le pasamos “w” como segundo argumento). Luego intentamos escribir un “hola” pasandole dicha string al método “write ( )”. Finalmente, llamamos a “close ( )” y cerramos el archivo.

Todo parece muy bonito hasta que pensamos en que si tenemos un error en la segunda línea de nuestra función, la ejecución del resto de lineas se interrumpe. Por ende, nunca se llama al método “close ( )” y nuestro archivo queda “retenido” en memoria, deperdiciando recursos e incluso imposibilitandonos de acceder a él desde otros puntos de nuestro programa hasta que lo liberemos.

Es más, ni siquiera hace falta que ocurra un error, te puede pasar (como me ha pasado a mí) que olvides llamar a “close ( )” y tu archivo, una vez abierto, quede en el inframundo inhóspito de tu memoria RAM esperando a que alguien lo rescate. Bueno, tampoco tan así pero se entiende.

“Ok amigo, pero dame soluciones, no problemas”. De acuerdo, acá te va una: context managers (o gestores de contexto). Esto se puede lograr usando la expresión “with”.

def escribir_en_archivo_con_context_manager(nombre_de_archivo):
	with open(nombre_de_archivo) as a:
		a.write("hola")

Lo que ocurre en esta nueva versión de nuestra función es que la expresión “with” llama a la función “open ( )”, abre el archivo y con la expresión “as” establecemos un alias, ahorrandonos escribir demás ya que desde este alias podemos llamar a los métodos que necesitemos.

Pero lo más importante acá es que nuestro archivo siempre se cierra. “¿Y si ocurre un error?” Se cierra. “Ah, pero no llamé a close!” No importa, se cierra igual una vez que tu programa sale del with, así que no te hace falta llamar a “close ( )”. Una maravilla. Te dejo un enlace que seguro te va a servir:

https://keepcoding.io/blog/errores-al-abrir-y-cerrar-archivos-en-python/

3. Usar solamente except al capturar excepciones

Entramos en el terreno de las excepciones. La idea no es explicar qué son y para qué se usan. Asumo que tenés una noción de esto. El punto es que te preguntes cómo estabas usando el mecanismo de “try-except”. Veamos lo siguiente.

def solamente_except():
	while True:
		try:
			entrada_usuario = input("Ingresa un número: ")
			número = int(entrada_usuario)
			break
		except:
			print("No es un número, por favor intenta de nuevo.")

Sencillo, ¿no? Función que contiene un loop infinito y que nos pide que ingresemos un número mediante la función “input ( )”. Luego intentamos mediante el método “int ( )” convertir lo que el usuario haya ingresado a un entero y lo guardamos en la variable “número”. Una vez hecho esto, el bucle se rompe en la primera iteración gracias a la sentencia “break”. Todo esto ocurre dentro de un bloque “try”, de modo que si algo sale mal, el bloque “except” imprimirá un mensaje diciendonos que lo que ingresamos previamente no es un número.

El problema que tenemos acá es que el bloque except capturará todo tipo de excepcion que ocurra. Si querés salir del bucle infinito haciendo algo como ctrl+c, esto dará como resultado la excepcion “KeyboardInterrupt” y adivina qué: el except lo va a capturar tambien, de modo que no vas a poder salir del bucle. Solamente vas a recibir por pantalla el print diciendo que lo que ingresaste no es un numero, algo que nada que ver si lo único que querías hacer es parar la ejecución con un ctrl+c. Veamos como evitar algo como esto.

def usando_clase_Exception():
	while True:
		try:
			entrada_usuario = input("Ingresa un número: ")
			número = int(entrada_usuario)
		except Exception:
			print("No es un número, por favor intenta de nuevo.")

Lo mismo, con la diferencia de que ahora estamos usando la clase “Exception”. Tal como dice la documentación de Python, todos las excepciones que no impliquen salir del sistema se derivan de esta clase. De modo que hacer algo como un ctrl+c no será capturado por el bloque except, permitiendonos parar el bucle/salir del programa. Pero podemos ir un paso mas allá.

def usando_tipo_de_excepcion_especifico():
	while True:
		try:
			entrada_usuario = input("Ingresa un número: ")
			número = int(entrada_usuario)
		except ValueError:
			print("No es un número, por favor intenta de nuevo.")

Ahora ya no usamos la clase “Exception”, sino que usamos el tipo de excepcion que verdaderamente va a ocurrir si ingresamos algo como una letra y luego queremos convertir esa letra a número mediante la función “int ( )”. Esta es la mejor forma de manejar excepciones, lo que permite que tu código sea más pythonico y legible. Si te interesa ahondar más en el tema, te comparto recursos de interés.

4. Confundir operador de potencia

Cuando ví esto en el video me resultó extraño. La verdad que a mi personalmente nunca me ocurrió, pero “hay de todo en la viña del Señor” dicen por ahí (frase de viejo, lo sé). Aunque sí aprendí algo nuevo y por eso decidí incluirlo en el artículo.

def confundir_simbolo_de_potencia(x, p):
	y = x ^ p #Esto no es potencia, es bitwise xor
	y = x ** p #Esto es potencia

A ciertas personas les ha ocurrido confundir el simbolo “^”, pensando que es el que hay que utilizar para realizar potencias. Nope, es un error. El símbolo correcto es el doble asterisco “**”. Sin embargo, el símbolo “^” es lo que se conoce como operador “XOR”, uno de los operadores denominados “bitwise” u “operadores de bit”. No voy a entrar en detalle porque sino el post va a quedar larguísimo, pero si te pica la curiosidad te dejo algo para leer luego.

https://ellibrodepython.com/operadores-bitwise

5. Uso de argumentos mutables por defecto

Voy a admitir que al momento de escribir este post (6 de Abril de 2024) no sabía esto que te voy a comentar a continuación. Supongamos que definimos la siguiente función:

def agregar_numero_a_lista(numero, lista = []):
	lista.append(numero)
	return lista
	
numero_1 = agregar_numero_a_lista(1) #Retorna lista con el numero [1]
numero_2 = agregar_numero_a_lista(2) #Retorna lista con numero 1 y numero 2 [1, 2] ¿Como es posible?

Arranquemos por lo primero: definir un argumento por defecto implica que si llamamos a una funcion y NO le pasamos dicho argumento, la funcion en vez de tirar un error, se queda con el valor que nosotros hayamos definido que debe tener por defecto.

En este caso, si llamamos a “agregar_numero_a_lista” pasandole un numero pero SIN pasarle el argumento lista, lo que va hacer la funcion es inicializar una lista vacia, ya que es el valor que establecimos por defecto para ese parametro.

Siguiendo con nuestro ejemplo, creamos dos variables donde hacemos dos llamados a esta funcion. En la variable “numero_1” la llamamos pasandole un 1 y SIN enviarle el argumento lista. La funcion hace lo esperado: inicializa una lista vacia y la retorna conteniendo el numero que le indicamos que agregue.

Lo interesante pasa en la variable “numero_2”. Al llamar a la funcion nuevamente y SIN pasarle el argumento lista, en teoría debería haber inicializado una lista vacia, agregarle el numero 2 y retornar dicha lista conteniendo solamente un 2. Pero no, no es lo que termina sucediendo.

Esto es así debido a que los argumentos a los cuales les establecemos un valor por defecto son definidos cuando la función es definida, pero no cuando es ejecutada. Lee nuevamente, porque es una diferencia importante. En nuestro caso, cada llamada a la función comparte la misma lista, y una de las propiedades de las listas en Python es que son mutables. Por ende, cada vez que llamamos a esta funcion SIN pasarle el argumento lista, realizará la operación de agregarle un numero a la lista pero con la lista en el estado en el que la dejó la ultima llamada a la funcion.

Te invito a copiar el codigo y probarlo en tu editor. Intentá imprimir los valores que contiene la lista despues de cada llamada. Es más, probá agregarle más numeros y ver como cada llamada a la función “retoma” la lista en el estado en el que la dejó la llamada anterior. Bueno, veamos ahora como evitar este resultado indeseado.

def agregar_numero_a_lista(numero, lista = None):
	if lista is None:
		lista = []
	lista.append(numero)
	return lista
	
numero_1 = agregar_numero_a_lista(1) #Retorna lista con el numero [1]
numero_2 = agregar_numero_a_lista(2) #Retorna lista con numero 2 [2]

Si queres un argumento que sea mutable y a la vez tenga un valor por defecto, podes “defaultearlo” a None. Luego podes chequear dentro de la funcion si el argumento es None y, si lo es, ahí recién estableces el valor por defecto que deseas que tenga.

Haciendo esto, cada llamada a nuestra funcion donde no se le pase el argumento “lista” tendrá su propia lista vacía y realizará operaciones en dicha lista, dejando de “compartir” la lista como en el anterior código.

Conclusión

Esta fue la primera parte de los 25 hábitos de novato en Python que es importante que tengamos en cuenta. Si los tenés, te invito a probar los métodos explicados acá para poder sacarte de encima esas costumbres y así puedas generar un código mas pythonico, legible y mantenible. También servirá para mostrar a otros (como pueden ser otros compas dev o quien sabe, un futuro empleador) que tu código va un paso mas allá de lo que se suele enseñar en cursos introductorios del lenguaje.

Ahora, si no los tenés, te invito a compartir este artículo con otros pythonistas para que puedan mejorar su código y contribuirles con conocimiento que pueda servirles. A ver, digo, si me maté escribiendo esto es para que todos puedan beneficiarse de él. No seas tacaño/a y no te quedes los aprendizajes para vos nomás, ayudá a otros 🙂

Para la próxima vamos a seguir examinando otros 5 hábitos de novato en Python a identificar y corregir, en caso de tenerlos.

Tags:
#python

Escrito por:

Elias Velazquez photo

Elias Velazquez

Python / ETL dev | Data Engineer en progreso | Musico | Nerd de yerbas varias