Mostrando entradas con la etiqueta Pickle. Mostrar todas las entradas
Mostrando entradas con la etiqueta Pickle. Mostrar todas las entradas

lunes, 3 de junio de 2024

Redes con Python: Programación de Sockets para la Comunicación. 5.- Parsing_serialización de datos.

Vamos a introducir en este post dos conceptos:

Parsing (análisis) y Serialización.

El parsing o análisis es el proceso de convertir los datos nuevamente en sus componentes. Toma un flujo de datos (estructurados) y luego hace algo basado en el contenido de ese flujo, generalmente creando una estructura en memoria basada en ella o ejecutando una secuencia de operaciones basada en ella.

La serialización es el proceso de convertir una estructura de datos en un conjunto de bytes, es decir, un formato que pueda almacenarse o transmitirse de forma segura y recrearse a partir del formato almacenado o transmitido. Generalmente se usa para referirse a la aplicación de este proceso a estructuras de datos complejas como árboles, objetos o gráficos, ya que no se prestan inherentemente a ser transmitidos como una simple cadena de datos binarios. Convertir un objeto en memoria a JSON o XML es un ejemplo de serialización.

Vamos a ver esta idea mejor con un ejemplo práctico.

Ponte en la piel de un programador que tiene que encontrar una solución al siguiente problema. Necesita comprobar todos los ordenadores de una red para asegurarse de que tienen instalada la última versión de Python.

Para ello va a crear un servidor y clientes en cada unos de los equipos. Cada cliente debería enviar la siguiente información al servidor:

- La fecha actual.

- El nombre de red del cliente.

- La versión de Python que el cliente está usando.

Con lo que ya conocemos sobre como crear un cliente usando el protocolo TCP/IP vamos a crear este programa de una forma muy básica. Crea un archivo, yo lo llamará cliente.py y añade el siguiente código:

cliente.py

import socket
from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual = time()
nombre_red = node()
version_python = version

print("fecha_actual:", ctime(fecha_actual))
print ('Descripción:', node()) 
print(version_python)

Salida:

fecha_actual: Mon Jun  3 17:48:08 2024
Descripción: machine
3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]


import socket.

Módulo para crear la conexión.

from time import time, ctime. 

La función time nos facilita la fecha actual en formato de tiempo Unix. Como queremos verlo más humanizado utilizamos el modulo ctime que nos trasforma el tiempo Unix en la fecha actual más legible.

from platform import node.

Con este módulo, podremos acceder a los datos de nuestro sistema tales como: el hardware, el sistema operativo, etc. utilizamos node para obtener simplemente el nombre de red de la máquina.

import pickle.

Librería para serializar datos es decir convertir datos en bytes o viceversa que pueden ser guardados o enviados.

from sys import version.

Con este módulo obtendremos la versión actual de Python en el sistema.

Ya tenemos todo lo necesario que el cliente tiene que enviar al servidor. Tenemos todos los datos en formato Str (texto); si hubiera algo en formato numérico, por ejemplo, habría que pasarlo a texto.

Vamos a prepararlo para enviar estos datos al servidor. Tenemos que crear el mensaje y como siempre codificarlo para poderlo enviar. Finalmente implantamos el código para conectarnos al servidor y enviar el mensaje.

cliente.py

from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual = time()
nombre_red = node()
version_python = version

print("fecha_actual:", ctime(fecha_actual))
print ('Descripción:', node()) 
print(version_python)

from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual_unix = time()

fecha_actual = ctime(fecha_actual_unix)
nombre_red = node()
version_python = version

print("fecha_actual:", fecha_actual)
print ('Descripción:', node()) 
print(version_python)

mensaje = fecha_actual + "\n" + nombre_red + "\n" + version_python
data = mensaje.encode()

HOST = "127.0.0.1"  # El nombre del servidor o IP del mismo
PORT = 8081  # El puerto usado por el servidor

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.send(data)


Por otra parte tenemos que crear el código para que el servidor reciba cada una de las lecturas que le irán enviando los clientes. Un código muy sencillo.


server.py

import socket

HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 8081 # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    # Vincula el socket a una dirección y puerto específicos
    s.bind((HOST, PORT))

    # Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
    s.listen()

    print("Servidor escuchando en el puerto 8081...")

    # Aceptar una conexión
    client_socket, client_address = s.accept()
    with client_socket:
        print(f"Conexión aceptada de {client_address}")
        
        mensaje = "Hola,¡Gracias por conectarte!"
        data = client_socket.recv(1024)
        mensaje = data.decode()
        print(mensaje)

El código anterior debería decodificar los bytes recibidos del cliente y realizar un parsing del mensaje para transformarlo en sus datos originales. Incluso para un mensaje bastante simple como el nuestro el proceso de traspasar los bytes en sus datos originales es bastante farragoso. Es relativamente lento de ejecutar, difícil de cambiar y propenso a que se comentan errores.

Por eso podemos usar la serialización. Como dijimos al principio del post la serialización es el proceso de convertir datos en una serie de bytes para que puedan ser guardados o transmitidos. El ejemplo anterior es una forma simple de serialización. Para hacer la serialización más fácil se utiliza la librería pickle en Python.

Por ejemplo para enviar el mensaje podemos modificar el cliente. Utilizamos una tupla de Python para guardar la información y enviarla con Pickle, nos da igual cual sea el tipo de datos a enviar. Puede ser cualquier cosa una tupla, una lista, un diccionario etc.

cliente.py

import socket
from time import time, ctime
from platform import node
import pickle
from sys import version

fecha_actual_unix = time()

fecha_actual = ctime(fecha_actual_unix)
nombre_red = node()
version_python = version

print("fecha_actual:", fecha_actual)
print ('Descripción:', node()) 
print(version_python)

# mensaje = fecha_actual + "\n" + nombre_red + "\n" + version_python
# data = mensaje.encode()
mensaje = (fecha_actual, nombre_red, version_python)
# pickle.dumps((mensaje) convierte el mesaje en bytes sea cual sea el tipo de datos.
data = pickle.dumps(mensaje)

HOST = "127.0.0.1"  # El nombre del servidor o IP del mismo
PORT = 8081  # El puerto usado por el servidor

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.sendall(data)
y lo mismo para esta versión muy sencilla del servidor creado.

server.py

import socket
import pickle

HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 8081 # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:

    # Vincula el socket a una dirección y puerto específicos
    s.bind((HOST, PORT))

    # Escucha conexiones entrantes (el argumento especifica el tamaño máximo de la cola de conexiones)
    s.listen()

    print("Servidor escuchando en el puerto 8081...")

    # Aceptar una conexión
    client_socket, client_address = s.accept()
    with client_socket:
        print(f"Conexión aceptada de {client_address}")
        
        mensaje = "Hola,¡Gracias por conectarte!"
        data = client_socket.recv(1024)
#         mensaje = data.decode()
        mensaje = pickle.loads(data)
        # hace lo contrario, convierte bytes a sus originales.
        print(mensaje)


Puedes encontrar el código de este post en este enlace de github.

En el siguiente post veremos. Protocolo UDP y diferencias con TCP.

domingo, 2 de junio de 2024

Redes con Python: Programación de Sockets para la Comunicación. 4.- Manejando múltiples conexiones

El modelo que hemos usado hasta ahora tiene sus limitaciones. El mayor es que el servidor atiende solamente a un cliente y luego finaliza la conexión. Para solventarlo usaremos una biblioteca de Python bastante similar, socketserver. Es muy útil para manejar sockets y nos ayudará a manejar múltiples conexiones. 

Vamos a verlo a través de un ejemplo. Crearemos un programa de preguntas y respuestas que se pueda jugar solo o bien entre dos jugadores. El servidor será el que envíe las preguntas y luego compruebe las respuestas de los clientes. Los clientes serán los jugadores.

Empecemos.

Vamos a empezar a crear el servidor importando las librerías que necesitaremos.

quiz_server.py

import socketserver
from collections import namedtuple
from random import choice
from threading import Event
import pickle

1.- socketserver. Es la librería que nos va a proporcionar la infraestructura básica para usar los servidores de red.  En el programa lo utilizaremos para crear un TCP multihilo que maneje las conexiones de los clientes (los jugadores)

2.- from collections import namedtuple. Es una extensión para poder utilizar las tuplas y acceder a sus elementos por su nombre, en vez de por su posición en la tupla.

3.- from random import choice. Proporciona funciones para generar números pseudo-aleatorios y seleccionar elementos aleatorios de una secuencia.

4.- from threading import Event. Con el crearemos diferentes hilos para cada cliente que se conecte. Utilizaremos instancias de Event para sincronizar el estado entre los diferentes hilos. Lo haremos explicando a medida que desarrollemos el programa.

5.- pickle. Se utiliza para serializar y deserializar datos en Python a una cadena de bytes que pueda ser enviada a través de sockets.

A continuación vamos a especificar los comandos que se podrán enviar entre servidor-cliente (luego veremos para que se usan) e iniciaremos las variables que vamos a necesitar posteriormente.

quiz_server.py

#...

''' Comandos: COLOCA TUS COMANDOS AQUÍ QUES - comando de pregunta UNIR - solicitud de unirse al principio 1 - Se utiliza para enviar una pregunta al cliente después de una solicitud 2 - Se utiliza para enviar "Espera el evento listo para empezar" al cliente. 4 - Se utiliza para enviar una respuesta al servidor después de una pregunta 7 - Se utiliza para enviar una respuesta "Correcta" o "Incorrecta" después de una respuesta

'''

