{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<p>\n",
    "<font size='5' face='Georgia, Arial'>IIC-2233 Apunte Programación Avanzada</font><br>\n",
    "<font size='1'> Modificado en 2019-1 al 2026-1 por Equipo Docente IIC2233. </font>\n",
    "</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Tabla de contenidos\n",
    "\n",
    "1. [Serialización de objetos](#Serialización-de-objetos)\n",
    "    1. [`pickle`](#pickle)\n",
    "        1. [Personalizar la serialización en `pickle`](#Personalizar-la-serialización-en-pickle)\n",
    "        2. [Personalizar la deserialización en `pickle`](#Personalizar-la-deserialización-en-pickle)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Serialización de objetos\n",
    "\n",
    "Continuando con lo que vimos la semana pasada sobre serialización. Python además incluye otra forma de serializar los datos a bytes.\n",
    "\n",
    "## `pickle`\n",
    "\n",
    "El módulo `pickle` de Python permite guardar y cargar casi cualquier objeto de Python. Este módulo ofrece los siguientes métodos:\n",
    "\n",
    "- `dumps`: serializa un objeto, es decir, lo **guarda** como una secuencia de bytes.\n",
    "- `loads`: deserializa un objeto serializado, es decir, **carga** un objeto a su estado original.\n",
    "\n",
    "Una vez que un objeto es serializado, este es persistente y está listo para volver a ser usado en el futuro por el mismo u otro programa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Resultado serialización: b'\\x80\\x04\\x95\\x13\\x00\\x00\\x00\\x00\\x00\\x00\\x00(\\x8c\\x01a\\x94K\\x01K\\x03\\x8c\\x04hola\\x94t\\x94.'\n",
      "Tipo de versión serializada: <class 'bytes'>\n",
      "Resultado deserialización: ('a', 1, 3, 'hola')\n",
      "\n",
      "Tupla original: ('a', 1, 3, 'hola')\n",
      "Tupla deserializada: ('a', 1, 3, 'hola')\n",
      "¿Las tuplas son iguales? True\n",
      "¿Las tuplas son el mismo objeto? False\n"
     ]
    }
   ],
   "source": [
    "import pickle\n",
    "\n",
    "\n",
    "tupla = (\"a\", 1, 3, \"hola\")\n",
    "tupla_serializada = pickle.dumps(tupla)\n",
    "\n",
    "print(f\"Resultado serialización: {tupla_serializada}\")\n",
    "print(f\"Tipo de versión serializada: {type(tupla_serializada)}\")\n",
    "\n",
    "tupla_deserializada = pickle.loads(tupla_serializada)\n",
    "print(f\"Resultado deserialización: {tupla_deserializada}\")\n",
    "\n",
    "print()\n",
    "print(f\"Tupla original: {tupla}\")\n",
    "print(f\"Tupla deserializada: {tupla_deserializada}\")\n",
    "print(f\"¿Las tuplas son iguales? {tupla == tupla_deserializada}\")\n",
    "print(f\"¿Las tuplas son el mismo objeto? {tupla is tupla_deserializada}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Además, `pickle` nos ofrece los métodos `dump` y `load` (casi el mismo nombre que los anteriores, pero sin la *s*). Estos métodos también serializan y deserializan, pero **a través de archivos**: \n",
    "\n",
    "- `dump`: guarda un archivo con el objeto serializado.\n",
    "- `load`: deserializa un objeto almacenado en un archivo.\n",
    "\n",
    "Nótese, acá como estamos trabajando con bytes en vez de _strings_, los archivos debemos abrirlos en modo binario (`b`) en vez de texto (que es el modo por defecto)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Lista original: [1, 2, 3, 7, 8, 3]\n",
      "Lista cargada : [1, 2, 3, 7, 8, 3]\n",
      "¿Las listas son iguales? True\n",
      "¿Las listas son el mismo objeto? False\n"
     ]
    }
   ],
   "source": [
    "from os import path\n",
    "\n",
    "lista = [1, 2, 3, 7, 8, 3]\n",
    "\n",
    "with open(path.join(\"data\", \"mi_lista.bin\"), \"wb\") as file:\n",
    "    pickle.dump(lista, file)\n",
    "\n",
    "with open(path.join(\"data\", \"mi_lista.bin\"), \"rb\") as file:\n",
    "    lista_cargada = pickle.load(file)\n",
    "\n",
    "print(f\"Lista original: {lista}\")\n",
    "print(f\"Lista cargada : {lista_cargada}\")\n",
    "print(f\"¿Las listas son iguales? {lista == lista_cargada}\")\n",
    "print(f\"¿Las listas son el mismo objeto? {lista is lista_cargada}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importante\n",
    "`pickle` es un módulo no seguro. Esto significa que **nunca** debes cargar un archivo *pickle* cuando no conoces su procedencia, ya que éste podría ejecutar código malicioso en tu computador. No entraremos en detalles sobre cómo inyectar código a través del módulo `pickle`, pero si te interesa puedes revisar este [enlace](https://checkoway.net/musings/pickle/) donde se demuestra su uso malicioso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Personalizar la serialización en `pickle`\n",
    "\n",
    "Cuando `pickle` trata de serializar un objeto, lo primero que hará es verificar que el objeto que se quiere serializar sea de una clase que tenga implementado el método `__getstate__`. Este método debe retornar un diccionario con los atributos que se quieren serializar. Si `__getstate__` no estuviese implementado, entonces `pickle` guardará el atributo `__dict__` del objeto. \n",
    "\n",
    "El atributo `__dict__` es un diccionario que guarda todos los atributos y métodos de un objeto. En otras palabras, `objeto.atributo` es equivalente a `objeto.__dict__[\"atributo\"]` y `objeto.atributo = 42` es equivalente a `objeto.__dict__[\"atributo\"] = 42`.\n",
    "\n",
    "El implementar el método `__getstate__` nos permite personalizar la serialización del objeto. Usando este método podemos crear un diccionario que contenga solo la información que deseamos guardar. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Persona:\n",
    "\n",
    "    def __init__(self, nombre: str, edad: int) -> None:\n",
    "        self.nombre = nombre\n",
    "        self.edad = edad\n",
    "        self.mensaje = \"No pasa nada\"\n",
    "\n",
    "    def __getstate__(self) -> dict:\n",
    "        \"\"\"\n",
    "        Retorna el estado actual del objeto, para que sea serializado por pickle\n",
    "\n",
    "        Aquí creamos una copia del diccionario actual, para modificar la copia \n",
    "        y no el objeto original\n",
    "        \"\"\"\n",
    "        # Usamos una copia del diccionario original para alterar solo la copia\n",
    "        nueva = self.__dict__.copy()\n",
    "        # Modificamos un atributo en el objeto serializado.\n",
    "        # Sin embargo, el objeto original no ha cambiado.\n",
    "        nueva.update({\"mensaje\": \"¡Me están serializando!\"})\n",
    "        # Lo que retornemos es lo que será serializado por pickle\n",
    "        return nueva\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Mensaje original: No pasa nada\n",
      "Mensaje original: No pasa nada\n",
      "Mensaje deserializado: ¡Me están serializando!\n"
     ]
    }
   ],
   "source": [
    "original = Persona(\"Juan\", 30)\n",
    "print(f\"Mensaje original: {original.mensaje}\")\n",
    "serializado = pickle.dumps(original)\n",
    "deserializado = pickle.loads(serializado)\n",
    "\n",
    "# El objeto original sigue igual\n",
    "print(f\"Mensaje original: {original.mensaje}\")\n",
    "print(f\"Mensaje deserializado: {deserializado.mensaje}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Personalizar la deserialización en `pickle`\n",
    "\n",
    "De forma análoga, podemos personalizar la **deserialización**. Para esto debemos implementar el método `__setstate__`, que se ejecutará cada vez que llamemos a `load` o `loads`. El método `__setstate__` recibe como argumento el diccionario que representa el estado del objeto que fue serializado. Luego debe asignarlo al diccionario del objeto `self.__dict__ = diccionario_con_estado`. Esto no impide que se realicen otras acciones que modifiquen `diccionario_con_estado` antes o después de la asignación.\n",
    "\n",
    "Si el método `__setstate__` no estuviese implementado, entonces se asignará al `__dict__` del objeto el estado deserializado sin realizar otras acciones adicionales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Persona:\n",
    "\n",
    "    def __init__(self, nombre: str, edad: int) -> None:\n",
    "        self.nombre = nombre\n",
    "        self.edad = edad\n",
    "        self.mensaje = \"No pasa nada\"\n",
    "\n",
    "    def __getstate__(self) -> dict:\n",
    "        nueva = self.__dict__.copy()\n",
    "        print(f\"[__getstate__] Serializando a {nueva['nombre']}\")\n",
    "        nueva.update({\"mensaje\": \"¡Me están serializando!\"})\n",
    "        # Lo que retornemos es lo que será serializado por pickle\n",
    "        return nueva\n",
    "\n",
    "    def __setstate__(self, state) -> None:\n",
    "        print(\"[__setstate__] Objeto recién deserializado, actualizando su estado\")\n",
    "        # Al desarializar modificamos el estado\n",
    "        state.update({\"nombre\": f\"{state['nombre']} deserializado\"})\n",
    "        self.__dict__ = state\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Nombre original: Juan\n",
      "[__getstate__] Serializando a Juan\n"
     ]
    }
   ],
   "source": [
    "original = Persona(\"Juan\", 30)\n",
    "print(f\"Nombre original: {original.nombre}\")\n",
    "# Al usar pickle.dumps() se ejecuta el método __getstate__\n",
    "serializado = pickle.dumps(original)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "¡Ejecutar loads → deserializar!\n",
      "[__setstate__] Objeto recién deserializado, actualizando su estado\n",
      "Nombre deserializado: Juan deserializado, y su mensaje: ¡Me están serializando!\n"
     ]
    }
   ],
   "source": [
    "print(\"¡Ejecutar loads → deserializar!\")\n",
    "# Al usar pickle.loads() se ejecuta el método __setstate__\n",
    "deserializado = pickle.loads(serializado)\n",
    "print(f\"Nombre deserializado: {deserializado.nombre}, y su mensaje: {deserializado.mensaje}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una aplicación de los métodos `__getstate__` y `__setstate__` es cuando necesitamos serializar un objeto que contiene un atributo que depende de las condiciones actuales del programa.\n",
    "\n",
    "Por ejemplo, imaginemos que un objeto que guarda información sobre los usuarios conectados actualmente al programa, como la cantidad de usuarios y la información correspondiente a la conexión con cada uno. Cuando guardamos el objeto, deberíamos eliminar estas conexiones, ya que al cargarlo en otra instancia del programa no deberíamos poder comunicarnos con los usuarios de la instancia anterior. Para lograr esto usamos el método `__getstate__`. \n",
    "\n",
    "Similarmente, cuando se cargue el mismo objeto desde el archivo serializado, será necesario volver a crear las conexiones con las condiciones del programa nuevo. Para realizar esto tendremos que implementar `__setstate__`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
