Estructuras

Estructuras



Python no tiene ningún mecanismo especial para crear estructuras, como la palabra reservada struct en C/C++ y en otros lenguajes. ¿Cómo podemos suplir esta necesidad? Que no nos sorprenda: una estructura no es más que un conjunto de valores asociados a un identificador. Dado que el lenguaje provee varias formas de conseguir esto, no fue necesario añadir una nueva.

En la sección de microrecursos tenemos un pequeño artículo sobre cómo crear una estructura en Python. Allí recomendamos dos métodos: el primero consiste en simplemente crear una clase.

Supongamos que necesitamos una estructura para representar clientes con un nombre, una dirección de correo electrónico, y un número de teléfono. Ello se consigue, según este primer método, de la siguiente manera.

class Customer:

    def __init__(self, name, email, phone):
        self.name = name
        self.email = email
        self.phone = phone

Luego, la creación de un cliente y el acceso a sus atributos es bastante trivial:

customer = Customer("Pablo", "pablo@empresa.com", "123-456-789")
print(customer.name)
print(customer.email)
print(customer.phone)

Utilizar clases nos permite hacer chequeos en la información que se provee como atributos. Por ejemplo, si queremos asegurarnos de que se trata de una dirección de correo electrónico válida:

class Customer:

    def __init__(self, name, email, phone):
        self.name = name
        if "@" not in email:
            raise ValueError("Not a valid email address.")
        self.email = email
        self.phone = phone

O bien asignar valores por defecto, como en cualquier otra función:

class Customer:

    def __init__(self, name, email=None, phone=None):
        self.name = name
        if email is not None and "@" not in email:
            raise ValueError("Not a valid email address.")
        self.email = email
        self.phone = phone

Este método parece ser ideal si necesitamos una estructura con un conjunto de atributos y validaciones bien definidos. Pero cuando manejamos información de estructura variada queremos algo más sencillo y versátil. En ese caso un simple diccionario puede suplir a una estructura.

customer = {
    "name": "Pablo",
    "email": "pablo@empresa.com",
    "phone": "123-456-789"
}
print(customer["name"])
print(customer["email"])
print(customer["phone"])

Además, de los diccionarios podemos aprovechar sus comparaciones por defecto. Si queremos comparar dos clientes, en nuestra estructura basada en clases tendríamos que definir el método mágico __eq__() que chequee la igualdad de cada uno de los atributos:

class Customer:
    # ...

    def __eq__(self, other):
        return (self.name == other.email and
        self.email == other.email and
        self.phone == other.phone)


customer1 = Customer("Pablo", "pablo@empresa.com", "123-456-789")
customer2 = Customer("Pablo", "pablo@empresa.com", "123-456-789")
print(customer1 == customer2)  # True

(Sin esta definición, dos instancias de Customer siempre son distintas, por más que tengan los mismos atributos y valores).

En cambio, en los diccionarios, los operadores de igualdad (==) y desigualdad (!=) se comportan de esta manera sin ningún código adicional:

customer1 = {
    "name": "Pablo",
    "email": "pablo@empresa.com",
    "phone": "123-456-789"
}
customer2 = {
    "name": "Pablo",
    "email": "pablo@empresa.com",
    "phone": "123-456-789"
}
print(customer1 == customer2)  # True

También, convertir un diccionario a formato JSON o XML es bastante sencillo, particularmente útil para intercambiar información con otras aplicaciones en la red. Por ejemplo:

import json

customer = {
    "name": "Pablo",
    "email": "pablo@empresa.com",
    "phone": "123-456-789"
}
print(json.dumps(customer))

Esto implicaría un poco de trabajo extra al trabajar con clases convencionales.

El segundo método que proponíamos era usando la colección estándar namedtuple:

from collections import namedtuple

Customer = namedtuple("Customer", ("name", "email", "phone"))
customer = Customer(name="Pablo", email="pablo@empresa.com",
                    phone="123-456-789")
print(customer.name)
print(customer.email)
print(customer.phone)

Que a su vez soporta la comparación de sus atributos sin código extra.

customer1 = Customer(name="Pablo", email="pablo@empresa.com",
                     phone="123-456-789")
customer2 = Customer(name="Pablo", email="pablo@empresa.com",
                     phone="123-456-789")
print(customer1 == customer2)  # True

Ahora bien, cuando necesitamos combinar la rigidez y la capacidad de validar datos que nos brindan las clases con la versatilidad, rapized y comparaciones por defecto de los diccionarios o tuplas con nombre, allí es cuando nos encontramos con el maravilloso módulo attrs:

import attr

@attr.s
class Customer:
    name = attr.ib()
    email = attr.ib()
    phone = attr.ib()

customer = Customer(name="Pablo", email="pablo@empresa.com",
                    phone="123-456-789")
print(customer.name)
print(customer.email)
print(customer.phone)

Los operadores de igualdad y desigualdad valen para toda clase definida con el decorador attr.s y chequean que todos los atributos definidos tengan el mismo valor (al igual que los diccionarios y las tuplas con nombre).

customer1 = Customer(name="Pablo", email="pablo@empresa.com",
                     phone="123-456-789")
customer2 = Customer(name="Pablo", email="pablo@empresa.com",
                     phone="123-456-789")
print(customer1 == customer2)  # True

Podemos, incluso, indicar valores por defecto y validadores sin siquiera crear el método __init__():

@attr.s
class Customer:
    name = attr.ib()
    email = attr.ib(default=None)
    phone = 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, usando attrs convertimos fácilmente nuestra estructura a otras colecciones:

print(attr.asdict(customer))
print(attr.astuple(customer))

Este increíble módulo no se incluye en la librería estándar (aunque algo similar intenta conseguirse con el módulo estándar dataclasses, a partir de Python 3.7). Para instalarlo, simplemente ejecuta

pip install attrs

En resumen, cuando tu código te solicite una estructura, recuerda la siguiente tabla para saber por qué solución inclinarte.

  Rigidez y validaciones Versatilidad, rapidez y comparaciones Otras funcionalidades geniales
Clases convencionales No No
Diccionarios / namedtuple No No
attrs


Deja un comentario