NUMERO_DE_JUGADORES = 2 jugadores = [] listo_para_empezar = Event() contestadas = 0 esperando_respuestas = Event() pregunta_Actual = None Pregunta = namedtuple('Pregunta', ['p', 'respuesta'])
Para tener un buen concurso de preguntas y respuestas tendremos que crear unas cuantas. Puedes poner las preguntas y las respuestas que te parezcan. Yo crearé unas pocas para que el programa sea funcional.

quiz_server.py

#...
juego_Preguntas = [
    Pregunta("¿Como se llama un polígono de tres lados?", "Triángulo"),
    Pregunta("¿Cuál es el quinto planeta del sistema solar?", "Jupiter"),
    Pregunta("¿Cuál es la capital de Francia?", "Paris"),
    Pregunta("¿Quién dijo la frase: 'Solo se que no se nada'?", "Sócrates"),
    Pregunta("¿Cuál es el antónimo de rico?", "Pobre")
]
Vamos a detenernos un momento para ver el funcionamiento de namedtuple.

Podríamos haber guardado las preguntas y respuestas como una tupla directamente dentro de una lista, es decir del siguiente modo:

juego_Preguntas = [(pregunta, respuesta), (pregunta, respuesta),....(pregunta, respuesta)]

Si son pocas preguntas podríamos manejarlo pero si fueran 1000 preguntas la cosa se nos complicaría, ya que nos tendríamos que acordar de que la posición 0 de la tupla es la pregunta y la posición 1 es la respuesta.  Sin embargo al usar nametupled podemos acceder a los elementos por su nombre:

Pregunta.p = La pregunta en cuestión
Pregunta.respuesta = la respuesta a la pregunta.

Por ejemplo la primera de la lista es el elemento cero. Si añades un print con el siguiente código verás:

quiz_server.py

#...
juego_Preguntas = [
    Pregunta("¿Como se llama un polígono de tres lados?", "Triángulo"),
    Pregunta("¿Cuál es el quinto planeta del sistema solar?", "Jupiter"),
    Pregunta("¿Cuál es la capital de Francia?", "Paris"),
    Pregunta("¿Quién dijo la frase: 'Solo se que no se nada'?", "Socrates"),
    Pregunta("¿Cual es el antónimo de rico?", "Pobre")
]

print(f"La pregunta es {juego_Preguntas[0].p} y su respuesta: {juego_Preguntas[0].respuesta}")
print("\n")
print(juego_Preguntas)

La salida será algo como esto:

>>> %Run 
La pregunta es ¿Como se llama un polígono de tres lados? y su respuesta: Triángulo


[Pregunta(p='¿Como se llama un polígono de tres lados?', 
respuesta='Triángulo'), Pregunta(p='¿Cuál es el quinto planeta del sistema solar?', respuesta='Jupiter'), 
Pregunta(p='¿Cuál es la capital de Francia?', respuesta='Paris'), 
Pregunta(p="¿Quién dijo la frase: 'Solo se que no se nada'?", respuesta='Socrates'), 
Pregunta(p='¿Cuál es el antónimo de rico?', respuesta='Pobre')]

Para conocer más sobre como funciona las tuplas con nombre puedes acceder a su documentación en Python.

Ahora, comenta o elimina los print que hemos utilizado anteriormente (los que están en negrita arriba) para ver el funcionamiento de las namedtuple y sigamos con el desarrollo del ejemplo.


Crear un servidor multijugador en Python. Entendiento ThreadedTCPServer.

¿Te has preguntado cómo funcionan los servidores que permiten a múltiples personas conectarse al mismo tiempo, como en los juegos en línea o las aplicaciones de chat? Vamos a explorar cómo crear un servidor básico en Python que pueda manejar varias conexiones al mismo tiempo, usando algo llamado ThreadedTCPServer. Nos servirá para que los jugadores de nuestro juego se puedan conectar de forma simultánea.

¿Qué es un Servidor?

Un servidor es como un anfitrión de una fiesta. Imagina que organizas una fiesta en tu casa. Las personas (los clientes) llegan a tu puerta y tú las recibes, hablas con ellas, y las dejas entrar. En el mundo de la informática, un servidor hace algo similar: recibe conexiones de otros programas (los clientes) y les envía y recibe información. En nuestro programa les enviará las preguntas y los clientes, los jugadores, le enviarán sus respuestas.

¿Qué es ThreadedTCPServer?

ThreadedTCPServer es una forma especial de crear un servidor en Python que puede hablar con muchos clientes al mismo tiempo. Sin ThreadedTCPServer, nuestro servidor solo podría hablar con una persona a la vez, lo cual no sería muy eficiente para una fiesta concurrida.

Vamos a desglosar ThreadedTCPServer y ver cómo funciona:

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

Aquí estamos creando una nueva clase llamada ThreadedTCPServer. Esta clase combina dos cosas:

  1. socketserver.ThreadingMixIn: Esto le da al servidor la capacidad de manejar cada conexión en un hilo separado. Piensa en un hilo como un asistente personal. Si tienes muchos asistentes, puedes hablar con muchos invitados a la vez sin tener que esperar a que termines de hablar con cada uno.

  2. socketserver.TCPServer: Esto le da al servidor la habilidad básica de recibir conexiones de clientes y hablar con ellos usando un protocolo llamado TCP (es como el lenguaje que usan para comunicarse).

Cuando combinamos estas dos cosas, tenemos un servidor que puede hablar con muchos clientes al mismo tiempo, de manera eficiente.

¿Cómo Usamos ThreadedTCPServer?

Vamos a ver cómo ponemos todo esto en acción. Olvidaté de momento del código anterior. Aquí vamos a ver  un pequeño programa que crea un servidor usando ThreadedTCPServer. 

import socketserver
from threading import Event

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

class QuizGame(socketserver.BaseRequestHandler):
    def handle(self):
        self.request.sendall(b"Hello, world")

if __name__ == "__main__":
    HOST, PORT = "localhost", 2065
    with ThreadedTCPServer((HOST, PORT), QuizGame) as server:
        print(f"Server running on {HOST}:{PORT}")
        # Hace que el servidor funcione de forma indefinida
        server.serve_forever() 

¿Qué Hace Este Programa?

  1. Creamos ThreadedTCPServer: Esto define nuestro servidor que puede manejar múltiples conexiones al mismo tiempo.
  2. Definimos QuizGame: Esta es la parte del servidor que se encarga de lo que sucede cuando alguien se conecta. En este caso, solo envía un mensaje "Hello, world".
  3. Configuramos y Ejecutamos el Servidor: Le decimos a nuestro servidor que escuche en "localhost" (tu propia computadora) en el puerto 2065. Luego, lo ponemos a funcionar indefinidamente para que esté siempre listo para recibir conexiones.

Cuando ejecutas este programa, puedes conectar múltiples clientes (otros programas) al servidor, y todos recibirán el mensaje "Hello, world" sin tener que esperar uno por uno. Puedes abrir en tu navegador varias pesatañas y conectarte a "localhost:2065" para ver como funciona el servidor.



servidor multihilo en funcionamiento


Como te habrás dado cuenta en post anteriores para enviar los datos utilizábamos el método send. Sin embargo en esta ocasión hemos utilizado sendall. ¿En que se diferencian?

La diferencia principal entre sendall y send en Python radica en cómo envían los datos a través de un socket y cómo manejan el tamaño de los datos enviados. Aquí te explico cada uno:

send

  • Descripción: send es un método del objeto socket que envía datos a través del socket. Sin embargo, no garantiza que todos los datos sean enviados en una sola llamada. Puede enviar solo una parte de los datos, especialmente si la cantidad de datos es grande o si la red está congestionada.
  • Uso: Útil cuando se está dispuesto a manejar la lógica para asegurarse de que todos los datos se envíen completamente.
  • Retorno: Devuelve el número de bytes efectivamente enviados. Es posible que necesites llamar a send repetidamente para enviar todos los datos.

