Entre diccionarios y data clases

En python tenemos algunos tipos de datos  Ver Figura 1 conocidos como data structures estos tipos de datos son datos iterables que permiten guardar información y heredar algunas operaciones que  nos hacen la vida un poco más fácil, entre estos se destaca el tipo de dato dict que junto a data clases será el tema que trataré en este artículo

Figura 1: Tipo de datos en python obtenido de https://towardsdatascience.com/which-python-data-structure-should-you-use-fa1edd82946c

Diccionarios

Este tipo de dato es uno de los más comunes sobre todo cuando se trabaja con aplicaciones web debido al estándar json ya que para python un objeto json es un diccionario, este funciona utilizando el concepto de mapping con clave:valor un ejemplo podría verse a continuación

[

    {"price": 20000, "owner": "Patrick", "discount": 0.0, "brand":"Chevrolet"},
    {"price": 10000, "owner": "Andrew", "discount": 0.0, "brand":"Chevrolet"}


]
Archivo .json a cargar en python
import json


def load_json(location: str):
    cars = []
    with open(location, mode="r", encoding="utf-8") as file:
        cars = json.loads(file.read())
    return cars


print(load_json("cars.json"))

se ejecuta y se obtiene como resultado una lista de diccionarios

[{'price': 20000, 'owner': 'Patrick', 'discount': 0.0, 'brand': 'Chevrolet'}, {'price': 10000, 'owner': 'Andrew', 'discount': 0.0, 'brand': 'Chevrolet'}]

en el código anterior se puede apreciar cómo al hacer json.loads() esto convierte los objetos json a diccionarios y de esta manera se pueden tratar como datos en python

Tip #1

Probablemente al trabajar con dicts nos encontremos con una lista de dicts y en ella la necesidad de contar cuántos elementos existen dentro de la lista para ciertos keys, por ejemplo en el código anterior quisiera contar cuantos carros de la marca "Chevrolet" existen , para ello realizaremos el siguiente código

import json
import collections


def load_json(location: str):
    cars = []
    with open(location, mode="r", encoding="utf-8") as file:
        cars = json.loads(file.read())
    return cars


def count_car_by_key(cars: list[dict], key: str):
    return collections.Counter([car[key] for car in cars])


cars = load_json("cars.json")

print(count_car_by_key(cars, "brand"))

se ejecuta el código se obtiene el siguiente resultado

Counter({'Chevrolet': 2})

En el anterior código se utiliza el módulo Collections este módulo es especializado para dar soporte a diferentes colecciones entre ellos el diccionario brindando algunas funciones y métodos que seguramente trataré en otro artículo para este utilizaré la función counter que permite agrupar los por el valor almacenado en una key dando como resultado las marcas de sus carros y las cantidades que existen por marca

Tip #2

Los diccionarios en python se encuentran desordenados y si se van agregando mas atributos van quedando sin un orden específico, pero en algunas situaciones quisiéramos imprimir la clave:valor ordenados por la clave, para ello siguiendo el ejemplo de los cars el código sería el siguiente

import json
import collections


def load_json(location: str):
    cars = []
    with open(location, mode="r", encoding="utf-8") as file:
        cars = json.loads(file.read())
    return cars


def count_car_by_key(cars: list[dict], key: str):
    return collections.Counter([car[key] for car in cars])


def sort_dict(cars):
    cars_sorted = []
    for car in cars:
        car_sorted = {}
        for key, value in sorted(car.items(), key=lambda item: item[0]):
            car_sorted[key] = value
        cars_sorted.append(car_sorted)
    return cars_sorted


cars = load_json("cars.json")
print(sort_dict(cars))

ejecuto el resultado sería el siguiente

[{'price': 20000, 'owner': 'Patrick', 'discount': 0.0, 'brand': 'Chevrolet'}, {'price': 10000, 'owner': 'Andrew', 'discount': 0.0, 'brand': 'Chevrolet'}]
[{'brand': 'Chevrolet', 'discount': 0.0, 'owner': 'Patrick', 'price': 20000}, {'brand': 'Chevrolet', 'discount': 0.0, 'owner': 'Andrew', 'price': 10000}]

en el código anterior se utiliza una función lambda para devolver los  keys ordenados alfabéticamente, y utilizando la función items() que devuelve una tupla se guarda el key y su valor, de esta manera se ordena el diccionario sin perder sus valores. Algunos contras de usar dicts para trabajar es que este tipo de dato permite agregar datos de manera arbitraria es decir se puede llamar un key si no existe te lo crea, y esto puede suceder por error (escribir mal un key) aquí es en donde entran a jugar las clases y los objetos

Data clases

