En lenguajes como C o C++ que aprovechan el paradigma funcional, en los 90s aún no se implementaba el paradigma orientado a objetos de forma masiva, sobre todo en C. Si ya eres viejo de seguirme en este blog y te interesaron los tutoriales de shaders que hay un montón, habrás notado que CG es básicamente lenguaje C con algunos cambios y solo enfocado a gráficos, cada vez que queríamos hacer un tipo de dato complejo para pasarlo empaquetado dentro de una función, lo hacíamos a través de un #struct.
Ahora estamos hablando de Python, en este lenguaje no hay tampoco algo como un struct donde podamos empaquetar un montón de variables y pasarlos como un parámetro dentro de una función, sin embargo hay alternativas.
Uso de clases.
Como mencioné arriba, un struct no es mas que un conjunto de datos asociados a un identificador, en Python hay esencialmente dos formas, la primera es crear una clase.
class Cliente:
def __init__(self,nombre,identidad,email,telefono):
self.nombre = nombre
self.identidad = identidad
self.email = email
self.telefono = telefono
# El acceso a los atributos es bastante trivial
cliente = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
print(cliente.nombre)
print(cliente.identificacion)
print(cliente.email)
print(cliente.telefono)
Usar clases nos permite hacer chequeos de información, como verificar si un correo es válido.
class Cliente:
def __init__(self,nombre,identidad,email,telefono):
self.nombre = nombre
self.identidad = identidad
if "@" not in email:
raise.ValueError("El e-mail no es valido")
self.email = email
self.telefono = telefono
También nos permite asignar valores predeterminados como en cualquier función.
class Cliente:
def __init__(self,nombre,identidad,email = None,telefono = None):
self.nombre = nombre
self.identidad = identidad
if "@" not in email:
raise.ValueError("El e-mail no es valido")
self.email = email
self.telefono = telefono
Las clases parecen funcionar bien siempre y cuando tengamos una estructura bien definida, pero en el caso que no sepamos que datos vamos a necesitar o si pensamos ampliar los datos sobre la marcha, un simple diccionario puede servir bien para la tarea.
cliente = {
"nombre" : "Juan Perez",
"identidad" : "43327846",
"email" : "juanperez@darkcom.dev",
"telefono : 3113458762"
}
print(cliente["nombre"])
print(cliente["identidad"])
print(cliente["email"])
print(cliente["telefono"])
Si usamos los diccionarios, podemos aprovecharnos de las comparaciones por defecto en el caso que queramos comparar dos clientes.
Si hacemos usos de clases, podemos hacerlo mediante el método mágico `__eq__()` que chequee la igualdad de cada uno de los atributos.
class Cliente:
def __init__(self,nombre,identidad,email,telefono):
self.nombre = nombre
self.identidad = identidad
self.email = email
self.telefono = telefono
def __eq__(self, other):
return (self.nombre == other.nombre and self.identidad == other.identidad and self.email == other.email and self.telefono == other.telefono)
cliente1 = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
cliente2 = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
print(cliente1 == cliente2) #True
Uso de diccionarios.
Sin esta definición, cada instancia de la clase, por mas que tengan los mismos nombres, los mismos teléfonos y todos los datos así sean iguales, la comparación va a devolver falso, simplemente porque los identificadores en memoria son distintos.
Con la función mágica #__eq__() podemos usar comparaciones sin ningún problema `== o !=`
En cambio con los diccionarios estas comparaciones funcionan sin implementar código adicional.
cliente1 = {
"nombre" : "Juan Perez",
"identidad" : "43327846",
"email" : "juanperez@darkcom.dev",
"telefono : 3113458762"
}
cliente2 = {
"nombre" : "Juan Perez",
"identidad" : "43327846",
"email" : "juanperez@darkcom.dev",
"telefono : 3113458762"
}
print(cliente1 == cliente2) #True
Otra ventaja que tienen los diccionarios es que es muy fácil convertir los datos a archivos JSON o XML, esto es particularmente útil para compartir información entre aplicaciones en la red por ejemplo.
import json
cliente = {
"nombre" : "Juan Perez",
"identidad" : "43327846",
"email" : "juanperez@darkcom.dev",
"telefono : 3113458762"
}
print(json.dumps(cliente))
El segundo método es usar una colección llamada #namedtuple, esto implica un poco de trabajo extra.
from collections import namedtuple
Cliente = namedtuple("Cliente",("nombre","identificacion","email","telefono"))
cliente = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
print(cliente.nombre)
print(cliente.identificacion)
print(cliente.email)
print(cliente.telefono)
También soporta la comparación sin código extra.
from collections import namedtuple
Cliente = namedtuple("Cliente",("nombre","identificacion","email","telefono"))
cliente1 = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
cliente2 = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
print(cliente1 == cliente2) #True
Uso de Attrs.
Ahora bien cuando necesitamos la validación de los datos y la rigidez de las clases, combinado con la rapidez, la versatilidad y las comparaciones de los diccionarios y las #tuplas nombradas, allí nos encontramos el maravilloso módulo de #attr.
import attr
@attr.s
class Cliente:
nombre = attr.ib()
identificacion = attr.ib()
email = attr.ib()
telefono = attr.ib()
cliente = Cliente("Juan Perez", "43327846", "juanperez@darkcom.dev", "3113458762")
print(cliente.nombre)
print(cliente.identificacion)
print(cliente.email)
print(cliente.telefono)
Los operadores de igualdad y desigualdad sirve para toda la clase con el #decorador attr.s
y chequean que todos los atributos de la clase tengan el mismo valor, como ocurre con los diccionarios y las tuplas nombradas.
Podemos incluso iniciar los valores sin usar la definición __init__()
import attr
@attr.s
class Cliente:
nombre = attr.ib()
identificacion = attr.ib()
email = attr.ib(default = None)
telefono = attr.ib(default = None)
@email.validator
def is_email(self, attribute, value):
if "@" not in value:
raise ValueError("Not a valid email address.")
Además con attr podremos convertir a otros tipos de colección como diccionarios y tuplas.
print(attr.asdict(cliente))
print(attr.astuple(cliente))
Este módulo no se incluye en las librerías estándar de Python aunque algo similar hace el módulo estándar de #dataclasses, a partir de la versión 3.7 de Python. Para instalarlo simplemente ejecuta.
pip instal attrs
En resumen cuando necesites una estructura de datos, mira la siguiente tabla para escoger lo que mas se adecue a tus necesidades.
+---------------+---------------+---------------+---------------+
| |rigidez y |versatilidad, |funciones |
| |validaciones |rapidez y |geniales |
| | |comparaciones | |
+---------------+---------------+---------------+---------------+
|Clases normales|SI |NO |NO |
+---------------+---------------+---------------+---------------+
|Diccionarios | | | |
|namedtuples |NO |SI |NO |
+---------------+---------------+---------------+---------------+
|Attrs |SI |SI |SI |
+---------------+---------------+---------------+---------------+
Conclusiones.
En un principio puede que Python parezca carente de ciertas características, pero conforme vas usando el lenguaje vas encontrando alternativas para hacer lo mismo, incluso a veces mejor.
Lo importante es saber que tipo de dato usar que sea versátil y que aporte funcionalidad, en mi experiencia he notado que según que ámbito uno usa mas algunos mas que otros, en videojuegos y todo aquello que tenga que ver con gameplay, hago mas uso de clases que de diccionarios y para la generación de datos, sobre todo el manejo de textos hago mas uso de diccionarios.
Con el tiempo y la experiencia, el instinto lo va guiando sobre que estructuras de datos usar. Bueno no siendo mas nos vemos en un próximo Blog.
Comments