sendall

  • Descripción: sendall es una versión más conveniente de send que intenta enviar todos los datos en una sola llamada. Internamente, sendall usa un bucle para llamar a send repetidamente hasta que todos los datos hayan sido enviados o se produzca un error.
  • Uso: Ideal cuando quieres asegurarte de que todos los datos se envíen completamente sin tener que manejar la lógica de reintentos manualmente.
  • Retorno: No devuelve nada si tiene éxito. Si ocurre un error, se lanza una excepción.

  • send es más flexible y te da más control, pero necesitas manejar la lógica para asegurarte de que todos los datos se envíen.
  • sendall es más conveniente cuando quieres asegurarte de que todo el mensaje se envíe sin tener que manejar manualmente los reintentos.

  • Después de las explicaciones vamos a ver como implementar esto en nuestro programa de preguntas y respuestas. Volvemos al código que teníamos. Para centrarnos un poco voy a volver a ponerlo entero:

    quiz_server.py

    import socketserver
    from collections import namedtuple
    from random import choice
    from threading import Event
    import pickle
    
    '''
    Comandos:
    COLOCA TUS COMANDOS AQUÍ
    QUES - comando de pregunta
    UNIR - solicitud de unirse al principio
    1 - Se utiliza para enviar una pregunta al cliente después de una solicitud
    2 - Se utiliza para enviar "Espera el evento listo para empezar" al cliente.
    4 - Se utiliza para enviar una respuesta al servidor después de una pregunta
    7 - Se utiliza para enviar una respuesta "Correcta" o "Incorrecta" después de una respuesta
    '''
    
    NUMERO_DE_JUGADORES = 2
    jugadores = []
    listo_para_empezar = Event()
    contestadas = 0
    esperando_respuestas = Event()
    pregunta_Actual = None
    Pregunta = namedtuple('Pregunta', ['p', 'respuesta'])
    # Pregunta = namedtuple('Pregunta', 'p respuesta')
    juego_Preguntas = [
        Pregunta("¿Como se llama un polígono de tres lados?", "Triángulo"),
        Pregunta("¿Cuál es el quinto planeta del sistema solar?", "Jupiter"),
        Pregunta("¿Cuál es la capital de Francia?", "Paris"),
        Pregunta("¿Quién dijo la frase: 'Solo se que no se nada'?", "Socrates"),
        Pregunta("¿Cuál es el antónimo de rico?", "Pobre")
    ]
    
    Ahora aplicaremos lo hemos visto anteriormente que es la base para crear nuestro servidor. Crearemos el servidor, crearemos una clase que indicará al programa que hacer cuando el cliente se conecte y finalmente haremos un punto para ejecutar el programa. Vamos a ello. Añade el siguiente código al final del anterior.

    quiz_server.py

    #...
    # Creamos el servidor multihilo
    class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
        pass
    
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        pass
    
    # Punto de entrada al programa.
    if __name__ == "__main__":
        HOST, PORT = "localhost", 2065
        with ThreadedTCPServer((HOST, PORT), QuizGame) as server:
            print(f"Servidor ejecutándose en {HOST}:{PORT}")
            server.serve_forever()
    
    
    Dentro de la clase QuizGame vamos a crear un módulo llamado "handle". El modulo "socketserver"  usará estos "handler" para interactuar con las conexiones que se realicen. Cuando un cliente se conecta, se crea una versión de esta clase para manejarlo. 

    Por otra parte también crearemos un módulo llamado "enviar_mensaje" que enviará un determinado mensaje desde el servidor al cliente.

    Actualicemos el código:

    quiz_server.py

    #...
    # Creamos el servidor multihilo
    class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
        pass
    
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        def handle(self):
            pass
    
        def enviar_mensaje(self, mensaje):
            pass
    
    # Punto de entrada al programa.
    if __name__ == "__main__":
        HOST, PORT = "localhost", 2065
        with ThreadedTCPServer((HOST, PORT), QuizGame) as server:
            print(f"Servidor ejecutándose en {HOST}:{PORT}")
            server.serve_forever()
    

    Módulo enviar_mensaje.

    Para enviar los mensajes a los clientes vamos a utilizar el modulo pickle de Python. Pickle nos permite convertir objetos Python a una cadena de bytes y viceversa. Definiremos el método enviar_mensaje dentro de la clase QuizGame para encapsular la lógica de serialización y envio de datos.

    Si utilizamos self.request.sendall(pickle.dumps(mensaje)) convertiremos el mensaje en bytes y lo enviaremos al cliente. Vamos a implementarlo.

    quiz_server.py

    #...
    # Creamos el servidor multihilo
    class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
        pass
    
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        def handle(self):
            pass
    
        def enviar_mensaje(self, mensaje):
            self.request.sendall(pickle.dumps(mensaje))
    
    # Punto de entrada al programa.
    if __name__ == "__main__":
        HOST, PORT = "localhost", 2065
        with ThreadedTCPServer((HOST, PORT), QuizGame) as server:
            print(f"Servidor ejecutándose en {HOST}:{PORT}")
            server.serve_forever()

    Ya nos queda solo el grueso del programa, que codificaremos a través de "handle".  Es importante que mantengas el nombre de este módulo tal como está "handle". Eso es así porque es el módulo predefinido que es invocado automáticamente cuando recibe una conexión del cliente.

    Manejo de Solicitudes del Cliente en el Servidor

    En esta parte del código, estamos viendo cómo el servidor maneja las solicitudes entrantes de los clientes. Vamos a ir añadiendo el código y desglosándolo paso a paso:

    1. Variables globales y Bucle Infinito

    quiz_server.py

    #...
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        def handle(self):
            global jugadores
            global contestadas
            global pregunta_Actual
    
            while True:
                pass
    
        def enviar_mensaje(self, mensaje):
           # ...
    Como estás variables van a ser usadas por múltiples clientes nos aseguraremos de que sean las mismas para todos y por ello las declararemos como globales.

    while True:

    Este bucle se ejecuta continuamente, lo que significa que el servidor estará siempre listo para recibir y manejar nuevas solicitudes de los clientes.

    2. Recepción de Datos del Cliente

    quiz_server.py

    #...
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        def handle(self):
            global jugadores
            global contestadas
            global pregunta_Actual
    
            while True:
                try:
                    data = self.request.recv(1024)
                    if not data:
                        break
                    request = pickle.loads(data)
                except EOFError:
                    break          
        def enviar_mensaje(self, mensaje):        # ...
    Aquí, el servidor espera recibir datos del cliente a través del socket de la conexión. `self.request.recv(1024)` recibe hasta 1024 bytes de datos del cliente. Si no se reciben datos o la conexión se cierra, el servidor sale del bucle con `break`. La función `pickle.loads()` se utiliza para deserializar los datos recibidos en un formato que el servidor pueda entender.

    3. Procesamiento de la Solicitud del Cliente

    Una vez que se recibe y deserializa la solicitud del cliente, el servidor procede a procesarla y tomar las acciones correspondientes según el contenido de la solicitud:

    quiz_server.py

    #...
    # Creamos la clase que especificará lo que se haga cuando se conecte
    # el cliente.
    class QuizGame(socketserver.BaseRequestHandler):
        def handle(self):
            global jugadores
            global contestadas
            global pregunta_Actual
    
            while True:
                try:
                    data = self.request.recv(1024)
                    if not data:
                        break
                    request = pickle.loads(data)
                except EOFError:
                    break
    if request[0] == "JOIN": nombre_equipo = request[1] jugadores.append(nombre_equipo) print(f'El Equipo {nombre_equipo} está conectado.')                 if len(jugadores) == NUMERO_DE_JUGADORES:                     # Si el número de jugadores es correcto listo_para_empezar.set()                     # activa el evento #Envia la respuesta de confirmación                 if listo_para_empezar.is_set() == False: mensaje = "[Server]...esperando que los otros jugadores se unan" else: mensaje = "[Server]...empezando" self.enviar_mensaje([2, mensaje])                 # Espera hasta que el evento este listo para comenzar listo_para_empezar.wait() if request[0] == "QUES": if pregunta_Actual == None: pregunta_Actual = choice(juego_Preguntas) esperando_respuestas.clear() self.enviar_mensaje((1, pregunta_Actual.p)) if request[0] == "4":                 # Envía si la respuesta es correcta o no if request[1] == pregunta_Actual.respuesta: self.enviar_mensaje((7, "Correcto")) else: self.enviar_mensaje((7, "Incorrecto")) contestadas += 1 if contestadas == len(jugadores): contestadas = 0 pregunta_Actual = None esperando_respuestas.set()                 esperando_respuestas.wait()          
        def enviar_mensaje(self, mensaje):        # ...


    - Si la solicitud es un comando `"JOIN"`, el servidor registra al equipo que se está uniendo, imprime un mensaje indicando que el equipo se ha conectado correctamente, y si el número de jugadores alcanza el número deseado (2 por defecto), activa un evento para indicar que el juego está listo para comenzar.
    - Si la solicitud es `"QUES"`, el servidor envía una pregunta al cliente.
    - Si la solicitud es `"4"`, el servidor verifica si la respuesta del cliente es correcta o incorrecta, actualiza el contador de respuestas y activa un evento si todas las respuestas han sido recibidas.

    Finalmente, después de enviar una pregunta al cliente o de recibir una respuesta, el servidor espera hasta que todas las respuestas de los jugadores hayan sido recibidas antes de continuar con el siguiente paso del juego.

    Este bloque de código es esencial para la comunicación bidireccional entre el servidor y los clientes en el juego, permitiendo que el servidor reciba y procese las acciones de los clientes de manera eficiente y oportuna.


    Los eventos (`Event`) en Python son una forma sencilla de coordinar la ejecución entre múltiples hilos. En el contexto de este programa, se utilizan dos eventos para sincronizar el flujo del juego entre el servidor y los clientes. Veamos cómo funcionan estos eventos:

    Eventos en Python

    - set(): Señala el evento, permitiendo que todos los hilos que están esperando en `wait()` continúen su ejecución.
    - clear(): Restablece el evento, haciendo que las llamadas a 'wait()' bloqueen nuevamente.
    - wait(): Bloquea el hilo actual hasta que el evento sea señalado.

    Uso de Eventos en el Programa

    1. 'listo_para_empezar'

    Este evento se utiliza para coordinar el inicio del juego. El juego no comenzará hasta que se hayan unido todos los jugadores necesarios.

    - listo_para_empezar.set(): Este método se llama cuando el número de jugadores alcanza el número requerido (`NUMERO_DE_JUGADORES`). Señala el evento, indicando que el juego está listo para empezar.
    - listo_para_empezar.wait(): Los hilos de los clientes llaman a este método para esperar hasta que el evento sea señalado, asegurándose de que todos los jugadores estén listos antes de comenzar.

    2. 'esperando_respuestas'

    Este evento se utiliza para sincronizar las respuestas de los jugadores a una pregunta.

    - esperando_respuestas.clear(): Este método se llama antes de enviar una nueva pregunta, restableciendo el evento y haciendo que las futuras llamadas a `wait()` bloqueen.
    - esperando_respuestas.set(): Este método se llama una vez que todas las respuestas han sido recibidas y procesadas. Señala el evento, permitiendo que los hilos de los clientes continúen.
    - esperando_respuestas.wait(): Los hilos de los clientes llaman a este método para esperar hasta que el evento sea señalado, asegurándose de que todas las respuestas hayan sido recibidas y procesadas antes de proceder.

    Ejemplo con Dos Clientes

    Supongamos que tenemos dos clientes (A y B) conectados al servidor. Aquí hay un flujo simplificado de cómo los eventos funcionan para coordinar el juego:

    1. **Inicio del Juego**:
        - Cliente A y Cliente B envían el comando `"JOIN"`.
        - El servidor agrega a ambos clientes a la lista `jugadores`.
        - Cuando el segundo cliente se une, el servidor llama a `listo_para_empezar.set()`.
        - Ambos clientes están esperando en `listo_para_empezar.wait()`, y ahora pueden continuar porque el evento ha sido señalado.

    2. **Enviar Pregunta**:
        - Un cliente envía el comando `"QUES"` para solicitar una pregunta.
        - El servidor elige una pregunta y la envía al cliente.
        - El servidor llama a `esperando_respuestas.clear()` antes de enviar la pregunta, asegurándose de que las futuras respuestas bloqueen.

    3. **Recibir Respuestas**:
        - Ambos clientes envían sus respuestas con el comando `"4"`.
        - El servidor recibe las respuestas y las procesa.
        - Cuando se reciben todas las respuestas, el servidor llama a `esperando_respuestas.set()`.
        - Los clientes que estaban esperando en `esperando_respuestas.wait()` ahora pueden continuar porque el evento ha sido señalado.

    4. **Repetir el Proceso**:
        - Este proceso se repite para cada pregunta, coordinando el flujo del juego entre el servidor y los clientes.

    Esta estructura asegura que el servidor y los clientes estén sincronizados, manteniendo el flujo del juego ordenado y manejando correctamente la concurrencia en un entorno multijugador.

    Una vez completado el código del servidor vamos con el del cliente.

    CLIENTE.

    quiz_client.py

    import socket
    import pickle
    
    jugando = True
    
    # Crear un socket TCP/IP
    servidor_preguntas = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Pedir al usuario que ingrese el nombre del equipo
    nombre_equipo = input("¿Cual es el nombre de tu equipo? >>> ")
    
    # Pedir al usuario que ingrese la dirección IP del servidor
    serverIP = input("Introduce la dirección IP (e.j. 127.0.0.1) a la que quieres conectarte o presiona ENTER para usar LOCALHOST: ")
    if serverIP == "":
        serverIP = "127.0.0.1"
    
    # Conectar con el servidor
    print(f"\n¡Bienvenido Equipo {nombre_equipo} al Gran Juego de las Preguntas!")
    servidor_preguntas.connect((serverIP, 2065))
    
    # Enviar un comando al servidor para unirse al juego
    servidor_preguntas.sendall(pickle.dumps(["JOIN", nombre_equipo]))
    
    while jugando:
        # Recibir datos del servidor
        response_data = servidor_preguntas.recv(1024)
        if not response_data:
            break
        response = pickle.loads(response_data)
        
        # Manejar las respuestas del servidor
        if response[0] == 1: # Respuesta de pregunta
            print(response[1])
            respuesta = input('Respuesta : ')
            servidor_preguntas.sendall(pickle.dumps(["4", respuesta]))
        elif response[0] == 2: # Esperando que todos los jugadores estén listos
            print(response[1])
            servidor_preguntas.sendall(pickle.dumps(["QUES", ""]))
        elif response[0] == 7: # Respuesta de correcto o incorrecto
            print(response[1])
            servidor_preguntas.sendall(pickle.dumps(["QUES", ""]))
        elif response[0] == 3: # Esperando que los otros jugadores respondan
            print("...esperandos las respuestas de los otros jugadores")
            servidor_preguntas.sendall(pickle.dumps(["QUES", ""]))
    
    servidor_preguntas.close()
    El código del cliente ya lo hemos visto previamente. Se crea un socket y se conecta al servidor. El funcionamiento posterior dependerá de las respuestas del servidor.

    Vamos a ver si todo funciona. Ejecuta el servidor y dos clientes. Sigue las instrucciones. Deberías ver algo similar a esto:


    imagen final del programa


    Con esto ya tendríamos visto el funcionamiento básico del servidor multihilo y los cliente. Falta bastante para completar el juego porque de momento se repetirían de forma infinita las preguntas, no se mostraría quien es el equipo ganador y bastantes cosas más, pero no es el objeto de este post. Si quieres puedes acabar de desarrollar la lógica del programa ya que lo básico que es la conexión y envío de datos ya está en funcionamiento.

    Puedes encontrar el código de este capítulo en este enlace de este enlace de Github.

    Próximo Post.


    martes, 22 de febrero de 2022

    Trabajar con archivos externos. Persistencia de datos.

    imagen de entrada conteniendo archivos


    En este post vamos a aprender como guardar nuestros datos en un archivo en el disco. Hasta ahora todo lo que hacíamos en Python se guardaba en memoria, pero si apagamos el equipo todo nuestro trabajo o datos se pierden. Así que vamos a aprender como conservar esos datos a través de un archivo de texto.


    Crear Archivos.

    Puede parecer poco lógico, pero para leer un archivo existente o crear uno nuevo, tienes que crear primero un objeto, al cual puedes llamar como quieras. Yo lo llamaré "file". Pero no te confundas, crear este objeto no es crear el archivo.

    Aquí tienes un ejemplo de código que abre un archivo (foo.txt) para lectura, para ir familiarizándonos con la forma.

    > file = open('foo.txt', 'r')
    > print(file.read())
    El resultado son las líneas de texto que contenga el archivo.

    Aquí haremos un inciso. Si intentas abrir para escritura un archivo que no existe, lógicamente Python te mostrará un error. Cuando la apertura de un archivo falla, el objeto de Exception IOerror tiene una propiedad llamada errno, que contiene el código de finalización de la operación fallida. Podemos usar este valor para diagnosticar el problema. 

    Veámoslo con un ejemplo. Asegúrate de que en el directorio en el que estar trabajando no haya ningún archivo llamado foo.txt. Ahora ejecuta este código:

    > >>> open('foo.txt','r')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'foo.txt'

    Para saber cual es el error número 2 podemos usar strerror del módulo os de esta manera.

    >>> print(strerror(2))
    No such file or directory
    
    El código elegante para capturar el error sería este:

    from os import strerror
    
    try:
        s = open("foo.txt", "rt")
        # El procesamiento va aquí.
        s.close()
    except Exception as exc:
        print("El archivo no pudo ser abierto:", strerror(exc.errno))

    Salida:

    El archivo no pudo ser abierto: No such file or directory

    Puedes encontrar más información sobre los errores de entrada/salida en https://docs.python.org/3.10/library/errno.html o importando el modulo errno y buscando en la ayuda (help(errno)).

    Dicho lo cual continuamos.

    Hemos creado el objeto "file" usando la función incorporada open(). La sintaxis global de open() es:

    open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

    Solamente el nombre del archivo es obligatorio, todos los demás son optativos. Vamos a centrarnos un poco en los valores que puede tener el segundo parámetro:


    Modo Descripción
    'r' Es la opción por defecto. Abre el archivo en modo lectura. (read)
    'w Abre un archivo para escribir en él. Ojo porque crea un nuevo archivo si no existe, pero si existe lo borra y remplaza su contenido. (write)
    'a' Abre el archivo para añadir contenido al final del mismo, pero sin borrar lo que ya tenga. (append). Si el archivo no existe crea uno nuevo.
    'x' Abre un archivo exclusivamente para crearlo. Si el archivo ya existe devuelve un error. 
     'b'  Modo binario o para trabajar con archivos binarios.
     't  Modo texto. (el que viene por defecto)
     '+'  Abre el archivo para actualizarlo(lectura y escritura) r+ 

    En cuanto al tercer parámetro encoding=None por defecto es utf.8. Como nota a mayores si quisiéramos saber el encoding por defecto del sistema con el que trabajamos (ascii, utf-8...) podemos usar el siguiente código de Python:

    > import locale
    > print(locale.getpreferredencoding())
    UTF-8


    Volviendo al ejemplo previo, hemos especificado que nos gustaría usar "open()" en el archivo "foo.txt" en modo lectura y poder hacer referencia  a ese nuevo objeto (de archivo de lectura) con la variable "file". Una vez que tenemos el objeto 'file' creado, podemos usar el método read() que lee todo el contenido del archivo.

    Crear un nuevo archivo para escritura es muy similar a la forma que hemos usado para la lectura. En lugar de usar el parámetro 'r', utilizamos una 'w'. Vamos a crear un nuevo archivo que no existe y grabar el texto "Hola Mundo". Ojo que si el nombre del archivo ya existiese y pusiéramos el mismo nombre en nuestro ejemplo el nuevo contenido lo guardaría encima, borrando el texto que ya tuviésemos.

    > file = open('nuevo.txt', 'w')
    > file.write('Hola Mundo')
    10
    > file.close()

    nota: la función write() devuelve el número de bytes escritos, en nuestro caso se corresponde con 10 caracteres.

    En este ejemplo, especificamos que nos gustaría usar open() en el archivo "nuevo.txt" en modo de escritura y poder referirnos a ese nuevo objeto de archivo en el que se puede escribir con la variable "file".  Una vez que tenemos el objeto "file", podemos write() "escribir" algo de texto y close() "cerrar" el archivo.

    Siempre tenemos que cerrar los archivos abiertos para que no se corrompan. Esto lo podemos hacer de dos maneras.

    La primera de ellas es rodear el comando open() con un bloque try/finally, especialmente cuando escribamos datos al archivo con la llamada write(). Aquí tienes un ejemplo de un archivo en el que se puede escribir situado en un bloque try/finally:

    try:
        file = open('nuevo.txt', 'w')
        file.write('otro texto')
    finally:
        file.close()

    Esta forma de escribir archivos hace que el método close() se invoque cuando ocurre ocurre alguna excepción  en algún lugar del bloque try. En realidad, el archivo se cierra siempre ya que los bloques finally se ejecutan después de completarse los bloques try, tanto si se encuentra una excepción como si no.

    La segunda forma es usando la sentencia with que nos permite utilizar lo que se denomina un gestor de contexto. Cuando se completa el bloque with, tanto si ocurre una excepción como sino, el método close() del objeto se invoca implícitamente cerrando automáticamente el archivo. 

    with open('nuevo.txt','w') as file:
        file.write('Estamos usando la intrucción with')

    Date cuenta de como, usando esta forma ahorramos código y nos aseguramos de que el archivo no se corrompa en caso de que ocurra un error. Aunque no invocamos la llamada close() en el objeto 'file', el gestor de contexto lo hace automáticamente por nosotros después de salir del bloque with.

    Es una buena práctica asegurarnos de gestionar las posibles excepciones que se puedan generar y asegurarnos de que los objetos 'file' se cierran cuando se espera que lo hagan.

    Dos aclaraciones antes de pasar al siguiente punto. 

    Lo que estamos escribiendo en el archivo son cadenas de texto. Si queremos guardar algo que no sean cadenas de texto en este tipo de archivos, deberemos convertirlo en cadenas primero. 

    La segunda cuestión es que con lo que hemos visto hasta ahora todo el texto se guarda como una enorme línea de texto, todo seguido. Si queremos poner saltos de línea tenemos que introducirlos nosotros utilizando el carácter de escape \n


    Leer Archivos.

    Una vez que tienes un objeto archivo de lectura 'file', que has abierto con la etiqueta r, existen tres métodos que son de utilidad para obtener los datos contenidos en el archivo: read(), readline() y readlines(). Pero antes de eso, vamos a ver como este objeto se puede recorrer directamente con un bucle for como cualquier string o lista de python.

    Pasemos a verlo con un ejemplo.

    Dado el siguiente archivo de texto (texto.txt) cuyo contenido es:

    archivo de texto con tres líneas de texto


    with open('texto.txt', 'r') as file:
        for linea in file:
            print(linea)
    

    Primera línea.
    
    Segunda línea.
    
    Tercera línea.

    Esto sin embargo nos deja unas líneas vacías. Esto es porque aunque no lo veamos al final de cada línea hay un carácter no imprimible (\n) que es el salto de línea. Por eso cuando Python lee el archivo línea a línea este se ejecuta ya que aunque no lo veamos directamente. Si queremos eliminarlo podemos usar el método strip() que nos elimina espacios en blanco, comillas y saltos de línea al principio y final de cada una. Lo haríamos de esta forma.

    with open('texto.txt', 'r') as file:
        for linea in file:
            print(linea.strip())

     que nos devuelve:

    Primera línea.
    Segunda línea.
    Tercera línea.


    Una vez visto lo anterior vamos a ver los métodos de los que hablábamos.

    - read():  Lee todo el fichero tal cual está, es decir nos devuelve un objeto string del archivo abierto. Podemos pasarle un parámetro opcional que es el numero de bytes (caracteres) a leer. Si especificamos más bytes de los que hay en el archivo, read() leerá hasta el final del archivo y devolverá los bytes que haya leído.

    Pasemos a verlo con el ejemplo anterior.

    read() funciona en un archivo de esta forma.

    with open('texto.txt', 'r') as file:
        print(file.read())
    Primera línea.
    Segunda línea.
    Tercera línea.

    Observa que cada frase se muestra en una línea distinta al usar \n al final de la misma.

    Y si solamente quisiéramos los 7 primeros bytes del archivo, podríamos hacer algo como esto:

    with open('texto.txt', 'r') as file:
        print(file.read(7))
    Primera

    Ahora el puntero esta en el byte 7, si le pidiésemos que leyera otros 3 bytes más, esto es lo que obtendríamos

       print(file.read(3)
     li

    Como vemos el puntero se desplaza a medida que vamos avanzando en la lectura del archivo.

    El siguiente método para obtener texto de un archivo es el método readline(). La finalidad de readline() es leer una línea de texto de cada vez, de un archivo. Es decir, lee todo el texto existente hasta que se encuentra un salto de línea. Podemos especificar el número máximo de bytes que readline() leerá antes de devolver una cadena, tanto si ha llegado al final de la línea como si no.

    En el siguiente ejemplo, el programa leerá la primera línea y luego leerá los 7 primeros bytes de la segunda, seguido por lo que queda de la segunda línea. Para finalizar leerá la tercera.

    >>> file = open('texto.txt')
    >>> file.readline()
    'Primera línea.\n'
    >>> file.readline(7)
    'Segunda'
    >>> file.readline()
    ' línea.\n'
    >>> file.readline()
    'Tercera línea.'
    


    El último método readlines(), en plural, devuelve una lista con todas las líneas del archivo.

    >>> file = open('texto.txt')
    >>> file.readlines()
    ['Primera línea.\n', 'Segunda línea.\n', 'Tercera línea.']
    >>> 


    Escribir Archivos.

    Algunas veces necesitamos hacer algo más con los archivos que leer datos de los mismos; Seguramente necesitaremos crear nuestros propios archivos y escribir datos en ellos. Existen dos métodos que nos valen para escribir datos en archivos. El primer método, que ya hemos visto antes, es write().

    file.write(cadena)

     Este toma como parámetro la cadena a escribir en el archivo. Aquí tenemos un ejemplo de datos que se escriben en un archivo utilizando el método write():

    file = open("algun_texto.txt", "w")
    file.write('Probando\nel archivo\n')
    file.close()
    
    file_read = open('algun_texto.txt')
    print(file_read.read())
    Probando
    el archivo
    
    

    En la primera línea, abrimos el archivo con el modo 'w', que significa "write" escritura. En la segunda línea se escriben dos líneas en el archivo. En la cuarta línea, utilizamos el nombre de variable file_read esta vez para asignarle el objeto archivo, aunque podíamos haber utilizado 'file' de nuevo. En la última línea imprimimos los datos para comprobar que son los mismos que escribimos primeramente en el mismo.

    El siguiente método común de escribir datos es writelines() que escribe el contenido de una lista (que deben ser strings) en un archivo, línea por línea. Es como la inversa de readlines()

    file.writelines(iterable_cadenas)

    Writelines() tiene un parámetro obligatorio: una secuencia que writelines() escribirá en el archivo abierto. La secuencia puede ser cualquier tipo de objetos por los que se puede iterar (una lista, tupla, cadena, un generador). 

     Aquí vamos a ver un ejemplo de bucle for utilizado para escribir datos en un archivo utilizando el método write() que ya vimos.

    file = open('archivo_writelines.txt', 'w')
    for i in range(10):
        file.write(f'{i}\n')
    file.close()
    
    g = open('archivo_writelines.txt')
    print(g.read())
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    y ahora vamos a ver el mismo ejemplo pero usando writelines().

    serie = []
    file = open('archivo_writelines.txt', 'w')
    for numero in range(10):
        serie.append(str(numero)+'\n')
    # imprimimos la serie para ver lo que vamos a guardar
    print(serie)
    
    # guardamos la lista de una vez.
    file.writelines(serie)
    file.close()
    
    g = open('archivo_writelines.txt')
    print(g.read())

    ['0\n', '1\n', '2\n', '3\n', '4\n', '5\n', '6\n', '7\n', '8\n', '9\n']
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    


    ES MUY IMPORTANTE indicar que writelines() no nos escribe una nueva línea (\n); tenemos que proporcionarla nosotros en la secuencia que le pasemos.

    Otros atributos que podemos utilizar en el objeto 'file' son:

    file.name -> nos devuelve el nombre del archivo.
    file.mode -> nos devuelve el modo con el que se ha abierto el archivo.
    file.encoding ->  nos devuelve el tipo de codificación del fichero.
    file.closed -> nos devuelve True si el fichero está abierto y False si esta cerrado.
    file.tell() -> nos indica en que posición se encuentra el puntero en el archivo
    file.seek(n) -> posiciona el puntero del archivo en la posición n

    Un ejemplo de todas estas llamadas podría ser el siguiente. Consideremos un archivo de texto llamado texto.txt con el siguiente contenido:

    'Primera línea.
    Segunda línea.
    Tercera línea.'

    >>> file = open('texto.txt')
    >>> file.name
    'texto.txt'
    >>> file.mode
    'r'
    >>> file.encoding
    'UTF-8'
    >>> file.closed
    False
    >>> file.tell() #el puntero está en el primer elemento ya que no hemos leído ningún dato.
    0
    >>> file.read()
    'Primera línea.\nSegunda línea.\nTercera línea.\n'
    >>> file.tell() #ahora el puntero del archivo está en el último elemento.
    48
    >>> # colocamos en puntero en el primer elemento 
    >>> # para volver a leer el archivo
    >>> file.seek(0)
    0
    

    En nuestros programas cuando almacenemos el valor de una línea en una variable, esta se guardará con el salto de línea incluido. Para quitarlo utilizamos el método .strip() que quita los espacios en blanco, saltos de línea... existentes al principio y final de una cadena.

    >>> # leemos la primera linea del archivo
    >>> file.readline()
    'Primera línea.\n'
    >>> # Como vemos incluye el salto de línea al final del mismo. 
    >>> # Para quitarlo utilizamos .strip
    >>> # Vuelvo a poner el puntero al principio del archivo con file.seek(0)
    >>> file.readline().strip()
    'Primera línea.'
    


    Comentar también que se puede imprimir en un archivo de Python usando el comando print que habitualmente usamos para imprimir cosas por pantalla. Para ello tenemos que abrir el archivo en modo de escritura y pasar el argumento file al comando print. Aquí te muestro un ejemplo:

    # Abre el archivo en modo escritura
    with open('archivo.txt', 'w') as f:
        # Utiliza print con el argumento file para escribir en el archivo
        print('Hola mundo!', file=f)
        print('Esta es otra línea.', file=f)

    También podemos mandar texto a la salida stderr de una forma similar a grabar datos en un archivo. Pero tanto stdin, stderr y stdout están siempre abiertos, no hace falta abrirlos así que para escribir solo tenemos que hacer:

    import sys
    sys.stderr.write("Mensaje de Error")


    Persistencia con archivo CSV

    Los archivos Csv ('valores separados por comas') son un tipo particular de los archivos de texto. En ellos, los datos se separan normalmente por comas y cada línea de datos finaliza con un retorno \n. Son muy utilizados como forma de guardar e importar archivos de datos en hojas de calculo y bases de datos. Puedes abrir un archivo CSV con EXCEL por ejemplo. 

    Para que te hagas una idea, si abres con un procesador de texto uno de estos archivos tiene la siguiente forma:

    nombre, edad, grupo
    pepe, 12, A
    Maria, 32, B  

    Como puedes ver, los elementos de un archivo csv están separados por comas. En este ejemplo la coma es un delimitador pero podríamos usar otros, como por ejemplo un tabulador.

    Internamente el archivo seria una cadena de texto con estos datos:

    'nombre, edad, grupo\npepe, 12, A\nMaria, 32, B\n'

    Para ver como funcionan este tipo de archivos, podemos abrir un procesador de textos cualquiera y escribimos un archivo con los siguientes datos.

    nano ejemplo.csv


    Trabajando con archivos CSV en Python. Módulos de Lectura y Escritura.

    Si bien podríamos usar la función open() incorporada en Python para trabajar con estos tipos de archivos, que no dejan de ser archivos de texto, hay un módulo csv que facilita mucho el trabajo con los mismos. 

    Pero antes de usar los métodos de lectura y escritura, debemos importar el módulo csv de la siguiente forma:

    import csv


    Leyendo archivos csv con el método csv.reader() 

    El módulo csv.reader() nos va a servir para crear un objeto con la información del archivo que queramos procesar. Su funcionamiento es similar a lo que ya vimos para leer archivos de texto.

    import csv
    with open('ejemplo.csv', 'r') as file:
        datos = csv.reader(file)
        for i in datos:
            print(i)
    ['nombre', 'edad', 'grupo']
    ['Pepe', '12', 'A']
    ['María', '34', 'B']
    ['Antonio', '56', 'A']
    ['Eugenia', '34', 'B']

    En este ejemplo hemos abierto el archivo 'ejemplo.csv' en modo de lectura usando:

    with open('ejemplo.csv', 'r') as file:
        .. ... ..
    

    Después, hemos usado csv.reader() para leer el archivo, el cual nos devuelve un objeto iterable que guardamos en la variable 'datos'. Como vemos el objeto 'datos' al recorrerlo con un bucle for, nos devuelve cada una de las filas del archivo en forma de lista.

    El módulo CSV también contiene la función next(), que devuelve la siguiente línea del archivo cuando se le pasa el objeto lector. Esto nos evita tener que usar un bucle for para recorrerlo sin no queremos usarlo:

    import csv
    with open('ejemplo.csv','r') as file:
        datos = csv.reader(file) # contiene una lista por cada fila de datos.
        linea_datos = next(datos)
        print(linea_datos)
    


    En el ejemplo anterior hemos usado csv.reader() en su forma estandar, usando las comas como delimitador. Sin embargo esta función es más personalizable. Supongamos que en el archivo 'ejemplo.csv' en vez de comas, hubiésemos usado tabuladores como delimitador de datos. Para trabajar con el archivo podríamos haber usado el siguiente código:

    import csv
    with open('ejemplo.csv','r') as file:
        datos = csv.reader(file, delimiter='\t')
        for i in datos:
            print(i)

    Date cuenta como hemos usado el parámetro opcional delimiter='\t'

    La sintaxis completa de la función csv.reader() es:

    csv.reader(csvfile, dialect='excel', **optional_parameters)

    Para ver muchos más detallas sobre 'dialect' y los parámetros opcionales que admite esta función puedes encontrarla en el siguiente link.


    Escribiendo archivos CSV con csv.writer()

    Se utiliza para escribir datos en un archivo csv. Como ya tenemos creado el archivo 'ejemplo.csv' vamos a abrirlo para añadir una nueva fila de datos. Realizaremos la apertura con el parámetro 'a' , de append o añadir, lo que nos servirá para añadir datos al final del archivo. Si solo utilizáramos 'w' borraría los datos del archivo y nos añadiría las nuevas líneas.

    import csv
    with open('ejemplo.csv', 'a') as file:
        datos_a_escribir = csv.writer(file)
        datos_a_escribir.writerow(['Lis', '32' ,'C'])
        datos_a_escribir.writerow(['Alex', '18', 'A'])
    nombre,edad,grupo
    Pepe,12,A
    María,34,B
    Antonio,56,A
    Eugenia,34,B
    Lis,32,C
    Alex,18,A
    
    

    Hemos pasado los nuevos datos a grabar como una lista. Esta lista fue convertida a un string por el programa y añadida al archivo csv con la función writerow().

    Si quisiéramos añadir varias listas al contenido del archivo podemos hacerlo con la función writerows().

    import csv
    with open('ejemplo.csv', 'a') as file:
        datos_a_escribir = csv.writer(file)
        datos_a_escribir.writerows([['Lucas','15','C'],['Dylan','43','B']]) 
    Aquí le hemos pasado una lista, que a su vez contiene otras dos con los datos que queremos añadir al archivo, usando el método writerows() para escribirlos en el archivo.


    Usando la clase csv.DictReader()

    El objeto devuelto por la clase csv.DictReader() puede ser usado para leer un archivo como si fuera un diccionario. Seguimos usando el archivo 'ejemplo.csv'

    import csv
    with open('ejemplo.csv','r') as file:
        datos = csv.DictReader(file)
        for fila in datos:
            print(fila)  
    {'nombre': 'Pepe', 'edad': '12', 'grupo': 'A'}
    {'nombre': 'María', 'edad': '34', 'grupo': 'B'}
    {'nombre': 'Antonio', 'edad': '56', 'grupo': 'A'}
    {'nombre': 'Eugenia', 'edad': '34', 'grupo': 'B'}
    {'nombre': 'Lis', 'edad': '32', 'grupo': 'C'}
    {'nombre': 'Alex', 'edad': '18', 'grupo': 'A'}
    {'nombre': 'Lucas', 'edad': '15', 'grupo': 'C'}
    {'nombre': 'Dylan', 'edad': '43', 'grupo': 'B'}

    Nota: Si usas una versión de Python inferior a la 3.8 tendrás que usar print(dict(fila))

    Como puedes ver, las entradas de la primera fila se utilizan como claves del diccionario. Y las entradas de las otras filas son utilizadas como valores.


    La clase de Python csv.DictWriter()

    El objeto generado por la clase csv.DictWriter() puede utilizarse para escribir un diccionario en un archivo CSV. 

    La sintaxis mínima de la misma es:

    csv.DictWriter(file, fieldnames)

    en donde,

    file - es el objeto file en el que queremos escribir.

    fieldnames - es una lista que contiene los nombres de los campos con el orden en el que se escribirán los datos.

    Vamos a crear un archivo nuevo para guardar los datos - ejemplo2.csv -

    import csv
    with open('ejemplo2.csv','w') as file:
        datos = csv.DictWriter(file, ['fruta','color'])
    
        # Escribimos la primera fila que contendrá las llaves del diccionario o cabecera.
        datos.writeheader()
        # Escribimos el diccionario
        datos.writerow({'fruta':'pera','color':'verde'})  
        datos.writerow({'fruta':'limon','color':'amarillo'})
        datos.writerow({'fruta':'manzana','color':'verde'})
    fruta,color
    pera,verde
    limon,amarillo
    manzana,verde

    Podríamos pasar varios diccionarios usando writerows([{..},...,{...}]) y pasando una lista con los datos (los diccionarios)



    Persistencia con archivos Binarios.


    ¿Que es un bytearray?


    Antes de comenzar a hablar de archivos binarios, vamos a ver una de las clases especializadas que usa Python para almacenar datos amorfos. Los datos amorfos son datos que no tienen una forma específica, solo son una serie de bytes. Esto no significa que estos bytes no puedan tener su propio significado o que no puedan representar un objeto útil, por ejemplo, puntos de un gráfico de mapa de bits.

    Lo más importante es que en el lugar en el que tenemos contacto con los datos,  no podemos o queremos saber nada al respecto. Los datos amorfos no pueden guardarse utilizando alguno de los sistemas que ya hemos visto, no son cadenas ni listas. Para ello utilizamos una clase especial llamada bytearray

    Por ejemplo si queremos usar un contenedor para leer una imagen de mapa de bits, tenemos que crearlo explícitamente.

    datos = bytearray(10)

    Esta invocación crea un objeto bytearray capaz de almacenar 10 bytes. El constructor rellena toda la estructura con ceros. Los bytearrays se parecen en muchos aspectos a las listas.  Por ejemplo, son mutables, se puede usar sobre ellos la función len, y puedes acceder a cualquiera de sus elementos usando la indexación convencional.

    Existe una limitación importante: no debes establecer ningún elemento del arreglo de bytes con un valor que no sea un entero y tampoco está permitido usar un valor fuera del rango 0 - 255. Recuerda que un byte son 8 bits y que con ello se pueden crear 2 elevado a 8 números binarios distintos es decir 256 números.

    Se puede tratar cualquier elemento del arreglo de bytes como un entero. Echa un vistazo a este ejemplo.

    datos = bytearray(10)
    
    for i in range(len(datos)):
        datos[i] = 10 - i
    
    for b in datos:
        print(hex(b))
    Salida:

    0xa
    0x9
    0x8
    0x7
    0x6
    0x5
    0x4
    0x3
    0x2
    0x1
    

    Hemos usado dos métodos para iterar el arreglo de bytes y la función hex() para ver los elementos impresos como valores hexadecimales.

    Dicho lo cual, vamos a ver como guardar un arreglo de bytes en un archivo binario, ya que no queremos guardar su representación legible sino escribir en el archivo uno a uno el contenido de la memoria física byte a byte.

    Observa el siguiente código.

    from os import strerror
    
    data = bytearray(10)
    
    for i in range(len(data)):
        data[i] = 10 + i
    
    try:
        bf = open('file.bin', 'wb')
        bf.write(data)
        bf.close()
    except IOError as e:
        print("Se produjo un error de E/S:", strerror(e.errno))

    Analicémoslo:

    • Primero, inicializamos bytearray con valores a partir de 10; si deseas que el contenido del archivo sea claramente legible, reemplaza el 10con algo como ord('a'), esto producirá bytes que contienen valores correspondientes a la parte alfabética del código ASCII (no pienses que harás que el archivo sea un archivo de texto; sigue siendo binario, ya que se creó con un indicador: wb).
    • Después, creamos el archivo usando la función open(), la única diferencia en comparación con las variantes anteriores es que el modo de apertura contiene el indicador b.
    • El método write() toma su argumento (bytearray) y lo envía (como un todo) al archivo.
    • El stream se cierra de forma rutinaria.

    El método write() devuelve la cantidad de bytes escritos correctamente.

    Si los valores difieren de la longitud de los argumentos del método, puede significar que hay algunos errores de escritura. En este caso, no hemos utilizado el resultado; esto puede no ser apropiado en todos los casos.

    Intenta ejecutar el código de forma local en tu ordenador y analiza el contenido del archivo recién creado.

    Lo vamos a usar en el siguiente paso.

    Cómo leer bytes de un flujo (stream)

    La lectura de un archivo binario requiere el uso de un método especializado llamado readinto() ya que el método no crea un nuevo objeto del arreglo de bytes, sino que llena uno creado previamente con los valores tomados del archivo binario.

    Nota:

    • El método devuelve el número de bytes leídos con éxito.
    • El método intenta llenar todo el espacio disponible dentro de su argumento; si existen más datos en el archivo que espacio en el argumento, la operación de lectura se detendrá antes del final del archivo; el resultado del método puede indicar que el arreglo de bytes solo se ha llenado de manera fragmentaria (el resultado también lo mostrará y la parte del arreglo que no está siendo utilizada por los contenidos recién leídos permanece intacta).

    Observa el código a continuación:

    from os import strerror
    
    data = bytearray(10)
    
    try:
        binary_file = open('file.bin', 'rb')
        binary_file.readinto(data)
        binary_file.close()
    
        for b in data:
            print(hex(b), end=' ')
    except IOError as e:
        print("Se produjo un error de E/S:", strerror(e.errno))

    Salida:

    0xa 0xb 0xc 0xd 0xe 0xf 0x10 0x11 0x12 0x13 

    Analicémoslo:


    Primero, abrimos el archivo (el que se creó usando el código anterior) con el modo descrito como rb. Luego, leemos su contenido en el arreglo de bytes llamado data, con un tamaño de diez bytes. Finalmente, imprimimos el contenido del arreglo de bytes: ¿Son los mismos que esperabas?


    Ejecuta el código y verifica si funciona.


    Se ofrece una forma alternativa de leer el contenido de un archivo binario mediante el método denominado read().


    Invocado sin argumentos, intenta leer todo el contenido del archivo en la memoria, haciéndolo parte de un objeto recién creado de la clase bytes.
    Esta clase tiene algunas similitudes con bytearray, con la excepción de una diferencia significativa: es immutable.


    Afortunadamente, no hay obstáculos para crear un arreglo de bytes tomando su valor inicial directamente del objeto de bytes, como aquí:

    from os import strerror
    
    try:
        binary_file = open('file.bin', 'rb')
        data = bytearray(binary_file.read())
        binary_file.close()
    
        for b in data:
            print(hex(b), end=' ')
    
    except IOError as e:
        print("Se produjo un error de E/S:", strerror(e.errno))

    Pero ¡Ten cuidado! No utilices este tipo de lectura sino estás seguro de que el contenido del archivo se ajusta a la memoria disponible en tu sistema.

    Si el método read() se invoca con un argumento, se le indica el número máximo de bytes a leer.

    El método intenta leer la cantidad deseada de bytes del archivo, y la longitud del archivo devuelto puede usarse para determinar la cantidad de bytes realmente leídos.

    Por ejemplo. 

    from os import strerror
    
    try:
        binary_file = open('file.bin', 'rb')
        data = bytearray(binary_file.read(5))
        binary_file.close()
    
        for b in data:
            print(hex(b), end=' ')
    
    except IOError as e:
        print("Se produjo un error de E/S:", strerror(e.errno))

    Salida:


    0xa 0xb 0xc 0xd 0xe 

    Se han leído los primeros 5 bytes del archivo mientras que los restantes están a la espera de ser leídos.

    Para repasar un poco lo que hemos visto creemos un programa sencillo para copiar un archivo:

    from os import strerror
    
    srcname = input("Ingresa el nombre del archivo fuente: ")
    try:
        src = open(srcname, 'rb')
    except IOError as e:
        print("No se puede abrir archivo fuente: ", strerror(e.errno))
        exit(e.errno)	
    
    dstname = input("Ingresa el nombre del archivo destino: ")
    try:
        dst = open(dstname, 'wb')
    except Exception as e:
        print("No se puede crear el archivo de destino: ", strerror(e.errno))
        src.close()
        exit(e.errno)	
    
    buffer = bytearray(65536)
    total  = 0
    try:
        readin = src.readinto(buffer)
        while readin > 0:
            written = dst.write(buffer[:readin])
            total += written
            readin = src.readinto(buffer)
    except IOError as e:
        print("No se puede crear el archivo de destino: ", strerror(e.errno))
        exit(e.errno)	
        
    print(total,'byte(s) escritos con éxito')
    src.close()
    dst.close()

    Analicémoslo:

    Las líneas 3 a la 8: solicitan al usuario el nombre del archivo a copiar e intentan abrirlo para leerlo; se termina la ejecución del programa si falla la apertura; nota: emplea la función exit() para detener la ejecución del programa y pasar el código de finalización al sistema operativo; cualquier código de finalización que no sea 0 significa que el programa ha encontrado algunos problemas; se debe utilizar el valor errno para especificar la naturaleza del problema.

    Las líneas 10 a la 16: repiten casi la misma acción, pero esta vez para el archivo de salida.

    La línea 18: prepara una parte de memoria para transferir datos del archivo fuente al destino; tal área de transferencia a menudo se llama un búfer, de ahí el nombre de la variable; el tamaño del búfer es arbitrario; en este caso, decidimos usar 64 kilobytes; técnicamente, un búfer más grande es más rápido al copiar elementos, ya que un búfer más grande significa menos operaciones de E/S; en realidad, siempre hay un límite, cuyo cruce no genera más ventajas; pruébalo tú mismo si quieres.

    Línea 19: cuenta los bytes copiados: este es el contador y su valor inicial.

    Línea 21: intenta llenar el búfer por primera vez.

    Línea 22: mientras se obtenga un número de bytes distinto a cero, repite las mismas acciones.

    Línea 23: escribe el contenido del búfer en el archivo de salida (nota: hemos usado un segmento para limitar la cantidad de bytes que se escriben, ya que write() siempre prefiere escribir todo el búfer).

    Línea 24: actualiza el contador.

    Línea 25: lee el siguiente fragmento de archivo.

    Las líneas 30 a la 32: limpieza final, el trabajo está hecho.



    La b
    iblioteca Pickle.

    Vamos a ver de forma rápida como guardar datos en código binario con la biblioteca Pickle. Hasta hace poco los archivos que habíamos guardado anteriormente eran archivos de texto que podíamos ver con cualquier editor de texto, sin embargo con este módulo los datos se guardarán de forma binaria, (al igual que por ejemplo una imagen).

    Podemos utilizar esta biblioteca para guardar cualquier objeto de Python (cadenas, listas, diccionarios etc) 

    Esta biblioteca Pickle tiene dos métodos:

    dump = para volcar datos en el archivo.

    load = para cargar datos de un archivo.


    Por ejemplo, para guardar una lista que contenga una serie de números haríamos lo siguiente:

    import pickle
    
    lista = [1,2,3,8]
    
    with open('archivo_bin','wb') as file:
        pickle.dump(lista, file)


    Como en anteriores ejemplos empezamos importando la librería, en este caso pickle. Abrimos el archivo que contendrá los datos. Fíjate en la particularidad que lo abrimos como 'wb',  es decir que lo escribimos para escribir datos binarios. Usamos pickle.dump(objeto a guardar, nombre del objeto archivo) para grabar los datos al fichero.

    Si por el contrario quisiéramos leer esta lista, después de abrir el archivo en modo binario de lectura con 'rb', usaríamos el método pickle.load():

    import pickle
    
    with open('archivo_bin','rb') as file:
        lista = pickle.load(file)
        print(lista)
      ... [1, 2, 3, 8]


    Persistencia usando archivos json.

    Otra forma de poder guardar datos, y que no sea mediante un archivo de solo cadenas, son los archivos json.  Si usamos el módulo JSON no solo podremos intercambiar datos entre distintos programas de Python, sino que también lo podremos hacer con programas escritos en otros lenguajes de programación ya que JSON (Java Script Object Notation) se desarrolló originalmente para Javascript pero actualmente lo usan muchos lenguajes de programación para intercambiar datos.

    El funcionamiento es muy similar al anterior ya que utilizaremos json.dump() para guardar los datos (cadenas, listas, diccionarios etc) y json.load() para recuperarlos.

    Para verlo con un ejemplo, vamos a crear un programa que pida su nombre al usuario y si este no lo ha hecho previamente (no existe el archivo json) lo cree y se guarde.

    import json
    
    # Carga el nombre de usuario si se ha guardado previamente.
    # Si no es el caso, solicita el nombre del usuario y lo
    # guardado
    
    archivo = 'nombre_usuario.json'
    try:
        with open(archivo) as f:
            nombre_usuario = json.load(f)
    except FileNotFoundError:
        nombre_usuario = input("¿Como te llamas? > ")
        with open(archivo, 'w') as f:
            json.dump(nombre_usuario, f)
            print(f"la próxima vez te recordaré, usuario {nombre_usuario}")
    else:
        print(f"Bienvenido: {nombre_usuario}")


    Si el archivo nombre_usuario.json existe lo leerá. Si no existe creará uno con los datos y en cualquiera de los dos casos terminará con una frase de bienvenida.


    Persistencia usando la librería "shelve".


    La librería shelve en Python proporciona una forma sencilla de almacenar y recuperar objetos en una base de datos persistente. Básicamente, shelve actúa como un "estante" o "estantería" en la que puedes guardar objetos Python y luego recuperarlos más adelante. Está diseñada para ser fácil de usar y ofrece una interfaz similar a un diccionario.

    La librería shelve utiliza un formato de almacenamiento basado en clave-valor, donde cada objeto almacenado se identifica mediante una clave única. Puedes pensar en ello como una especie de diccionario persistente en el que puedes guardar objetos complejos y luego recuperarlos utilizando su clave correspondiente.

    Una característica interesante de shelve es que puede almacenar objetos Python arbitrarios, incluyendo listas, diccionarios, clases personalizadas, entre otros. Internamente, shelve utiliza el módulo pickle para la serialización y deserialización de los objetos.

    Aquí hay un ejemplo simple que muestra cómo utilizar shelve para almacenar y recuperar objetos:

    import shelve
    
    # Abrir o crear una base de datos de estante
    db = shelve.open('mi_estante.db')
    
    # Almacenar objetos en la base de datos
    db['clave1'] = 'valor1'
    db['clave2'] = [1, 2, 3]
    db['clave3'] = {'nombre': 'Juan', 'edad': 30}
    
    # Recuperar objetos de la base de datos
    valor1 = db['clave1']
    lista = db['clave2']
    diccionario = db['clave3']
    
    print(valor1)  # Imprime: valor1
    print(lista)  # Imprime: [1, 2, 3]
    print(diccionario)  # Imprime: {'nombre': 'Juan', 'edad': 30}
    
    # Cerrar la base de datos
    db.close()
    
    

    En este ejemplo, se crea o abre una base de datos de estante utilizando shelve.open(). Luego, se almacenan objetos en la base de datos utilizando claves y valores. Posteriormente, se recuperan los objetos utilizando las claves correspondientes. Finalmente, se cierra la base de datos utilizando close().

    La librería shelve es útil cuando necesitas almacenar objetos de Python en un formato persistente y recuperarlos en un momento posterior. Puede ser útil para almacenar datos, configuraciones o cualquier otro objeto que necesites conservar más allá de la ejecución actual del programa.

    Si queremos abrir el archivo creado solo como lectura hay que añadir en open el argumento flag = 'r' despues del nombre del archivo.

    Para añadir o trabajar con datos de una estantería ya existente.


    Hay que tener cuidado porque por defecto shelve no detecta cambios en estanterías ya creadas salvo que usemos writeback = True en los argumentos.

    Por defecto, cuando se almacena un objeto en shelve, se crea una copia en la base de datos y los cambios posteriores en el objeto no se reflejan automáticamente en la copia almacenada. Si se utiliza el argumento writeback=True al abrir la base de datos, se activa el modo de escritura y los cambios realizados en los objetos almacenados se actualizan automáticamente en la base de datos sin requerir una operación de reescritura explícita.

     Para entenderlo mejor veamos un ejemplo:

    >>> import shelve
    >>> open shelve.open('mi_estanteria') as db:
    >>> db['key1'] = {'int':10, 'float': 9.5, 'texto': 'una frase'}
    # Esto funciona como se espera.
    >>> print(db['key1'])
    {'int': 10, 'float': 9.5, 'texto': 'una frase'}
    >>> db['key1']['nuevo']='no estaba antes' 
    # Pero esto no funciona. No actualiza online la nueva clave.
    >>> print(db['key1'])
    {'int': 10, 'float': 9.5, 'texto': 'una frase'}
    db.close()
        
    
    Si queremos que funcione on fly tenemos que añadir el parametro writeback = True de la siguiente manera.

    >>> import shelve
    >>> open shelve.open('mi_estanteria', writeback = True) as db:
    >>> db['key1'] = {'int':10, 'float': 9.5, 'texto': 'una frase'}
    >>> print(db['key1'])
    {'int': 10, 'float': 9.5, 'texto': 'una frase'}
    
    >>> db['key1']['nuevo']='no estaba antes' 
    >>> print(db['key1'])
    {'int': 10, 'float': 9.5, 'texto': 'una frase', 'nuevo': 'no estaba antes'}
    >>> db.close()

    Esto es a costa de sacrificar un poco de memoria y que el cierre del archivo se haga más lentamente.

    Metodo .sync()

    El método .sync() de la librería shelve en Python se utiliza para asegurar que los cambios realizados en la base de datos se escriban físicamente en el disco. Cuando se modifican objetos almacenados en shelve, los cambios se mantienen en memoria y se escriben en disco de forma diferida para mejorar el rendimiento. Sin embargo, si necesitas asegurarte de que los cambios se guarden de inmediato, puedes llamar al método .sync().

    Aquí tienes un ejemplo para ilustrar el uso del método .sync():

    import shelve
    
    # Abrir la base de datos de estante
    with shelve.open('mi_estante.db') as db
    
        # Almacenar un objeto en la base de datos
        db['clave1'] = 'valor1'
    
        # Modificar el objeto almacenado
        db['clave1'] = 'valor_modificado'
    
        # Guardar los cambios en disco
        db.sync()
    

    En este ejemplo, se abre la base de datos de estante y se almacena un objeto con la clave 'clave1'. Luego, se modifica el objeto asignando un nuevo valor a la clave 'clave1'. Después de realizar la modificación, se llama al método .sync() para asegurarse de que los cambios se guarden físicamente en el disco.

    Es importante destacar que el método .sync() puede ser útil en situaciones en las que necesitas garantizar que los cambios se guarden de forma inmediata y no quieres depender de la escritura diferida en la base de datos. Sin embargo, debes tener en cuenta que el uso frecuente de .sync() puede afectar el rendimiento, ya que implica una operación de escritura en disco adicional. Por lo tanto, se recomienda utilizar este método de manera consciente y evaluar las necesidades específicas de tu aplicación.


    Y con esto finalizamos un repaso sencillo sobre la persistencia de datos en Python.