Es un decorador que a partir de la estructura de la clase que yo cree  genera unos métodos especiales a diferencia de la creación de clases normal, este escribe un código por ti y te permite evitar escribir líneas de código innecesario, además  este decorator facilita utilizar sugar syntax veamos un ejemplo en código

from dataclasses import dataclass, field
from pprint import pprint


@dataclass(order=True)
class Car:
    sort_index: int = field(init=False, repr=False)
    price: int
    owner: str
    discount: float = 0.1

    def __post_init__(self):
        self.sort_index = self.price


cars: list[Car] = []
cars.append(Car(price=20000, owner="Patrick", discount=0.0))
cars.append(Car(price=10000, owner="Andrew", discount=0.0))

pprint(sorted(cars))

Se ejecuta el código y se obtiene como resultado

[Car(price=10000, owner='Andrew', discount=0.0),
 Car(price=20000, owner='Patrick', discount=0.0)]

el código anterior utiliza el decorador @dataclass este recibe unos argumentos order=True que le dirá a dataclass que genere el metodo de comparacion para ello debe agregarse un atributo llamado sort_index el cual utilizando field se le indica que no lo pida cuando inicializa una clase y también le indica repr=False para que cuando se imprima un objeto no lo muestre como atributo, finalmente agrego el método post_init que es llamado después de crear el objeto y se utiliza para decirle que valor tendrá sort_index en este caso el precio, por lo tal ordena una lista de objeto Car utilizando el precio

Tip #1

Como mencione para python los objetos en JSON son diccionarios esto es útil hasta cierto punto pero a veces se requiere convertir este tipo de dato a objetos por diferentes razones para ello lo hará utilizando el siguiente código

from dataclasses import dataclass, field
from pprint import pprint


@dataclass(order=True)
class Car:
    sort_index: int = field(init=False, repr=False)
    price: int
    owner: str
    discount: float = 0.1

    def __post_init__(self):
        self.sort_index = self.price


cars = [
    {"price": 20000, "owner": "Patrick", "discount": 0.0},
    {"price": 10000, "owner": "Andrew", "discount": 0.0},
]

cars = [Car(**car) for car in cars]

pprint(sorted(cars))

se ejecuta el código y se obtiene como resultado

[Car(price=10000, owner='Andrew', discount=0.0),
 Car(price=20000, owner='Patrick', discount=0.0)]

en el código anterior se utiliza  un código bastante pythonico aunque hay algunas situaciones que no me terminan de convencer sobre el idiomatic code ya que algunas veces el código no se entiende muy bien en este caso usamos list comprehension para crear una nueva lista recorriendo la lista de diccionarios y desempaquetando sus keys y pasandolos como valor a la clase, este permite crear un objeto de clase Car, al finalizar se imprime una lista de cars ordenados por precio. Anteriormente utilizamos data class que además también FastAPI hace poco permite en sus feature trabajar con dataclass aunque internamente siga usando pydantic el programador tendría la opción de usar este decorator

Tip # 2

pydantic ya lo he mencionado en varios de los artículos que he escrito, pero esta herramienta puede ser un complemento perfecto para trabajar con clases de forma pythonica veamos el mismo ejemplo de cars pero utilizando pydantic

from pydantic import BaseModel


class Car(BaseModel):
    price: int
    owner: str
    discount: float = 0.1

    def __lt__(self, other):
        if other.__class__ is self.__class__:
            return self.price < other.price


cars = [
    {"price": 20000, "owner": "Patrick", "discount": 0.0},
    {"price": 10000, "owner": "Andrew", "discount": 0.0},
]

cars = [Car(**car) for car in cars]

print(sorted(cars))

se ejecuta y se obtiene como resultado

[Car(price=10000, owner='Andrew', discount=0.0), Car(price=20000, owner='Patrick', discount=0.0)]

a pesar de que pydantic tiene una gran cantidad de validaciones relacionadas a los tipos de datos hay algunos metodos que aun tendremos que agregar en este caso dataclass uno de los métodos que generaba era el método __lt__ el cual estos se llaman métodos mágicos y sorted() los utiliza para saber sobre qué atributo ordenar por tal motivo se sobreescribe este método para poder comparar dos objetos

Conclusión

Los diccionarios son un tipo de dato que actualmente en desarrollo web se utiliza mucho debido a que para python los objetos json se transforman a dict por eso es importante conocer sus métodos y funciones para trabajar de manera eficiente con ellos, sin embargo en varias situaciones este tipo de dato puede quedarse un poco corto en lo que se necesita, para ello se opta para trabajar con objetos y clases  las cuales son potenciadas con dataclass y pydantic que permiten hacer transformaciones y validaciones utilizando código pythonico