Index of content
Temas de Pregrado en Ciencias de la Computación Guía avanzada a Python 3 Programación Juan caza
Temas de Pregrado en Ciencias de la Computación Editor de series Ian Mackie, Universidad de Sussex, Brighton, Reino Unido Editores asesores Samson Abramsky, Departamento de Ciencias de la Computación, Universidad de Oxford, Oxford, Reino Unido Chris Hankin, Departamento de Informática, Imperial College London, Londres, Reino Unido Dexter C. Kozen, Departamento de Informática, Universidad de Cornell, Ithaca, NY, EE.UU Andrew Pitts, Universidad de Cambridge, Cambridge, Reino Unido Hanne Riis Nielson , Departamento de Matemática Aplicada e Informática, Universidad Técnica de Dinamarca, Kongens Lyngby, Dinamarca Steven S. Skiena, Departamento de Ciencias de la Computación, Universidad de Stony Brook, Stony Brook, Nueva York, EE. UU. Iain Stewart, Departamento de Ciencias de la Computación, Laboratorios de Ciencias, Universidad de Durham, Durham, Reino Unido Mike Hinchey, Universidad de Limerick, Limerick, Irlanda
‘Temas de pregrado en Ciencias de la Computación’ (UTiCS) ofrece alta calidad contenido instructivo para estudiantes universitarios que estudian en todas las áreas de computación y Ciencias de la Información. Desde el material fundamental y teórico básico hasta el último año temas y aplicaciones, los libros de UTiCS adoptan un enfoque fresco, conciso y moderno y son ideales para el autoaprendizaje o para un curso de uno o dos semestres. Los textos son todos escrito por expertos establecidos en sus campos, revisado por un asesor internacional tablero, y contienen numerosos ejemplos y problemas, muchos de los cuales incluyen completamente soluciones trabajadas. El concepto UTiCS se basa en libros concisos y de alta calidad en formato de tapa blanda, y generalmente un máximo de 275 a 300 páginas. Para libros de texto de pregrado que son probable que sea más largo, más expositivo, Springer continúa ofreciendo el muy respetado Textos de la serie Informática, a los que remitimos a los posibles autores. Más información sobre esta serie en http://www.springer.com/series/7592
Juan caza Guía avanzada de Python 3 Programación 123
Juan caza marisma Tecnología Midmarsh Ltd. Chippenham, Wiltshire, Reino Unido ISSN 1863-7310 ISSN 2197-1781 (electrónico) Temas de Pregrado en Ciencias de la Computación ISBN 978-3-030-25942-6 ISBN 978-3-030-25943-3 (libro electronico) https://doi.org/10.1007/978-3-030-25943-3 © Springer Nature Suiza AG 2019 Esta obra está sujeta a derechos de autor. Todos los derechos están reservados por el editor, ya sea en su totalidad o en parte del material se refiere, en concreto los derechos de traducción, reimpresión, reutilización de ilustraciones, recitación, radiodifusión, reproducción en micropelículas o de cualquier otra forma física, y transmisión o almacenamiento y recuperación de información, adaptación electrónica, software de computadora, o por medios similares o diferentes metodología ahora conocida o desarrollada en el futuro. El uso de nombres descriptivos generales, nombres registrados, marcas comerciales, marcas de servicio, etc. en este publicación no implica, incluso en ausencia de una declaración específica, que dichos nombres estén exentos de las leyes y reglamentos de protección pertinentes y, por lo tanto, gratis para uso general. El editor, los autores y los editores están seguros de asumir que el consejo y la información en este libro se cree que son verdaderos y precisos en la fecha de publicación. Ni el editor ni el los autores o los editores dan garantía, expresa o implícita, con respecto al material contenido en este documento o por cualquier error u omisión que se haya cometido. El editor se mantiene neutral con respecto a reclamos jurisdiccionales en mapas publicados y afiliaciones institucionales. Este pie de imprenta de Springer es publicado por la empresa registrada Springer Nature Switzerland AG La dirección de la empresa registrada es: Gewerbestrasse 11, 6330 Cham, Suiza
Para Denise, mi esposa.
Prefacio Algunos de los aspectos clave de este libro son:
- Supone conocimientos de Python 3 y de conceptos como funciones, clases, protocolos, clases base abstractas, decoradores, iterables, tipos de colección (como Lista y Tupla) etc.
- Sin embargo, el libro asume muy poco conocimiento o experiencia de los temas. presentado.
- El libro está dividido en ocho áreas temáticas; Gráficos por computadora, Juegos, Pruebas, Entrada/salida de archivos, acceso a bases de datos, registro, concurrencia y paralelismo y Programación de redes.
- Cada tema del libro tiene un capítulo introductorio seguido de capítulos que profundizar en ese tema.
- El libro incluye ejercicios al final de la mayoría de los capítulos.
- Todos los ejemplos de código (y soluciones de ejercicios) se proporcionan en línea en un GitHub repositorio. Organización del Capítulo Cada capítulo tiene una breve introducción, el cuerpo principal del capítulo, seguido de una lista de referencias en línea que se pueden utilizar para leer más. Después de esto, normalmente hay una sección de Ejercicios que enumera uno o más ejercicios que se basan en las habilidades que habrá aprendido en ese capítulo. Las soluciones de muestra para los ejercicios están disponibles en un repositorio de GitHub que apoya este libro. viii
Que necesitas Por supuesto, puedes simplemente leer este libro; sin embargo, siguiendo los ejemplos de este libro se asegurará de que obtenga el mayor provecho posible del contenido. Para esto necesitarás una computadora. Python es un lenguaje de programación multiplataforma y, como tal, puede usar Python en una PC con Windows, una Linux Box o una Apple Mac, etc. Esto significa que no está atado a un tipo particular de sistema operativo; puedes usar lo que tengas disponible. Sin embargo, deberá instalar algún software en su computadora. en un mini- mamá, necesitarás Python. El enfoque de este libro es Python 3, así que esa es la versión que se asume para todos los ejemplos y ejercicios. Como Python está disponible para una amplia gama de plataformas desde Windows hasta Mac OS y Linux; tendrás que asegurarte que descargues la versión para tu sistema operativo. Python se puede descargar desde el sitio web principal de Python, que se puede encontrar en http://www.python.org. También necesitará algún tipo de editor en el que escribir sus programas. Allá hay numerosos editores de programación genéricos disponibles para diferentes sistemas operativos con VIM en Linux, Notepad++ en Windows y Sublime Text en Windows y Las Mac son opciones populares. viii Prefacio
Sin embargo, usando un editor IDE (Entorno de Desarrollo Integrado) como PyCharm puede hacer que escribir y ejecutar sus programas sea mucho más fácil. Sin embargo, este libro no asume ningún editor, IDE o entorno en particular. (aparte de Python 3 en sí). Versiones de Python Actualmente hay dos versiones principales de Python llamadas Python 2 y Python 3. • Python 2 se lanzó en octubre de 2000 y ha sido, y sigue siendo, muy utilizado. • Python 3 se lanzó en diciembre de 2008 y es una revisión importante del lenguaje. calibre que no es compatible con versiones anteriores. Los problemas entre las dos versiones se pueden resaltar mediante la impresión simple instalación: • En Python 2 esto se escribe como print ‘Hello World’ • En Python 3 esto se escribe como impresión (‘Hello World’) Puede que no parezca una gran diferencia, pero la inclusión de ‘()’ marca un cambio importante y significa que cualquier código escrito para una versión de Python probablemente no se ejecute en la otra versión. Hay herramientas disponibles, como el 2to3 utilidad, que (parcialmente) automatizará la traducción de Python 2 a Python 3 pero en En general, todavía le queda mucho trabajo por hacer. Esto entonces plantea la pregunta de qué versión usar. Aunque el interés en Python 3 aumenta constantemente, hay muchas organizaciones que todavía usan Python 2. Elegir qué versión usar es una preocupación constante para muchas empresas. Sin embargo, el plan de fin de vida de Python 2 se anunció inicialmente en 2015 y aunque se pospuso hasta 2020 debido a la preocupación de que una gran cantidad de El código no se pudo transferir fácilmente a Python 3, todavía está viviendo en préstamo. tiempo. Python 3 es el futuro del lenguaje Python y es esta versión la que tiene introdujo muchas de las características nuevas y mejoradas del lenguaje y la biblioteca (que han es cierto que ha sido portado a Python 2 en muchos casos). Este libro es únicamente centrado en Python 3. Recursos útiles de Python Hay una amplia gama de recursos en la web para Python; destacaremos algunos aquí que usted debe marcar. No seguiremos haciendo referencia a estos para evitar repetición, pero puede volver a consultar esta sección siempre que lo necesite: • https://en.wikipedia.org/wiki/Python_Software_Foundation Pitón Software Base. Prefacio ix
• https://docs.python.org/3/ El sitio principal de documentación de Python 3. Contiene tutoriales, referencias de bibliotecas, guías de configuración e instalación, así como Python cómo-tos • https://docs.python.org/3/library/index.html Una lista de todas las funciones integradas para el lenguaje Python—aquí es donde puede encontrar documentación en línea para el varias clases y funciones que usaremos a lo largo de este libro. • https://pymotw.com/3/ el módulo Python 3 del sitio de la semana. Este sitio contiene muchos, muchos módulos de Python con breves ejemplos y explicaciones de lo que los módulos lo hacen. Un módulo de Python es una biblioteca de funciones que se basan y amplían el lenguaje central de Python. Por ejemplo, si estás interesado en construir juegos usando Python entonces pygame es un módulo específicamente diseñado para hacer esto más fácil. • https://www.fullstackpython.com/email.html es a mensual Boletin informativo eso se enfoca en un solo tema de Python cada mes, como una nueva biblioteca o módulo. • http://www.pythonweekly.com/ es un resumen semanal gratuito de las últimas novedades de Python artículos, proyectos, videos y próximos eventos. Cada sección del libro proporcionará referencias en línea adicionales relevantes para el tema que se está discutiendo. Convenciones A lo largo de este libro encontrará una serie de convenciones utilizadas para los estilos de texto. Estos estilos de texto distinguen entre diferentes tipos de información. Palabras de código, variables y valores de Python, utilizados en el cuerpo principal del texto, se muestran usando una fuente Courier. Por ejemplo: Este programa crea una ventana de nivel superior (el wx.Frame) y le da un título. también crea una etiqueta (un objeto wx.StaticText) que se mostrará dentro del marco. En el párrafo anterior, wx.Frame y wx.StaticText son clases disponibles en un Biblioteca de interfaz gráfica de usuario de Python. Un bloque de código de Python se establece como se muestra aquí: X Prefacio
Tenga en cuenta que las palabras clave se muestran en negrita. En algunos casos se puede resaltar con color algo de particular interés: Cualquier línea de comando o entrada de usuario se muestra en cursiva y en color púrpura; para ejemplo: O Código de ejemplo y soluciones de muestra Los ejemplos utilizados en este libro (junto con soluciones de muestra para los ejercicios en el final de la mayoría de los capítulos) están disponibles en un repositorio de GitHub. GitHub proporciona una web interfaz para Git, así como un entorno de servidor que aloja a Git. Git es un sistema de control de versiones que normalmente se usa para administrar archivos de código fuente (como como los que se utilizan para crear sistemas en lenguajes de programación como Python, pero también Java, C#, C++, Scala, etc.). Sistemas como Git son muy útiles para la colaboración. ya que permiten que varias personas trabajen en una implementación y fusionar su trabajo juntos. También proporcionan una vista histórica útil del código. (que también permite a los desarrolladores revertir los cambios si las modificaciones resultan ser inadecuado). Si ya tiene Git instalado en su computadora, entonces puede clonar (obtener un copia de) el repositorio localmente usando: Prefacio xi
Si no tiene Git, puede obtener un archivo zip de los ejemplos usando Por supuesto, puede instalar Git usted mismo si lo desea. Para hacer esto, consulte https://git-scm. com/descargas. Versiones del cliente Git para Mac OS, Windows y Linux/Unix están disponibles aquí. Sin embargo, muchos IDE, como PyCharm, vienen con soporte Git y, por lo tanto, ofrecen otro enfoque para obtener un repositorio Git. Para obtener más información sobre Git, consulte http://git-scm.com/doc. Esta guía de Git proporciona una muy buena base y es muy recomendable. Agradecimientos Quisiera agradecer a Phoebe Hunt por crear las imágenes de píxeles utilizadas para la Juego de StarshipMeteors en el Cap. 8. xi Prefacio
Contenido 1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Parte I Gráficos de computadora 2 Introducción a la Computación Gráfica. . . . . . . . . . . . . . . . . . . . . . . . 5 2.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2 Fondo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.3 La era de la computadora gráfica. . . . . . . . . . . . . . . . . . . . . . . . . 6 2.4 Gráficos interactivos y no interactivos. . . . . . . . . . . . . . . . 7 2.5 píxeles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.6 Mapa de bits frente a gráficos vectoriales. . . . . . . . . . . . . . . . . . . . . . 10 2.7 Almacenamiento en búfer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.8 Python y Computación Gráfica. . . . . . . . . . . . . . . . . . . . . . . 10 2.9 Referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.10 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3 Gráficos de tortuga pitón. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2 La biblioteca de gráficos Turtle. . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2.1 El Módulo Tortuga. . . . . . . . . . . . . . . . . . . . . . . . . 13 3.2.2 Gráficos básicos de tortugas. . . . . . . . . . . . . . . . . . . . . . . 14 3.2.3 Formas de dibujo. . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3.2.4 Formas de relleno. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.3 Otras bibliotecas gráficas. . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 3.4 Gráficos 3D. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.4.1 PyOpenGL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 3.5 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.6 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 XIII
4 Arte generado por computadora. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.1 Creación de arte por computadora. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 4.2 Un generador de arte por computadora. . . . . . . . . . . . . . . . . . . . . . . . . . 25 4.3 Fractales en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 4.3.1 El copo de nieve de Koch. . . . . . . . . . . . . . . . . . . . . . . . 28 4.3.2 Conjunto de Mandelbrot. . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 4.4 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 4.5 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5 Introducción a Matplotlib. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 5.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 5.2 Matplotlib. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 5.3 Componentes de la trama. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 5.4 Arquitectura matplotlib. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 5.4.1 Capa de fondo. . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 5.4.2 La capa del artista. . . . . . . . . . . . . . . . . . . . . . . . . . . 40 5.4.3 La capa de secuencias de comandos. . . . . . . . . . . . . . . . . . . . . . . . 41 5.5 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 6 Graficando con Matplotlib pyplot. . . . . . . . . . . . . . . . . . . . . . . . . . 43 6.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 6.2 La API de pyplot. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 6.3 Gráficos de línea. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 6.3.1 Cadenas de formato codificadas. . . . . . . . . . . . . . . . . . . . . . . 46 6.4 Gráfico de dispersión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 6.4.1 Cuándo usar gráficos de dispersión. . . . . . . . . . . . . . . . . . 49 6.5 Gráficos circulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 6.5.1 Segmentos en expansión. . . . . . . . . . . . . . . . . . . . . . . . 52 6.5.2 Cuándo usar gráficos circulares. . . . . . . . . . . . . . . . . . . . . 53 6.6 Gráfica de barras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 6.6.1 Gráficos de barras horizontales. . . . . . . . . . . . . . . . . . . . . . . 55 6.6.2 Barras de colores. . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 6.6.3 Gráficos de barras apiladas. . . . . . . . . . . . . . . . . . . . . . . . . 57 6.6.4 Gráficos de barras agrupadas. . . . . . . . . . . . . . . . . . . . . . . . 58 6.7 Figuras y subtramas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 6.8 Gráficos 3D. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 6.9 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . sesenta y cinco 7 Interfaces gráficas de usuario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 7.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 7.2 GUI y WIMPS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 xiv Contenido
7.3 Frameworks de ventanas para Python. . . . . . . . . . . . . . . . . . . . 69 7.3.1 Bibliotecas GUI independientes de la plataforma. . . . . . . . . . . . . 70 7.3.2 Bibliotecas GUI específicas de la plataforma. . . . . . . . . . . . . . . . 70 7.4 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 8 La biblioteca GUI de wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 8.1 La biblioteca wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 8.1.1 Módulos wxPython. . . . . . . . . . . . . . . . . . . . . . . . . 74 8.1.2 Ventanas como Objetos. . . . . . . . . . . . . . . . . . . . . . . . 75 8.1.3 Un ejemplo sencillo. . . . . . . . . . . . . . . . . . . . . . . . . 75 8.2 La clase wx.App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 8.3 Clases de ventana. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 8.4 Clases de widget/control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 8.5 Diálogos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 8.6 Organizar widgets dentro de un contenedor. . . . . . . . . . . . . . . . . . 82 8.7 Dibujar gráficos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 8.8 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 8.9 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 8.9.1 Aplicación de GUI simple. . . . . . . . . . . . . . . . . . . . . 86 9 Eventos en las interfaces de usuario de wxPython. . . . . . . . . . . . . . . . . . . . . . . . 87 9.1 Manejo de eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 9.2 Definiciones de eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 9.3 Tipos de Eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 9.4 Vinculación de un evento a un controlador de eventos. . . . . . . . . . . . . . . . . . 89 9.5 Implementación del manejo de eventos. . . . . . . . . . . . . . . . . . . . . . . . 89 9.6 Una GUI interactiva de wxPython. . . . . . . . . . . . . . . . . . . . . . . . 92 9.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9.8 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9.8.1 Aplicación de GUI simple. . . . . . . . . . . . . . . . . . . . . 96 9.8.2 Interfaz GUI para un juego de tres en raya. . . . . . . . . . . 98 10 Aplicación de ejemplo de PyDraw wxPython. . . . . . . . . . . . . . . . . . . . . 99 10.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 10.2 La aplicación PyDraw. . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 10.3 La estructura de la aplicación. . . . . . . . . . . . . . . . . . . . . . 100 10.3.1 Arquitectura de modelo, vista y controlador. . . . . . . . . 101 10.3.2 Arquitectura PyDraw MVC. . . . . . . . . . . . . . . . . . . 102 10.3.3 Clases adicionales. . . . . . . . . . . . . . . . . . . . . . . . . 103 10.3.4 Relaciones de objetos. . . . . . . . . . . . . . . . . . . . . . . . 104 10.4 Las interacciones entre objetos. . . . . . . . . . . . . . . . . . . . . 105 10.4.1 La aplicación PyDraw. . . . . . . . . . . . . . . . . . . . . . . . . . . 105 10.4.2 El constructor PyDrawFrame. . . . . . . . . . . . . . . . 106 Contenido XV
10.4.3 Cambio del modo de aplicación. . . . . . . . . . . . . . . 106 10.4.4 Adición de un objeto gráfico. . . . . . . . . . . . . . . . . . . . 107 10.5 Las clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 10.5.1 La clase PyDrawConstants. . . . . . . . . . . . . . . . . . 108 10.5.2 La clase PyDrawFrame. . . . . . . . . . . . . . . . . . . . . 109 10.5.3 La clase PyDrawMenuBar. . . . . . . . . . . . . . . . . . 110 10.5.4 La clase PyDrawToolBar. . . . . . . . . . . . . . . . . . . 111 10.5.5 La clase PyDrawController. . . . . . . . . . . . . . . . . . 111 10.5.6 La clase del modelo de dibujo. . . . . . . . . . . . . . . . . . . . 113 10.5.7 La clase DrawingPanel. . . . . . . . . . . . . . . . . . . . . 113 10.5.8 La clase DrawingController. . . . . . . . . . . . . . . . . . 114 10.5.9 La clase de figuras. . . . . . . . . . . . . . . . . . . . . . . . . . 115 10.5.10 La Clase Cuadrada. . . . . . . . . . . . . . . . . . . . . . . . . . 115 10.5.11 La clase del círculo. . . . . . . . . . . . . . . . . . . . . . . . . . . 116 10.5.12 La clase de línea. . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 10.5.13 La clase de texto. . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 10.6 Referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 10.7 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 Parte II Juegos de computadora 11 Introducción a la Programación de Juegos. . . . . . . . . . . . . . . . . . . . . . . 121 11.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 11.2 Marcos de juegos y bibliotecas. . . . . . . . . . . . . . . . . . . . . 121 11.3 Desarrollo de juegos en Python. . . . . . . . . . . . . . . . . . . . . . . . . 122 11.4 Usando Pygame. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 11.5 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 12 Juegos de construcción con pygame. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 12.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 12.2 La superficie de visualización. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 12.3 Eventos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 12.3.1 Tipos de eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 12.3.2 Información del Evento . . . . . . . . . . . . . . . . . . . . . . . . . . 128 12.3.3 La cola de eventos. . . . . . . . . . . . . . . . . . . . . . . . . . 129 12.4 Una primera aplicación de pygame. . . . . . . . . . . . . . . . . . . . . . . . . . 130 12.5 Conceptos adicionales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 12.6 Una aplicación pygame más interactiva. . . . . . . . . . . . . . . . . 136 12.7 Enfoque alternativo para el procesamiento de dispositivos de entrada. . . . . . . . . 138 12.8 Módulos Pygame. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 12.9 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 xvi Contenido
13 StarshipMeteors pygame. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 13.1 Crear un juego de nave espacial. . . . . . . . . . . . . . . . . . . . . . . . . . 141 13.2 La clase de juego principal. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 13.3 La clase GameObject. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 13.4 Mostrando la nave estelar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 13.5 Moviendo la nave espacial. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 13.6 Adición de una clase de meteorito. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 13.7 Moviendo los meteoritos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 13.8 Identificación de una colisión. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 13.9 Identificación de una victoria. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 13.10 Aumentar el número de meteoritos. . . . . . . . . . . . . . . . . . . . . 154 13.11 Pausando el Juego. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 13.12 Visualización del mensaje Game Over. . . . . . . . . . . . . . . . . . . . 156 13.13 El juego StarshipMeteors. . . . . . . . . . . . . . . . . . . . . . . . . . 157 13.14 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 13.15 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 Parte III Pruebas 14 Introducción a las Pruebas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 14.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 14.2 Tipos de Pruebas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 14.3 ¿Qué debe probarse? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 14.4 Pruebas de Sistemas de Software. . . . . . . . . . . . . . . . . . . . . . . . . . . 167 14.4.1 Examen de la unidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 14.4.2 Pruebas de integración . . . . . . . . . . . . . . . . . . . . . . . . . 169 14.4.3 Pruebas del sistema. . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 14.4.4 Pruebas de instalación/actualización . . . . . . . . . . . . . . . . . . 170 14.4.5 Pruebas de humo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 14.5 Automatización de Pruebas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 14.6 Desarrollo dirigido por pruebas. . . . . . . . . . . . . . . . . . . . . . . . . . . 171 14.6.1 El ciclo TDD. . . . . . . . . . . . . . . . . . . . . . . . . . . 172 14.6.2 Complejidad de la prueba. . . . . . . . . . . . . . . . . . . . . . . . . . . 173 14.6.3 Refactorización. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 14.7 Diseño para Testabilidad. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 14.7.1 Reglas empíricas de comprobabilidad. . . . . . . . . . . . . . . . . . . 173 14.8 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 14.9 Recursos del libro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 15 Marco de pruebas de PyTest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 15.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 15.2 ¿Qué es PyTest? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 15.3 Configuración de PyTest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 15.4 Un ejemplo simple de PyTest. . . . . . . . . . . . . . . . . . . . . . . . . . . 176 Contenido xvii
15.5 Trabajando con PyTest. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 15.6 Pruebas parametrizadas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 15.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 15.8 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 dieciséis Burlarse de la prueba. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 16.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 16.2 ¿Por qué burlarse? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 16.3 ¿Qué es burlarse?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190 16.4 Conceptos comunes de Mocking Framework. . . . . . . . . . . . . . . . 191 16.5 Simulación de marcos para Python. . . . . . . . . . . . . . . . . . . . . . 192 16.6 La biblioteca unittest.mock. . . . . . . . . . . . . . . . . . . . . . . . . . . 192 16.6.1 Clases de Mock y Magic Mock. . . . . . . . . . . . . . . . 193 16.6.2 Los parcheadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 16.6.3 Burlarse de los objetos devueltos. . . . . . . . . . . . . . . . . . . 195 16.6.4 Se han llamado simulacros de validación. . . . . . . . . . . . 196 16.7 Uso de Mock y MagicMock. . . . . . . . . . . . . . . . . . . . . . . . 197 16.7.1 Poniendo nombre a tus simulacros. . . . . . . . . . . . . . . . . . . . . . . 197 16.7.2 Clases simuladas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 16.7.3 Atributos en clases simuladas. . . . . . . . . . . . . . . . . . . 198 16.7.4 Constantes burlonas. . . . . . . . . . . . . . . . . . . . . . . . . 199 16.7.5 Propiedades de burla. . . . . . . . . . . . . . . . . . . . . . . . . 199 16.7.6 Generación de excepciones con simulacros. . . . . . . . . . . . . . . . 199 16.7.7 Aplicación de parches a cada método de prueba. . . . . . . . . . . 200 16.7.8 Uso de Patch como administrador de contexto. . . . . . . . . . . . . 200 16.8 Simula donde lo usas. . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 16.9 Problemas con el pedido de parches. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 16.10 ¿Cuántas burlas? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 16.11 Consideraciones burlonas. . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 16.12 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 16.13 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 Parte IV Entrada/salida de archivos 17 Introducción a Archivos, Rutas y IO. . . . . . . . . . . . . . . . . . . . . . . . . 207 17.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 17.2 Atributos de archivo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 17.3 Caminos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 17.4 Entrada/salida de archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 17.5 Acceso secuencial versus acceso aleatorio. . . . . . . . . . . . . . . 213 17.6 Archivos y E/S en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 17.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 xviii Contenido
18 Lectura y escritura de archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 18.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 18.2 Obtención de Referencias a Archivos. . . . . . . . . . . . . . . . . . . . . . . . 215 18.3 Lectura de archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217 18.4 Iteración del contenido del archivo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 18.5 Escritura de datos en archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 18.6 Usando Archivos y con Sentencias. . . . . . . . . . . . . . . . . . . . . . 219 18.7 El módulo de entrada de archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 18.8 Renombrar archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 18.9 Eliminación de archivos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 18.10 Archivos de acceso aleatorio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 18.11 Directorios. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 18.12 Archivos temporales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224 18.13 Trabajando con Rutas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 18.14 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 18.15 Ejercicio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 19 Transmitir E/S. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 19.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 19.2 ¿Qué es una corriente? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 19.3 Secuencias de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232 19.4 IOBase. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233 19.5 Clases de E/S sin procesar/E/S sin búfer. . . . . . . . . . . . . . . . . . . . . . . 234 19.6 Clases de E/S binarias/E/S en búfer. . . . . . . . . . . . . . . . . . . . . . . . 234 19.7 Clases de flujo de texto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 19.8 Propiedades de flujo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 19.9 Corrientes de cierre. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 19.10 Volviendo a la función open(). . . . . . . . . . . . . . . . . . . . . . 238 19.11 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 19.12 Ejercicio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 20 Trabajar con archivos CSV. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 20.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 20.2 Archivos CSV. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 20.2.1 La clase de escritor de CSV. . . . . . . . . . . . . . . . . . . . . . 242 20.2.2 La clase de lector de CSV. . . . . . . . . . . . . . . . . . . . . . 243 20.2.3 La clase CSV DictWriter. . . . . . . . . . . . . . . . . . . 244 20.2.4 La clase CSV DictReader. . . . . . . . . . . . . . . . . . . 245 20.3 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 20.4 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246 21 Trabajar con archivos de Excel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 21.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 21.2 Archivos Excel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 Contenido xix
21.3 El Openpyxl. Clase de libro de trabajo. . . . . . . . . . . . . . . . . . . . . . 250 21.4 El Openpyxl. Objetos de hoja de trabajo. . . . . . . . . . . . . . . . . . . . . 250 21.5 Trabajando con Celdas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 21.6 Ejemplo de aplicación de creación de archivos de Excel. . . . . . . . . . . . . . . . . 251 21.7 Cargar un libro de trabajo desde un archivo de Excel. . . . . . . . . . . . . . . . 253 21.8 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 21,9 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254 22 Expresiones regulares en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 22.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 22.2 ¿Qué son las expresiones regulares? . . . . . . . . . . . . . . . . . . . . . . . 257 22.3 Patrones de expresiones regulares. . . . . . . . . . . . . . . . . . . . . . . . . . 258 22.3.1 Patrón Metacaracteres. . . . . . . . . . . . . . . . . . . . . . 259 22.3.2 Secuencias especiales. . . . . . . . . . . . . . . . . . . . . . . . . . 259 22.3.3 conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260 22.4 El módulo Python re. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261 22.5 Trabajar con expresiones regulares de Python. . . . . . . . . . . . . . . 261 22.5.1 Uso de cadenas en bruto. . . . . . . . . . . . . . . . . . . . . . . . . 261 22.5.2 Ejemplo sencillo. . . . . . . . . . . . . . . . . . . . . . . . . . . 262 22.5.3 El objeto de coincidencia. . . . . . . . . . . . . . . . . . . . . . . . . . 262 22.5.4 La función buscar(). . . . . . . . . . . . . . . . . . . . . . . 263 22.5.5 La función de coincidencia(). . . . . . . . . . . . . . . . . . . . . . . 264 22.5.6 La diferencia entre hacer coincidir y buscar. . . 265 22.5.7 La función findall(). . . . . . . . . . . . . . . . . . . . . . . 265 22.5.8 La función findter() . . . . . . . . . . . . . . . . . . . . . . 266 22.5.9 La función dividir(). . . . . . . . . . . . . . . . . . . . . . . . 266 22.5.10 La función sub(). . . . . . . . . . . . . . . . . . . . . . . . . 267 22.5.11 La función compilar(). . . . . . . . . . . . . . . . . . . . . . 268 22.6 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270 22.7 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270 Parte V Acceso a la base de datos 23 Introducción a las Bases de Datos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 23.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 23.2 ¿Que es una base de datos? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 23.2.1 Relaciones de datos. . . . . . . . . . . . . . . . . . . . . . . . . 276 23.2.2 El esquema de la base de datos. . . . . . . . . . . . . . . . . . . . . . . 277 23.3 SQL y Bases de Datos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 23.4 Lenguaje de manipulación de datos . . . . . . . . . . . . . . . . . . . . . . . . . 280 23.5 Transacciones en Bases de Datos. . . . . . . . . . . . . . . . . . . . . . . . . . . 281 23.6 Otras lecturas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282 XX Contenido
24 Python DB-API. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 24.1 Acceso a una base de datos desde Python. . . . . . . . . . . . . . . . . . . . 283 24.2 La DB-API. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 24.2.1 La función de conexión. . . . . . . . . . . . . . . . . . . . . . . 284 24.2.2 El objeto de conexión. . . . . . . . . . . . . . . . . . . . . . 284 24.2.3 El Objeto Cursor. . . . . . . . . . . . . . . . . . . . . . . . . 285 24.2.4 Asignaciones de tipos de base de datos a tipos de Python. . . 286 24.2.5 Generando Errores. . . . . . . . . . . . . . . . . . . . . . . . . . 286 24.2.6 Descripciones de filas. . . . . . . . . . . . . . . . . . . . . . . . . . 287 24.3 Transacciones en PyMySQL. . . . . . . . . . . . . . . . . . . . . . . . . . . 288 24.4 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 25 Módulo PyMySQL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 25.1 El Módulo PyMySQL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 25.2 Trabajando con el Módulo PyMySQL. . . . . . . . . . . . . . . . . . . 291 25.2.1 Importando el Módulo. . . . . . . . . . . . . . . . . . . . . . . 292 25.2.2 Conéctese a la base de datos. . . . . . . . . . . . . . . . . . . . . 292 25.2.3 Obtención del Objeto Cursor. . . . . . . . . . . . . . . . . . 293 25.2.4 Usando el Objeto Cursor. . . . . . . . . . . . . . . . . . . . . 293 25.2.5 Obtención de información sobre los resultados. . . . . . . . . 294 25.2.6 Obtención de resultados. . . . . . . . . . . . . . . . . . . . . . . . . . . 294 25.2.7 Cierra la Conexión. . . . . . . . . . . . . . . . . . . . . . . 295 25.3 Ejemplo completo de consulta de PyMySQL. . . . . . . . . . . . . . . . . . . 295 25.4 Inserción de datos en la base de datos. . . . . . . . . . . . . . . . . . . . . . . . 296 25.5 Actualización de datos en la base de datos. . . . . . . . . . . . . . . . . . . . . . . . 298 25.6 Eliminación de datos en la base de datos. . . . . . . . . . . . . . . . . . . . . . . . 299 25.7 Creación de tablas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300 25,8 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 25,9 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 Parte VI Inicio sesión 26 Introducción al registro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 26.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 26.2 ¿Por qué iniciar sesión? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 26.3 ¿Cuál es el propósito del registro? . . . . . . . . . . . . . . . . . . . . . . 306 26.4 ¿Qué debe registrar? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306 26.5 Qué no registrar. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 26.6 ¿Por qué no usar simplemente la impresión? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308 26.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 27 Iniciar sesión en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 27.1 El módulo de registro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 27.2 El Registrador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312 Contenido xxx
27.3 Control de la cantidad de información registrada. . . . . . . . . . . 313 27.4 Métodos de registro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315 27.5 Registrador predeterminado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316 27.6 Registradores de nivel de módulo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317 27.7 Jerarquía de registradores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318 27,8 Formateadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 27.8.1 Formateo de mensajes de registro. . . . . . . . . . . . . . . . . . . . 319 27.8.2 Salida de registro de formato. . . . . . . . . . . . . . . . . . . . . . 319 27,9 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322 27.10 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322 28 Registro avanzado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 28.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 28.2 manipuladores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 28.2.1 Configuración del controlador de salida raíz. . . . . . . . . . . . . . . 325 28.2.2 Configuración programática del controlador. . . . . . . . . . . 326 28.2.3 Manipuladores múltiples. . . . . . . . . . . . . . . . . . . . . . . . . . 328 28.3 filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329 28.4 Configuración del registrador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 28.5 Consideraciones de rendimiento. . . . . . . . . . . . . . . . . . . . . . . . . . 333 28.6 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334 Parte VII Concurrencia y Paralelismo 29 Introducción a la Concurrencia y el Paralelismo. . . . . . . . . . . . . . . . . 337 29.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 29.2 concurrencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 29.3 Paralelismo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 29.4 Distribución. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340 29.5 Computación Grid. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340 29.6 Concurrencia y Sincronización. . . . . . . . . . . . . . . . . . . . . 342 29.7 Orientación a Objetos y Concurrencia. . . . . . . . . . . . . . . . . . . . 342 29.8 Hilos V Procesos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343 29,9 Algo de terminología. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 29.10 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 30 Enhebrar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 30.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 30.2 Hilos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 30.3 Estados de subprocesos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 30.4 Creación de un hilo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 30.5 Creación de instancias de la clase Thread. . . . . . . . . . . . . . . . . . . . . . . . 349 30.6 La clase de hilo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350 XXII Contenido
30.7 Las funciones del módulo de enhebrado. . . . . . . . . . . . . . . . . . . . . . 352 30.8 Pasar argumentos a un hilo. . . . . . . . . . . . . . . . . . . . . . . 352 30,9 Ampliación de la clase Thread. . . . . . . . . . . . . . . . . . . . . . . . . . 354 30.10 Hilos de demonio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355 30.11 Nomenclatura de hilos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356 30.12 Subproceso de datos locales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357 30.13 temporizadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358 30.14 El bloqueo de intérprete global. . . . . . . . . . . . . . . . . . . . . . . . . . 359 30.15 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360 30.16 Ejercicio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360 31 Multiprocesamiento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363 31.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363 31.2 La clase de proceso. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363 31.3 Trabajando con la Clase de Proceso. . . . . . . . . . . . . . . . . . . . . . . 365 31.4 Formas alternativas de iniciar un proceso. . . . . . . . . . . . . . . . . . . . 366 31.5 Uso de una piscina. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 31.6 Intercambio de datos entre procesos. . . . . . . . . . . . . . . . . . . 372 31.7 Compartir estado entre procesos. . . . . . . . . . . . . . . . . . . . . . 374 31.7.1 Procesar memoria compartida. . . . . . . . . . . . . . . . . . . . . 374 31.8 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 31,9 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376 32 Sincronización entre subprocesos/procesos. . . . . . . . . . . . . . . . . . . . . . . 377 32.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 32.2 Uso de una barrera. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 32.3 Señalización de eventos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380 32.4 Sincronización de código concurrente. . . . . . . . . . . . . . . . . . . . . . . 382 32.5 Bloqueos de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383 32.6 Condiciones de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386 32.7 Semáforos de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388 32.8 La clase de cola concurrente. . . . . . . . . . . . . . . . . . . . . . . . . 389 32,9 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 32.10 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 33 futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 33.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 33.2 La necesidad de un futuro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395 33.3 Futuros en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396 33.3.1 Creación Futura. . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 33.3.2 Ejemplo simple de futuro. . . . . . . . . . . . . . . . . . . . . . 397 33.4 Ejecución de futuros múltiples. . . . . . . . . . . . . . . . . . . . . . . . . . . 399 33.4.1 Esperando a que se completen todos los futuros. . . . . . . . . . . . 400 33.4.2 Procesando resultados como completados. . . . . . . . . . . . . . . 402 Contenido XXIII
33.5 Procesamiento de resultados futuros utilizando una devolución de llamada. . . . . . . . . . . . . . 403 33.6 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405 33.7 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405 34 Concurrencia con AsyncIO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407 34.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407 34.2 E/S asíncrona. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407 34.3 Bucle de eventos de E/S asíncrono. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 408 34.4 Las palabras clave Async y Await. . . . . . . . . . . . . . . . . . . . . . 409 34.4.1 Usando Async y Await . . . . . . . . . . . . . . . . . . . . . 409 34.5 Tareas de E/S asíncronas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 34.6 Ejecución de varias tareas. . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 34.6.1 Recopilación de resultados de varias tareas. . . . . . . . . . . 414 34.6.2 Manejo de los resultados de las tareas a medida que se realizan Disponible . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415 34.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416 34.8 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417 Parte VIII Programación reactiva 35 Introducción a la programación reactiva. . . . . . . . . . . . . . . . . . . . . . . . 421 35.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 35.2 ¿Qué es una aplicación reactiva? . . . . . . . . . . . . . . . . . . . . . . 421 35.3 El proyecto ReactiveX. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422 35.4 El patrón del observador. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422 35.5 Observables fríos y calientes. . . . . . . . . . . . . . . . . . . . . . . . . . . 423 35.5.1 Observables fríos. . . . . . . . . . . . . . . . . . . . . . . . . . 424 35.5.2 Observables calientes. . . . . . . . . . . . . . . . . . . . . . . . . . . 424 35.5.3 Implicaciones de observables fríos y calientes. . . . . . . . 424 35.6 Diferencias entre la programación dirigida por eventos y Programación Reactiva. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425 35.7 Ventajas de la Programación Reactiva. . . . . . . . . . . . . . . . . . 425 35.8 Desventajas de la programación reactiva. . . . . . . . . . . . . . . . 426 35,9 El Marco de Programación Reactiva RxPy. . . . . . . . . . . . . 426 35.10 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426 35.11 Referencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427 36 RxPy Observables, Observadores y Sujetos . . . . . . . . . . . . . . . . . . 429 36.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429 36.2 Observables en RxPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429 36.3 Observadores en RxPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430 36.4 Múltiples Suscriptores/Observadores. . . . . . . . . . . . . . . . . . . . . . . 432 36.5 Sujetos en RxPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433 XXIV Contenido
36.6 Concurrencia de observadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435 36.6.1 Programadores disponibles. . . . . . . . . . . . . . . . . . . . . . . . 437 36.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438 36.8 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438 37 Operadores RxPy. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439 37.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439 37.2 Operadores de programación reactiva. . . . . . . . . . . . . . . . . . . . . . 439 37.3 Operadores de tuberías. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440 37.4 Operadores Creacionales. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441 37.5 Operadores transformacionales. . . . . . . . . . . . . . . . . . . . . . . . . . 441 37.6 Operadores combinatorios. . . . . . . . . . . . . . . . . . . . . . . . . . . . 443 37.7 Operadores de filtrado. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444 37.8 Operadores matemáticos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445 37,9 Operadores de encadenamiento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446 37.10 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448 37.11 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448 Parte IX Programación de red 38 Introducción a los Sockets y los Servicios Web. . . . . . . . . . . . . . . . . . . 451 38.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 38.2 Enchufes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 38.3 Servicios web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452 38.4 Servicios de direccionamiento. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452 38.5 servidor local. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453 38.6 Números de puerto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454 38.7 IPv4 frente a IPv6. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455 38.8 Sockets y Servicios Web en Python. . . . . . . . . . . . . . . . . . . 455 38,9 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 456 39 Enchufes en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457 39.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457 39.2 Comunicación socket a socket. . . . . . . . . . . . . . . . . . . . . . 457 39.3 Configuración de una conexión. . . . . . . . . . . . . . . . . . . . . . . . . . . . 458 39.4 Un ejemplo de aplicación de servidor de cliente. . . . . . . . . . . . . . . . . . 458 39.4.1 La Estructura del Sistema. . . . . . . . . . . . . . . . . . . . . . . 458 39.4.2 Implementación de la aplicación del servidor. . . . . . . . . . . . 459 39.5 Tipos de sockets y dominios. . . . . . . . . . . . . . . . . . . . . . . . . . 461 39.6 Implementación de la aplicación cliente. . . . . . . . . . . . . . . . . . . 461 39.7 El módulo del servidor de socket. . . . . . . . . . . . . . . . . . . . . . . . . . . 463 39.8 Servidor HTTP. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465 39.9 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 39.10 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 Contenido xiv
40 Servicios Web en Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 40.1 Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 40.2 Servicios REST. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 40.3 Una API RESTful. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472 40.4 Marcos web de Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 40.5 matraz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 40.6 Hola mundo en frasco. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 40.6.1 Usando JSON. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474 40.6.2 Implementación de un servicio web Flask. . . . . . . . . . . . . 475 40.6.3 Un servicio sencillo. . . . . . . . . . . . . . . . . . . . . . . . . . 475 40.6.4 Suministro de información de enrutamiento. . . . . . . . . . . . . . . . 476 40.6.5 Ejecución del Servicio. . . . . . . . . . . . . . . . . . . . . . . . 477 40.6.6 Invocación del Servicio. . . . . . . . . . . . . . . . . . . . . . . . 478 40.6.7 La solución definitiva . . . . . . . . . . . . . . . . . . . . . . . . . 479 40.7 Recursos en línea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479 41 Servicio web de la librería. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481 41.1 Creación de un servicio de librería Flask. . . . . . . . . . . . . . . . . . . . 481 41.2 El diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 481 41.3 El modelo de dominio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482 41.4 Codificación de libros en JSON. . . . . . . . . . . . . . . . . . . . . . . . . . 484 41.5 Configuración de los servicios GET. . . . . . . . . . . . . . . . . . . . . . . . . 486 41.6 Eliminación de un libro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 488 41.7 Adición de un nuevo libro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 489 41.8 Actualización de un libro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491 41,9 ¿Qué pasa si nos equivocamos? . . . . . . . . . . . . . . . . . . . 492 41.10 Listado de servicios de librería. . . . . . . . . . . . . . . . . . . . . . . . . . . 494 41.11 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 497 xxi Contenido
Capítulo 1 Introducción 1.1 Introducción A lo largo de los años, he escuchado a muchas personas decir que Python es un lenguaje fácil de aprender. y que Python es también un lenguaje simple. Hasta cierto punto, ambas afirmaciones son verdaderas; pero sólo hasta cierto punto. Si bien el núcleo del lenguaje Python es fácil de aprender y relativamente simple (en parte gracias a su consistencia); la gran riqueza de las construcciones del lenguaje y la flexibilidad disponible puede ser abrumadora. Además, el entorno de Python, su ecosistema, la variedad de bibliotecas disponibles, las opciones que a menudo compiten entre sí etc., puede hacer que pasar al siguiente nivel sea abrumador. Una vez que haya aprendido los elementos centrales del idioma, como la forma en que las clases y el trabajo de herencia, cómo funcionan las funciones, qué son los protocolos y la Base Abstracta Clases, etc. ¿Adónde vas ahora? El objetivo de este libro es profundizar en esos próximos pasos. El libro está organizado en ocho temas diferentes:
- Gráficos por computadora. El libro cubre Gráficos por computadora y Computación Arte generado en Python, así como interfaces gráficas de usuario y gráficos/ Gráficos a través de MatPlotLib.
- Programación de Juegos. Este tema se cubre usando la biblioteca pygame.
- Pruebas y Simulacros. La prueba es un aspecto importante de cualquier desarrollo de software. opmento; este libro presenta las pruebas en general y el módulo PyTest en detalle. También considera la burla dentro de las pruebas, incluido qué y cuándo burlarse.
- Entrada/salida de archivos. El libro cubre la lectura y escritura de archivos de texto, así como lectura y escritura de archivos CSV y Excel. Aunque no está estrictamente relacionado con el archivo entrada, las expresiones reguladoras se incluyen en esta sección, ya que se pueden utilizar para procesar datos textuales contenidos en archivos.
- Acceso a la base de datos. El libro introduce las bases de datos y la base de datos relacional en particular. Luego presenta el estándar de acceso a la base de datos Python DB-API y © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_1 1
una implementación de este estándar, el módulo PyMySQL utilizado para acceder a un Base de datos mysql. 6. Registro. Un tema que a menudo se pasa por alto es el del registro. Por lo tanto, el libro introduce duce el registro de la necesidad de registro, qué registrar y qué no registrar, así como el módulo de registro de Python. 7. Concurrencia y Paralelismo. El libro proporciona una amplia cobertura de temas de simultaneidad, incluidos subprocesos, procesos e interprocesos o procesos sincronización. También presenta Futures y AsyncIO. 8. Programación Reactiva. Esta sección del libro presenta Reactive Programación utilizando la biblioteca de programación reactiva PyRx. 9. Programación de Redes. El libro concluye presentando socket y web comunicaciones de servicio en Python. Cada sección es introducida por un capítulo que proporciona los antecedentes y la clave conceptos de ese tema. Los capítulos siguientes cubren varios aspectos del tema. Por ejemplo, el primer tema cubierto es Gráficos por computadora. Esta sección tiene un capítulo introductorio sobre gráficos por computadora en general. Luego introduce el Biblioteca Turtle Graphics Python que se puede utilizar para generar una visualización gráfica. El siguiente capítulo considera el tema del Arte Generado por Computadora y los usos la biblioteca Turtle Graphics para ilustrar estas ideas. Así son varios ejemplos presentado que podría ser considerado arte. El capítulo concluye presentando la conocido Koch Snowflake y el conjunto Mandelbrot Fractal. A esto le sigue un capítulo que presenta la biblioteca MatPlotLib utilizada para generar generación de diagramas y gráficos en 2D y 3D (como un gráfico de líneas, un gráfico de barras o un gráfico de dispersión) grafico). La sección concluye con un capítulo sobre interfaces gráficas de usuario (o GUI) utilizando la biblioteca wxpython. Este capítulo explora lo que queremos decir con una GUI y algunas de las alternativas disponibles en Python para crear una GUI. Los temas posteriores siguen un patrón similar. Cada capítulo orientado a la programación o la biblioteca también incluye numerosos ejemplos programas que se pueden descargar desde el repositorio de GutHub y ejecutar. Estos Los capítulos también incluyen uno o más ejercicios de fin de capítulo (con ejemplos de soluciones). también en el repositorio de GutHub). Los temas dentro del libro se pueden leer en su mayoría de forma independiente unos de otros. Este permite al lector sumergirse en las áreas temáticas cuando sea necesario. por ejemplo, el La sección de entrada/salida de archivos y la sección de acceso a la base de datos se pueden leer de forma independiente. mutuamente (aunque en este caso evaluar ambas tecnologías puede ser útil en la selección de un enfoque apropiado para adoptar para el largo plazo persistente almacenamiento de datos en un sistema particular). Dentro de cada apartado suele haber dependencias, por ejemplo es necesario para comprender la biblioteca de pygame de ‘Construir juegos con pygame’ capítulo introductorio, antes de explorar el estudio de caso trabajado presentado por el capítulo sobre el juego StarshipMeteors. Del mismo modo es necesario haber leído el Capítulos Threading y Multiprocessing antes de leer Inter Thread/Process Capítulo de sincronización. 2 1 Introducción
Parte I Gráficos de computadora
Capitulo 2 Introducción a los gráficos por computadora 2.1 Introducción Los gráficos por computadora están en todas partes; están en tu TV, en anuncios de cine, el núcleo de muchas películas, en su tableta o teléfono móvil y ciertamente en su PC o Mac así como en el salpicadero de tu coche, en tu reloj inteligente y en los niños juguetes electronicos Sin embargo, ¿qué queremos decir con el término gráficos por computadora? El término se remonta a un momento en que muchas (la mayoría) de las computadoras eran puramente textuales en términos de su entrada y salida y muy pocas computadoras podrían generar pantallas gráficas y mucho menos manejar la entrada a través de una pantalla de este tipo. Sin embargo, en términos de este libro tomamos el término Gráficos por computadora para incluir la creación de interfaces gráficas de usuario (o GUI), gráficos y diagramas como gráficos de barras o diagramas de línea de datos, gráficos en computadora juegos (como Space Invaders o Flight Simulator) así como la generación de 2D y escenas o imágenes en 3D. También usamos el término para incluir Arte generado por computadora. La disponibilidad de Gráficos por Computador es muy importante para la gran aceptación de sistemas informáticos por no informáticos en los últimos 40 años. es en parte gracias a la accesibilidad de los sistemas informáticos a través de interfaces gráficas informáticas que casi todo el mundo utiliza ahora algún tipo de sistema informático (ya sea un PC, un tableta, un teléfono móvil o un televisor inteligente). Una interfaz gráfica de usuario (GUI) puede capturar la esencia de una idea o un situación, a menudo evitando la necesidad de un largo pasaje de texto o comandos textuales. Él es también porque una imagen vale más que mil palabras; siempre y cuando sea lo correcto imagen. En muchas situaciones donde las relaciones entre grandes cantidades de información debe transmitirse, es mucho más fácil para el usuario asimilar esto gráficamente que textualmente. De manera similar, a menudo es más fácil transmitir algún significado manipulando algunas entidades del sistema en pantalla, que por combinaciones de comandos de texto. Por ejemplo, un gráfico bien elegido puede aclarar información que es difícil de determinar a partir de una tabla de los mismos datos. A su vez, un juego de estilo aventura puede © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_2 5
volverse atractivo e inmersivo con gráficos de computadora que están en marcado contraste con las versiones textuales de los años ochenta. Esto destaca las ventajas de un presentación visual frente a una puramente textual. 2.2 Fondo Todo sistema de software interactivo tiene una interfaz hombre-computadora, ya sea una sistema de una sola línea de texto o una pantalla gráfica avanzada. Es el vehiculo utilizado por desarrolladores para obtener información de sus usuarios y, a su vez, cada usuario tiene enfrentar algún tipo de interfaz de computadora para realizar cualquier operación de computadora deseada operación. Históricamente, los sistemas informáticos no tenían una interfaz gráfica de usuario y rara vez generó una vista gráfica. Estos sistemas de los años 60, 70 y 80 normalmente centrado en tareas numéricas o de procesamiento de datos. Se accedía a través de verde o pantallas grises en un terminal orientado a texto. Había poca o ninguna oportunidad para salida gráfica. Sin embargo, durante este período varios investigadores de laboratorios como Stanford, MIT, Bell Telephone Labs y Xerox estaban analizando las posibilidades que los gráficos los sistemas pueden ofrecer a las computadoras. De hecho, ya en 1963 Ivan Sutherland mostró que los gráficos interactivos por computadora eran factibles con su Ph.D. tesis sobre la Sistema de bloc de dibujo. 2.3 La era de la computadora gráfica Las pantallas gráficas de computadora y las interfaces gráficas interactivas se convirtieron en algo común. medio de interacción humano-computadora durante la década de 1980. Tales interfaces pueden ahorrar un usuario de la necesidad de aprender comandos complejos. Son menos propensos a intimidar ingenuos en computación y pueden proporcionar una gran cantidad de información rápidamente en una forma que pueden ser fácilmente asimilados por el usuario. El uso generalizado de interfaces gráficas de alta calidad (como las proporcionadas por el Apple Macintosh y la primera interfaz de Windows) llevó a muchos usuarios de computadoras esperar tales interfaces para cualquier software que utilicen. De hecho, estos sistemas allanaron el camino para el tipo de interfaz que ahora es omnipresente en PC, Mac, cajas de Linux, tabletas y teléfonos inteligentes, etc. Esta interfaz gráfica de usuario se basa en WIMP paradigma (ventanas, íconos, menús y punteros) que ahora es el tipo predominante de interfaz gráfica de usuario en uso hoy en día. La principal ventaja de cualquier sistema basado en ventanas, y particularmente de un WIMP ambiente, es que requiere sólo una pequeña cantidad de capacitación del usuario. No hay necesita aprender comandos complejos, ya que la mayoría de las operaciones están disponibles como íconos, operaciones en iconos, acciones del usuario (como deslizar) o desde opciones de menú, y son fácil de usar. (Un icono es un pequeño objeto gráfico que suele simbolizar una 6 2 Introducción a los gráficos por computadora
operación o de una entidad más grande, como un programa de aplicación o un archivo). En general, Los sistemas basados en WIMP son fáciles de aprender, intuitivos de usar, fáciles de retener y fácil de trabajar. Estos sistemas WIMP están ejemplificados por la interfaz Apple Macintosh (ver Goldberg y Robson, así como Tesler), que fue influenciado por el pionero trabajo realizado en el Centro de Investigación de Palo Alto en la Xerox Star Machine. Fue, sin embargo, el Macintosh que trajo tales interfaces al mercado masivo, y primero ganó aceptación para ellos como herramientas para los negocios, el hogar y la industria. esta interfaz transformó la forma en que los humanos esperaban interactuar con sus computadoras, convirtiéndose en un estándar de facto, lo que obligó a otros fabricantes a proporcionar interfaces en sus propias máquinas, por ejemplo, Microsoft Windows para PC. Este tipo de interfaz se puede aumentar proporcionando manipulación directa gráficos. Estos son gráficos que pueden ser tomados y manipulados por el usuario, usando un mouse, para realizar alguna operación o acción. Los iconos son una versión simple de esto, la “apertura” de un icono hace que la aplicación asociada se ejecute o la ventana asociada que se mostrará. 2.4 Gráficos Interactivos y No Interactivos Los gráficos por computadora se pueden subdividir ampliamente en dos categorías: • Gráficos por computadora no interactivos • Computación Gráfica Interactiva. En Gráficos por computadora no interactivos (también conocidos como Gráficos por computadora pasivos) un la imagen es generada por una computadora típicamente en una pantalla de computadora; esta imagen puede ser visto por el usuario (sin embargo, no puede interactuar con la imagen). Ejemplos de Los gráficos no interactivos que se presentan más adelante en este libro incluyen los generados por computadora. Arte en el que se genera una imagen utilizando la biblioteca Python Turtle Graphics. Semejante una imagen puede ser vista por el usuario pero no modificada. Otro ejemplo podría ser un gráfico de barras básico generado con MatPlotLib que presenta un conjunto de datos. Los gráficos interactivos por computadora, por el contrario, involucran al usuario interactuando con el imagen que se muestra en la pantalla de alguna manera, esto podría ser para modificar los datos que se están mostrar o cambiar la forma en que se representa la imagen, etc. Es tipificado por interfaces gráficas de usuario (GUI) interactivas en las que un usuario interactúa con menús, botones, campo de entrada, controles deslizantes, barras de desplazamiento, etc. Sin embargo, otras pantallas visuales pueden también ser interactivo. Por ejemplo, se podría usar un control deslizante con un gráfico de MatplotLib. Este la pantalla podría presentar el número de ventas realizadas en una fecha determinada; como es el deslizador se mueve para que los datos cambien y el gráfico se modifique para mostrar diferentes conjuntos de datos. Otro ejemplo está representado por todos los juegos de computadora que son inherentemente interactivos y la mayoría, si no todos, actualizan su visualización en respuesta a algún usuario entradas. Por ejemplo, en el clásico juego de simulador de vuelo, a medida que el usuario mueve el joystick o mouse, el plano simulado se mueve en consecuencia y la pantalla presentada a las actualizaciones de usuario. 2.3 La era de la computadora gráfica 7
2.5 Píxeles Un concepto clave para todos los sistemas de gráficos por computadora es el píxel. Pixel fue originalmente un palabra formada al combinar y acortar las palabras picture (o pix) y ele- mento Un píxel es una celda en la pantalla de la computadora. Cada celda representa un punto en la pantalla. El tamaño de este punto o celda y el número de celdas disponibles variará dependiendo del tipo, tamaño y resolución de la pantalla. Por ejemplo, fue Es común que las primeras PC con Windows tengan una resolución de pantalla de 640 por 480 (usando un tarjeta gráfica VGA). Esto se relaciona con el número de píxeles en términos de ancho y altura. Esto significaba que había 640 píxeles en la pantalla con 480 filas de píxeles hacia abajo de la pantalla. Por el contrario, las pantallas de TV 4K de hoy tienen 4096 por 2160 píxeles El tamaño y la cantidad de píxeles disponibles afectan la calidad de la imagen como presentado a un usuario. Con pantallas de menor resolución (con menos píxeles individuales) la imagen puede aparecer bloqueada o mal definida; donde como con una resolución más alta puede aparecer nítido y claro. Se puede hacer referencia a cada píxel por su ubicación en la cuadrícula de visualización. Al llenar un píxeles en la pantalla con diferentes colores se pueden crear varias imágenes/visualizaciones. Por ejemplo, en la siguiente imagen se ha rellenado un solo píxel en la posición 4 por 4: Una secuencia de píxeles puede formar una línea, un círculo o cualquier número de formas diferentes. Sin embargo, dado que la cuadrícula de píxeles se basa en puntos individuales, una línea diagonal o un Es posible que el círculo deba utilizar varios píxeles que, al hacer zoom, pueden tener irregularidades. bordes Por ejemplo, la siguiente imagen muestra parte de un círculo en el que tenemos aumentado: 8 2 Introducción a los gráficos por computadora
Cada píxel puede tener asociado un color y una transparencia. El rango de los colores disponibles dependen del sistema de visualización que se utilice. por ejemplo mono Las pantallas cromadas solo permiten blanco y negro, mientras que las pantallas en escala de grises solo permiten permite que se muestren varios tonos de gris. En los sistemas modernos suele ser posible representar una amplia gama de colores utilizando los códigos de color RGB tradicionales (donde R representa rojo, G representa verde y B representa azul). En esto la codificación sólida El rojo se representa mediante un código como [255, 0, 0] donde como sólido El verde está representado por [0, 255, 0] y el azul sólido por [0, 0, 255]. Basado en esta idea varios tonos se pueden representar mediante la combinación de estos códigos, como Orange que podría estar representado por [255, 150, 50]. Esto se ilustra a continuación para un conjunto de Colores RGB usando diferentes valores de rojo, verde y azul: Además, es posible aplicar una transparencia a un píxel. Esto se usa para indicar qué tan sólido debe ser el color de relleno. La cuadrícula anterior ilustra el efecto de aplicando un 75%, 50% y 25% de transparencia a los colores que se muestran usando Python Biblioteca de interfaz gráfica de usuario wxPython. En esta biblioteca, la transparencia se denomina alfa. valor opaco. Puede tener valores en el rango 0-255 donde 0 es completamente trans- padre y 255 es completamente sólido. 2.5 Píxeles 9
2.6 Mapa de bits frente a gráficos vectoriales Hay dos formas de generar una imagen/visualización a través de los píxeles de la pantalla. Un enfoque se conoce como gráficos de mapa de bits (o de trama) y el otro se conoce como gráficos vectoriales En el enfoque de mapas de bits, cada píxel se asigna a los valores para mostrarse para crear la imagen. En el enfoque de gráficos vectoriales formas geométricas se describen (como líneas y puntos) y luego se representan en una pantalla. Los gráficos de trama son más sencillos, pero los gráficos vectoriales proporcionan mucha más flexibilidad y escalabilidad 2.7 almacenamiento en búfer Un problema para las pantallas gráficas interactivas es la capacidad de cambiar la pantalla como suave y limpiamente como sea posible. Si una pantalla es entrecortada o parece saltar de una imagen a otra, los usuarios lo encontrarán incómodo. Por lo tanto, es común a dibujó la siguiente pantalla en alguna estructura de memoria; a menudo denominado amortiguador. Este búfer se puede representar en la pantalla una vez que se ha visualizado toda la imagen. creado. Por ejemplo, Turtle Graphics permite al usuario definir cuántos cambios debe hacerse en la pantalla antes de que se represente (o dibuje) en la pantalla. Este puede acelerar significativamente el rendimiento de una aplicación gráfica. En algunos casos, los sistemas utilizarán dos búferes; a menudo denominado doble almacenamiento en búfer. En este enfoque, un búfer se representa o se dibuja en la pantalla mientras que el se está actualizando otro búfer. Esto puede mejorar significativamente el rendimiento general. funcionamiento del sistema, ya que las computadoras modernas pueden realizar cálculos y generar datos mucho más rápido de lo que normalmente se pueden dibujar en una pantalla. 2.8 Python y gráficos por computadora En el resto de esta sección del libro veremos cómo generar computadoras gráficos utilizando la biblioteca Python Turtle Graphics. También discutiremos el uso de este biblioteca para crear arte generado por computadora. A continuación exploraremos la Biblioteca MatPlotLib utilizada para generar gráficos y diagramas de datos como gráficos de barras, dispersión gráficos, diagramas de líneas y mapas de calor, etc. Luego exploraremos el uso de Python bibliotecas para crear GUI usando menús, campos, tablas, etc. 10 2 Introducción a los gráficos por computadora
2.9 Referencias En este capítulo se hace referencia a lo siguiente: • ES DECIR. Sutherland, Sketchpad: un sistema de comunicación gráfica hombre-máquina (Cortesía Laboratorio de Computación, Universidad de Cambridge UCAM-CL-TR-574, septiembre de 2003), enero de 1963. • D.C. Smith, C. Irby, R. Kimball, B. Verplank, E. Harslem, Diseñando la estrella interfaz de usuario. BYTE 7(4), 242–282 (1982). 2.10 Recursos en línea Lo siguiente proporciona material de lectura adicional: • https://en.wikipedia.org/wiki/Sketchpad Ivan Sutherlands Sketchpad de 1963. • http://images.designworldonline.com.s3.amazonaws.com/CADhistory/ Sketchpad_A_Man-Machine_Graphical_Communication_System_Jan63.pdf Iván Sutherlands Ph.D. 1963. • https://en.wikipedia.org/wiki/Xerox_Star La computadora Xerox Star y la GUI. 2.9 Referencias 11
Capítulo 3 Gráficos de tortuga pitón 3.1 Introducción Python está muy bien soportado en términos de bibliotecas de gráficos. Uno de los más ampliamente bibliotecas gráficas usadas es la biblioteca Turtle Graphics presentada en este capítulo. Este es en parte porque es fácil de usar y en parte porque lo proporciona predeterminado con el entorno de Python (y esto no necesita instalar ningún bibliotecas adicionales para usarlo). El capítulo concluye considerando brevemente una serie de otras bibliotecas gráficas incluido PyOpen GL. La biblioteca PyOpenGL se puede utilizar para crear sofisticados Escenas 3D. 3.2 La biblioteca de gráficos de tortugas 3.2.1 El Módulo Tortuga Esto proporciona una biblioteca de características que permiten lo que se conoce como gráficos vectoriales para ser creado. Los gráficos vectoriales se refieren a las líneas (o vectores) que se pueden dibujar en el pantalla. El área de dibujo a menudo se denomina plano de dibujo o tablero de dibujo. y tiene la idea de las coordenadas x, y. La biblioteca Turtle Graphics está pensada solo como una herramienta de dibujo básica; otro Las bibliotecas se pueden utilizar para dibujar gráficos de dos y tres dimensiones (como MatPlotLib), pero tienden a centrarse en tipos específicos de pantallas gráficas. La idea detrás del módulo Turtle (y su nombre) se deriva del programa Logo. lenguaje de programación de los años 60 y 70 que fue diseñado para introducir ming a los niños. Tenía una tortuga en pantalla que podía ser controlada por comandos como adelante (lo que movería a la tortuga hacia adelante), derecha (lo que haría girar a la tortuga por un cierto número de grados), hacia la izquierda (que gira la tortuga hacia la izquierda por un cierto número de grados). © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_3 13
grados), etc. Esta idea ha continuado en la biblioteca Python Turtle Graphics actual. donde comandos como turtle.forward(10) mueven la tortuga (o el cursor a medida que avanza). es ahora) avanzar 10 píxeles, etc. Combinando estos aparentemente simples comandos, es posible crear formas complejas intrincadas y silenciosas. 3.2.2 Gráficos básicos de tortugas Aunque el módulo tortuga está integrado en Python 3, es necesario importar el módulo antes de usarlo: De hecho, hay dos formas de trabajar con el módulo tortuga; uno es para usar las clases disponibles con la biblioteca y la otra es usar un conjunto más simple de funciones que ocultan las clases y los objetos. En este capítulo nos centraremos en el conjunto de funciones puede usar para crear dibujos con la biblioteca Turtle Graphics. Lo primero que haremos será configurar la ventana que usaremos para nuestros dibujos; la clase TurtleScreen es el padre de todas las implementaciones de pantalla utilizadas para sea cual sea el sistema operativo en el que se esté ejecutando. Si está utilizando las funciones proporcionadas por el módulo tortuga, entonces la pantalla El objeto se inicializa según corresponda para su sistema operativo. Esto significa que usted solo puede enfocarse en las siguientes funciones para configurar el diseño/pantalla como esta La pantalla puede tener un título, un tamaño, una ubicación de inicio, etc. Las funciones clave son: • setup(ancho, alto, startx, starty) Establece el tamaño y la posición de la ventana/pantalla principal. Los parámetros son: – ancho: si es un número entero, un tamaño en píxeles, si es un flotante, una fracción de la pantalla; el valor predeterminado es el 50 % de la pantalla. – altura: si es un número entero, la altura en píxeles; si es un flotante, una fracción de la pantalla; el valor predeterminado es el 75 % de la pantalla. – startx: si es positivo, la posición inicial en píxeles desde el borde izquierdo del pantalla, si es negativo desde el borde derecho, si no, centrar la ventana horizontalmente. – starty: si es positivo, la posición inicial en píxeles desde el borde superior de la pantalla, si es negativo desde el borde inferior, si no, centrar la ventana verticalmente. • title(titlestring) establece el título de la pantalla/ventana. • exitonclick() cierra la pantalla/ventana de gráficos de la tortuga cuando el uso hace clic en la pantalla. • bye() cierra la pantalla/ventana de gráficos de la tortuga. • done() inicia el ciclo del evento principal; esta debe ser la última declaración en una tortuga programa de gráficos importar tortuga 14 3 Gráficos de tortuga pitón
• velocidad(velocidad) la velocidad de dibujo a utilizar, el valor predeterminado es 3. Cuanto mayor sea la cuanto más rápido se lleva a cabo el dibujo, se aceptan valores en el rango de 0 a 10. • turtle.tracer(n = Ninguno) Esto se puede usar para actualizar por lotes la tortuga pantalla de gráficos Es muy útil cuando un dibujo se vuelve grande y complejo. Por configurando el número (n) a un número grande (digamos 600) entonces 600 elementos serán dibujado en la memoria antes de que la pantalla real se actualice de una sola vez; esto puede significar acelerar significativamente la generación de, por ejemplo, una imagen fractal. cuando se llama sin argumentos, devuelve el valor actualmente almacenado de n. • turtle.update() Realiza una actualización de la pantalla de la tortuga; esto debería ser se llama al final de un programa cuando se ha utilizado tracer(), ya que garantizará que todos los elementos han sido dibujados incluso si el umbral del trazador aún no ha sido alcanzó. • pencolor(color) utilizado para establecer el color utilizado para dibujar líneas en la pantalla; el color se puede especificar de muchas maneras, incluido el uso de un conjunto de colores con nombre como ‘rojo’, ‘azul’, ‘verde’ o usando los códigos de color RGB o especificando el colorear usando números hexadecimales. Para más información sobre los colores nombrados y códigos de color RGB para usar, consulte https://www.tcl.tk/man/tcl/TkCmd/colors.htm. Tenga en cuenta que todos los métodos de color usan ortografía estadounidense, por ejemplo, este método es pencolor (no pencolour). • color de relleno (color) utilizado para establecer el color que se utilizará para rellenar áreas cerradas dentro lineas dibujadas ¡Otra vez tenga en cuenta la ortografía del color! El siguiente fragmento de código ilustra algunas de estas funciones: Ahora podemos ver cómo dibujar una forma en la pantalla. El cursor en la pantalla tiene varias propiedades; estos incluyen el actual color de dibujo del lápiz que mueve el cursor, sino también su posición actual (en el coordenadas x, y de la pantalla) y la dirección en la que se encuentra actualmente. Tenemos importar tortuga
establezca un título para su ventana de lienzo
tortuga.title(‘Mi animación de tortuga’)
configurar el tamaño de la pantalla (en píxeles)
establecer el punto de inicio de la tortuga (0, 0)
tortuga.setup(ancho=200, alto=200, startx=0, starty=0)
establece el color del bolígrafo en rojo
tortuga.pencolor(‘rojo’) #…
Agregue esto para que la ventana se cierre cuando se haga clic en
tortuga.exitonclick() 3.2 La biblioteca de gráficos de tortugas 15
Ya vimos que puedes controlar una de estas propiedades usando pencolor() método, otros métodos se utilizan para controlar el cursor (o tortuga) y se presentan abajo. La dirección en la que apunta el cursor se puede modificar utilizando varios funciones que incluyen: • right(angle) Gira el cursor a la derecha en unidades de ángulo. • izquierda(ángulo) Gira el cursor hacia la izquierda en unidades de ángulo. • setheading(to_angle) Establece la orientación del cursor en to_angle. Donde 0 es este, 90 es norte, 180 es oeste y 270 es sur. Puede mover el cursor (y si el lápiz está hacia abajo, dibujará una línea) usando: • adelante (distancia) mueve el cursor hacia adelante la distancia especificada en la dirección en la que apunta actualmente el cursor. Si el bolígrafo está abajo, dibuje un línea. • hacia atrás (distancia) mueve el cursor hacia atrás la distancia en el dirección opuesta a la que apunta el cursor. Y también puedes posicionar explícitamente el cursor: • goto(x, y) mueve el cursor a la ubicación x, y en la pantalla especificada; Si el el bolígrafo está abajo dibujar una línea. También puede usar los pasos y establecer la posición para hacer lo mismo cosa. • setx(x) establece la coordenada x del cursor, deja la coordenada y sin cambios. • sety(y) establece la coordenada y del cursor, deja la coordenada x sin cambios. También es posible mover el cursor sin dibujar modificando si el el bolígrafo está arriba o abajo: • penup() mueve el lápiz hacia arriba—mover el cursor ya no dibujará una línea. • pendown() mueve el lápiz hacia abajo: mover el cursor ahora dibujará una línea en el color actual de la pluma. El tamaño de la pluma también se puede controlar: • pensize(width) establece el grosor de la línea en ancho. El método width() es un alias para este método. También es posible dibujar un círculo o un punto: • circle(radio, extensión, pasos) dibuja un círculo usando el radio dado. La extensión determina qué parte del círculo se dibuja; si no se da el alcance entonces se dibuja todo el círculo. Pasos indica el número de pasos que se utilizarán para dibujó el círculo (se puede usar para dibujar polígonos regulares). • punto(tamaño, color) dibuja un círculo relleno con el diámetro de tamaño usando el color especificado. dieciséis 3 Gráficos de tortuga pitón
Ahora puede usar algunos de los métodos anteriores para dibujar una forma en la pantalla. Para este primer ejemplo, lo mantendremos muy simple, dibujaremos un cuadrado simple: Lo anterior mueve el cursor hacia adelante 50 píxeles y luego gira 90° antes de repetir estos pasos tres veces. El resultado final es que se dibuja un cuadrado de 50 × 50 píxeles en la pantalla: Tenga en cuenta que el cursor se muestra durante el dibujo (esto se puede desactivar con turtle.hideturtle() ya que originalmente se hacía referencia al cursor como la tortuga). 3.2.3 Formas de dibujo Por supuesto, no necesita usar valores fijos para las formas que dibuja, puede usar variables o calcular posiciones basadas en expresiones, etc. Por ejemplo, el siguiente programa crea una secuencia de cuadrados rotados alrededor de una ubicación central para crear una imagen atractiva:
Dibujar un cuadrado
tortuga.adelante(50) tortuga.derecha(90) tortuga.adelante(50) tortuga.derecha(90) tortuga.adelante(50) tortuga.derecha(90) tortuga.adelante(50) tortuga.derecha(90) 3.2 La biblioteca de gráficos de tortugas 17
En este programa se han definido dos funciones, una para configurar la pantalla o ventana con un título y un tamaño y para desactivar la visualización del cursor. El segundo La función toma un parámetro de tamaño y lo usa para dibujar un cuadrado. La parte principal de la Luego, el programa configura la ventana y usa un ciclo for para dibujar 12 cuadrados de 50 píxeles cada uno rotando continuamente 120° entre cada cuadrado. Tenga en cuenta que como lo hacemos no es necesario hacer referencia a la variable de bucle que estamos usando el formato ‘_’ que es considerada una variable de bucle anónimo en Python. La imagen generada por este programa se muestra a continuación: importar tortuga configuración definida (): """ Proporcione la configuración para la pantalla """ turtle.title(‘Animación de Múltiples Cuadrados’) tortuga.setup(100, 100, 0, 0) tortuga.ocultartortuga() def dibujar_cuadrado(tamaño): """ Dibuja un cuadrado en la dirección actual """ tortuga.adelante(tamaño) tortuga.derecha(90) tortuga.adelante(tamaño) tortuga.derecha(90) tortuga.adelante(tamaño) tortuga.derecha(90) tortuga.adelante(tamaño) configuración() para _ en el rango (0, 12): dibujar_cuadrado(50)
Girar la dirección de inicio
tortuga.derecha(120)
Agregue esto para que la ventana se cierre cuando se haga clic en
tortuga.exitonclick() 18 3 Gráficos de tortuga pitón
3.2.4 formas de relleno También es posible rellenar el área dentro de una forma dibujada. Por ejemplo, podrías desea llenar uno de los cuadrados que hemos dibujado como se muestra a continuación: Para hacer esto podemos usar las funciones begin_fill() y end_fill(): • begin_fill() indica que las formas deben rellenarse con la columna de relleno actual. Por ejemplo, esta función debe llamarse justo antes de dibujar la forma que se va a rellenar. • Se llama a end_fill() después de que se haya terminado la forma que se va a rellenar. Esto causará la forma dibujada desde la última llamada a begin_fill() para ser rellenada usando el color de relleno actual. • filling() Devuelve el estado de llenado actual (Verdadero si se está llenando, Falso si no). El siguiente programa usa esto (y la función anterior draw_square()) para dibuja el cuadrado relleno de arriba: 3.3 Otras bibliotecas de gráficos Por supuesto, Turtle Graphics no es la única opción de gráficos disponible para Python; sin embargo, otras bibliotecas gráficas no vienen preempaquetadas con Python y deben ser descargado usando una herramienta como Anaconda, PIP o PyCharm. turtle.title(‘Ejemplo de Cuadrado Relleno’) tortuga.setup(100, 100, 0, 0) tortuga.ocultartortuga() tortuga.pencolor(‘rojo’) tortuga.fillcolor(‘amarillo’) tortuga.begin_fill() dibujar_cuadrado(60) tortuga.end_fill() tortuga.hecho() 3.2 La biblioteca de gráficos de tortugas 19
• PyQtGraph. La biblioteca PyQtGraph es una biblioteca Python pura orientada a aplicaciones gráficas matemáticas, científicas y de ingeniería, así como GUI aplicaciones Para obtener más información, consulte http://www.pyqtgraph.org. • Almohada. Pillow es una biblioteca de imágenes de Python (basada en PIL, la biblioteca de imágenes de Python). biblioteca) que proporciona capacidades de procesamiento de imágenes para su uso en Python. Para más información sobre Pillow, consulte https://pillow.readthedocs.io/en/stable. • Cochinillo. pyglet es otra biblioteca multimedia y de ventanas para Python. Ver https://bitbucket.org/pyglet/pyglet/wiki/Home. 3.4 Gráficos 3D Aunque ciertamente es posible que un desarrollador cree imágenes 3D convincentes utilizando Turtle Graphics; no es el objetivo principal de la biblioteca. Esto significa que hay no hay soporte directo para crear imágenes en 3D que no sean el movimiento básico del cursor facilidades y la habilidad de los programadores. Sin embargo, hay bibliotecas de gráficos 3D disponibles para Python. Una de esas bibliotecas es Panda3D (https://www.panda3d.org) mientras que otro es VPython (https://vpython.org) mientras que un tercero es pi3d (https://pypi.org/project/pi3d). Sin embargo, veremos brevemente la biblioteca PyOpenGL ya que se basa en la biblioteca OpenGL muy utilizada. 3.4.1 PyOpenGL PyOpenGL es un proyecto de código abierto que proporciona un conjunto de enlaces (o envolturas) alrededor) la biblioteca OpenGL. OpenGL es la biblioteca abierta de gráficos que es un API multilenguaje y multiplataforma para renderizar gráficos vectoriales 2D y 3D. OpenGL se utiliza en una amplia gama de aplicaciones, desde juegos hasta realidad virtual, a través de sistemas de visualización de datos e información hasta el Diseño Asistido por Computador (CAD) sistemas. PyOpenGL proporciona un conjunto de funciones de Python que llaman desde Python a las bibliotecas OpenGL subyacentes. Esto hace que sea muy fácil crear 3D. imágenes basadas en vectores en Python utilizando la biblioteca OpenGL estándar de la industria. un muy A continuación se dan ejemplos simples de una imagen creada usando PyOpenGL: 20 3 Gráficos de tortuga pitón
3.5 Recursos en línea Lo siguiente proporciona material de lectura adicional: • https://docs.python.org/3/library/turtle.html Documentación de gráficos de tortugas. • http://pythonturtle.org/ El entorno de programación Python Turtle: este diseñado para enseñar los conceptos básicos detrás de la programación usando Turtle biblioteca de gráficos. • http://pyopengl.sourceforge.net La página de inicio de PyOpenGL. • https://www.opengl.org La página de inicio de OpenGL. 3.6 Ejercicios El objetivo de este ejercicio es crear una pantalla gráfica usando Python Turtle Graphics. Debes crear un programa simple para dibujar un octágono en Turtle Graphics pantalla. Modifique su programa para que haya una función de dibujo hexagonal. Este La función debe tomar tres parámetros, las coordenadas x e y para comenzar a dibujar el octágono y el tamaño de cada lado del octágono. Modifique su programa para dibujar el hexágono en múltiples ubicaciones para crear el siguiente imagen: 3.5 Recursos en línea 21
Capítulo 4 Arte generado por computadora 4.1 Crear arte por computadora Computer Art se define como cualquier arte que utiliza una computadora. Sin embargo, en el contexto de En este libro queremos decir que es arte generado por una computadora o, más específicamente, por un programa de computadora. El siguiente ejemplo, ilustra cómo en muy pocas líneas de El código de Python, utilizando la biblioteca de gráficos Turtle, puede crear imágenes que podrían ser considerado arte informático. La siguiente imagen es generada por una función recursiva que dibuja un círculo en un Dada la ubicación x, y de un tamaño específico. Esta función recursivamente se llama a sí misma por modificando los parámetros para que se dibujen círculos cada vez más pequeños en diferentes ubicaciones hasta que el tamaño de los círculos sea inferior a 20 píxeles. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_4 23
El programa utilizado para generar esta imagen se proporciona a continuación como referencia: importar tortuga ANCHO = 640 ALTURA = 360 def configuración_ventana():
Configurar la ventana
tortuga.title(‘Círculos en mi mente’) tortuga.setup(ANCHO, ALTO, 0, 0) tortuga.colormode(255)
Indica que los números RGB estarán en
el rango de 0 a 255 tortuga.ocultartortuga()
Dibujo por lotes en la pantalla para una representación más rápida
tortuga.tracer(2000)
Acelerar el proceso de dibujo
tortuga.velocidad(10) tortuga.penup() def dibujar_circulo(x, y, radio, rojo=50, verde=255, azul=10, ancho=7): """ Dibuja un círculo en una ubicación x, y específica. Luego dibuja cuatro círculos más pequeños recursivamente""" color = (rojo, verde, azul)
Círculos más pequeños dibujados recursivamente
si radio > 50:
Calcular colores y ancho de línea para círculos más pequeños
si rojo < 216: rojo = rojo + 33 verde = verde - 42 azul = azul + 10 ancho -= 1 24 4 Arte generado por computadora
Ejecuta el programa
imprimir(‘Iniciando’) configuración_ventana() dibujar_circulo(25, -100, 200)
Asegurarse de que todo el dibujo esté renderizado
tortuga.update() imprimir(‘Terminado’) tortuga.hecho() Hay algunos puntos a tener en cuenta sobre este programa. Utiliza la recursividad para dibujar el se dibujan círculos con círculos cada vez más pequeños hasta el radio de los círculos cae por debajo de cierto umbral (el punto de terminación). También usa la función turtle.tracer() para acelerar el dibujo de la imagen. ya que 2000 cambios se almacenarán en el búfer antes de que se actualice la pantalla. Finalmente, los colores usados para los círculos se cambian en cada nivel de recesión; a se utiliza un enfoque muy simple para que los códigos rojo, verde y azul se cambien resultando en diferentes círculos de color. También se utiliza un ancho de línea para reducir el tamaño de el contorno del círculo para añadir más interés a la imagen. 4.2 Un generador de arte por computadora Como otro ejemplo de cómo puede usar los gráficos Turtle para crear arte computarizado, el siguiente programa genera aleatoriamente colores RGB para usar en las líneas que se están dibujado que le da más interés a las imágenes. También permite al usuario introducir un demás: rojo = 0 verde = 255
Calcula el radio de los círculos más pequeños
nuevo_radio = int(radio / 1.3)
Dibuja cuatro círculos
dibujar_circulo(int(x + radio_nuevo), y, radio_nuevo, rojo, verde, azul, ancho) dibujar_circulo(x - nuevo_radio, y, nuevo_radio, rojo, verde, azul, ancho) dibujar_circulo(x, int(y + radio_nuevo), radio_nuevo, rojo, verde, azul, ancho) dibujar_circulo(x, int(y - nuevo_radio), nuevo_radio, rojo, verde, azul, ancho)
Dibuja el círculo original
tortuga.goto(x, y) tortuga.color(color) tortuga.ancho(ancho) tortuga.pendown() tortuga.circle(radio) tortuga.penup() 4.1 Crear arte por computadora 25
ángulo que se utilizará al cambiar la dirección en la que se dibuja la línea. como el dibujo sucede dentro de un bucle, incluso este simple cambio en el ángulo utilizado para dibujar las líneas puede generar imágenes muy diferentes.
Juguemos con algunos colores
importar tortuga de randint de importación aleatoria def get_input_angle(): """ Obtener entrada del usuario y convertir a un int""" mensaje = ‘Proporcione un ángulo:’ value_as_string = entrada (mensaje) mientras que no value_as_string.isnumeric(): print(‘La entrada debe ser un entero!’) value_as_string = entrada (mensaje) devuelve int(valor_como_cadena) def generar_color_aleatorio(): “““Genera valores R,G,B aleatoriamente en el rango 0 a 255 "”” r = aleatorio(0, 255) g = randint(0, 255) b = aleatorio(0, 255) devuelve r, g, b imprimir(‘Configurar pantalla’) tortuga.title(‘Patrón de colores’) tortuga.setup(640, 600) tortuga.ocultartortuga() tortuga.bgcolor(’negro’)
Establecer el color de fondo de la
pantalla turtle.colormode(255) # Indica que los números RGB estarán en el rango de 0 a 255 tortuga.velocidad(10) ángulo = get_input_angle() imprimir(‘Iniciar el dibujo’) para i en el rango (0, 200): tortuga.color(generar_color_aleatorio()) tortuga.adelante(i) tortuga.derecha(ángulo) imprimir(‘Terminado’) tortuga.hecho() 26 4 Arte generado por computadora
Algunas imágenes de muestra generadas a partir de este programa se dan a continuación. La izquierda la mayoría de las imágenes se generan ingresando un ángulo de 38 grados, la imagen en la la derecha usa un ángulo de 68 grados y la imagen de abajo un ángulo de 98 grados. Las siguientes imágenes a continuación usan ángulos de 118, 138 y 168 grados respectivamente. Lo interesante de estas imágenes es lo diferente que es cada una; a pesar de que usar exactamente el mismo programa. Esto ilustra cómo la generación algorítmica o informática El arte grabado puede ser tan sutil y flexible como cualquier otra forma de arte. También ilustra que incluso con tal proceso, todavía depende del ser humano determinar qué imagen (si cualquiera) es el más agradable estéticamente. 4.2 Un generador de arte por computadora 27
4.3 Fractales en Python Dentro del ámbito del Computer Art, los fractales son una forma de arte muy conocida. Factrales son patrones recurrentes que se calculan utilizando un enfoque iterativo (como un bucle for) o un enfoque recursivo (cuando una función se llama a sí misma pero con modificaciones parámetros). Una de las características realmente interesantes de los fractales es que exhiben la mismo patrón (o casi el mismo patrón) en sucesivos niveles de granularidad. Eso es, si magnificara una imagen fractal encontraría que el mismo patrón está siendo repetidas con aumentos cada vez más pequeños. Esto se conoce como ex- simetría panding o simetría desplegable; si esta replicación es exactamente la misma en cada escala, entonces se llama autosimilar afín. Los fractales tienen sus raíces en el mundo de las matemáticas a partir del siglo XVII, con el término fractal acuñado en el siglo XX por el matemático Benoit Mandelbrot en 1975. Una descripción citada a menudo que Mandelbrot publicó para describir fractales geométricos es una forma geométrica tosca o fragmentada que se puede dividir en partes, cada una de las cuales es (al menos aproximadamente) una copia en tamaño reducido del todo. Para más información ver Mandelbrot, Benoît B. (1983). La geometría fractal de naturaleza. Macmillan. ISBN (978-0-7167-1186-5). Desde finales del siglo XX, los fractales han sido una forma comúnmente utilizada de crear arte por ordenador. Un ejemplo de un fractal de uso frecuente en el arte de la computadora es el copo de nieve de Koch, mientras que otro es el conjunto de Mandelbrot. Ambos se utilizan en este capítulo como ejemplos para ilustrar cómo se pueden usar Python y la biblioteca de gráficos Turtle para crear fractales arte basado. 4.3.1 El copo de nieve de Koch El copo de nieve de Koch es un fractal que comienza con un triángulo equilátero y luego reemplaza el tercio medio de cada segmento de línea con un par de segmentos de línea que formar una protuberancia equilátera. Este reemplazo se puede realizar a cualquier profundidad gene- tringulos cada vez ms finos (cada vez ms pequeos) hasta que la forma general se asemeja a un copo de nieve. 28 4 Arte generado por computadora
El siguiente programa se puede utilizar para generar un copo de nieve de Koch con diferentes niveles de recursividad. Cuanto mayor sea el número de niveles de recursividad, más veces cada uno se diseca el segmento de línea. importar tortuga
Configurar constantes
ÁNGULOS = [60, -120, 60, 0] TAMAÑO_DE_COPO DE NIEVE = 300 def obtener_input_profundidad(): "”" Obtener entrada del usuario y convertir a un int""" mensaje = ‘Proporcione la profundidad (0 o un positivo entero):’ value_as_string = entrada (mensaje) mientras que no value_as_string.isnumeric(): print(‘La entrada debe ser un entero!’) value_as_string = entrada (mensaje) devuelve int(valor_como_cadena) def setup_screen(título, fondo=‘blanco’, screen_size_x=640, tamaño_pantalla_y=320, tamaño_trazador=800): imprimir(‘Configurar pantalla’) tortuga.título(título) tortuga.setup(tamaño_de_pantalla_x, tamaño_de_pantalla_y) tortuga.ocultartortuga() tortuga.penup() tortuga.atrás(240)
Dibujo por lotes en la pantalla para una representación más rápida
tortuga.trazador(tamaño_trazador) turtle.bgcolor(background) # Establece el color de fondo de la pantalla 4.3 Fractales en Python 29
A continuación se muestran varias ejecuciones diferentes del programa con la profundidad establecida en 0, 1, 3 y 7. def draw_koch(tamaño, profundidad): si profundidad > 0: para ángulo en ÁNGULOS: draw_koch(tamaño / 3, profundidad - 1) tortuga.izquierda(ángulo) demás: tortuga.adelante(tamaño) profundidad = obtener_input_profundidad() setup_screen(‘Copo de nieve Koch (profundidad ’ + str(profundidad) + ‘)’, fondo=‘negro’, tamaño_de_pantalla_x=420, tamaño_de_pantalla_y=420)
Establecer colores de primer plano
tortuga.color(‘azul cielo’)
Asegúrese de que el copo de nieve esté centrado
tortuga.penup() tortuga.setposition(-180,0) tortuga.izquierda(30) tortuga.pendown()
Dibuja tres lados del copo de nieve
para _ en el rango (3): dibujar_koch(TAMAÑO_DE_COPO DE NIEVE, profundidad) tortuga.derecha(120)
Asegurarse de que todo el dibujo esté renderizado
tortuga.update() imprimir(‘Terminado’) tortuga.hecho() 30 4 Arte generado por computadora
Ejecutar la función simple draw_koch() con diferentes profundidades lo hace fácil para ver la forma en que cada lado de un triángulo se puede dividir en otro forma triangular. Esto se puede repetir a múltiples profundidades dando una visión más detallada estructurado en el que la misma forma se repite una y otra vez. 4.3.2 Conjunto Mandelbrot Probablemente una de las imágenes fractales más famosas esté basada en el conjunto de Mandelbrot. El conjunto de Mandelbrot es el conjunto de números complejos c para los cuales la función z * z + c no diverge cuando se itera desde z = 0 para el cual la secuencia de funciones (func(0), func(func(0)) etc.) permanece limitada por un valor absoluto. El definición del Conjunto de Mandelbrot y su nombre es abajo a el Francés matemático Adrien Douady, quien lo nombró en homenaje al matemático Benoit Mandelbrot. Se pueden crear imágenes de conjuntos de Mandelbrot muestreando los números complejos y probando, para cada punto de muestra c, si la secuencia func(0), func(func(0)) etc. va hasta el infinito (en la práctica esto significa que se hace una prueba para ver si deja algo vecindad acotada predeterminada de 0 después de un número predeterminado de iteraciones). Tratar las partes real e imaginaria de c como coordenadas de imagen en el plano complejo, los píxeles se pueden colorear de acuerdo con qué tan pronto secuencia cruza un umbral elegido arbitrariamente, con un color especial (generalmente negro) utilizado para los valores de c para los que la secuencia no ha cruzado el umbral 4.3 Fractales en Python 31
después del número predeterminado de iteraciones (esto es necesario para distinguir claramente la imagen del conjunto de Mandelbrot a partir de la imagen de su complemento). La siguiente imagen fue generada para el conjunto de Mandelbrot usando Python y Gráficos de tortugas. El programa utilizado para generar esta imagen se muestra a continuación: para y en rango (IMAGE_SIZE_Y): zy = y * (MAX_Y - MIN_Y) / (IMAGE_SIZE_Y - 1) + MIN_Y para x en el rango (IMAGE_SIZE_X): zx = x * (MAX_X - MIN_X) / (IMAGE_SIZE_Y - 1) + MIN_X z = zx + zy * 1j c = z para i en rango (MAX_ITERACIONES): si abs(z) > 2.0: romper z = z * z + c tortuga.color((i % 4 * 64, i % 8 * 32, i % 16 * 16)) tortuga.setposition(x - SCREEN_OFFSET_X, y - PANTALLA_DESPLAZAMIENTO_Y) tortuga.pendown() tortuga.punto(1) tortuga.penup() 32 4 Arte generado por computadora
4.4 Recursos en línea Lo siguiente proporciona material de lectura adicional: • https://en.wikipedia.org/wiki/Fractal Para la página de Wikipedia sobre Fractales. • https://en.wikipedia.org/wiki/Koch_snow ake La página de Wikipedia sobre Koch copo de nieve • https://en.wikipedia.org/wiki/Mandelbrot_set Página de Wikipedia sobre el Mandel- conjunto hermano. 4.5 Ejercicios El objetivo de este ejercicio es crear un árbol fractal. Un árbol fractal es un árbol en el que la estructura general se replica con mayor precisión y precisión. niveles más finos a través del árbol hasta alcanzar un conjunto de elementos de hoja. Para dibujar el árbol fractal necesitarás: • Dibujar el tronco. • Al final del baúl, divida el baúl en dos con el baúl izquierdo y el baúl derecho el tronco está a 30° a la izquierda/derecha del tronco original. Por motivos estéticos el maletero puede volverse más delgado cada vez que se divide. El tronco puede ser dibujado en un particular color como el marrón. • Continúe esto hasta que haya ocurrido un número máximo de divisiones (o el tamaño del tronco reduce a un mínimo particular). Ahora has llegado a las hojas (puedes dibuja las hojas en un color diferente, p. verde). A continuación se muestra un ejemplo de un árbol fractal: 4.4 Recursos en línea 33
Capítulo 5 Introducción a Matplotlib 5.1 Introducción Matplotlib es una biblioteca de gráficos y trazados de Python que puede generar una variedad de diferentes tipos de gráficos o tablas en una variedad de formatos diferentes. puede ser usado para genere gráficos de líneas, gráficos de dispersión, mapas de calor, gráficos de barras, gráficos circulares y gráficos en 3D. Él incluso puede admitir animaciones y pantallas interactivas. A continuación se muestra un ejemplo de un gráfico generado con Matplotlib. Esto muestra un gráfico de líneas utilizado para trazar una onda de señal simple: Matplotlib es una biblioteca gráfica muy flexible y poderosa. Puede soportar un variedad de diferentes plataformas gráficas de Python y ventanas del sistema operativo entornos. También puede generar gráficos de salida en una variedad de formatos diferentes. incluyendo PNG, JPEG, SVG y PDF, etc. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_5 35
Matplotlib se puede usar solo o junto con otras bibliotecas para proporcionar una amplia variedad de instalaciones. Una biblioteca que a menudo se usa en conjunto con Matplotlib es NumPy, que es una biblioteca que se usa a menudo en aplicaciones de ciencia de datos que proporciona una variedad de funciones y estructuras de datos (como n-dimensional matrices) que pueden ser muy útiles al procesar datos para mostrarlos en un gráfico. Sin embargo, Matplotlib no viene integrado en el entorno de Python; es un módulo opcional que debe agregarse a su entorno o IDE. En este capítulo presentaremos la biblioteca Matplotlib, su arquitectura, la componentes que comprenden un gráfico y la API de pyplot. La API de pyplot es la forma más simple y común en la que un programador interactúa con Matplotlib. Luego exploraremos una variedad de diferentes tipos de gráficos y cómo pueden ser creado usando Matplotlib, desde gráficos de líneas simples, pasando por gráficos de dispersión, hasta gráficos de barras gráficos y gráficos circulares. Terminaremos observando un gráfico 3D simple. 5.2 matplotlib Matplotlib es una biblioteca de trazado de gráficos para Python. Para gráficos simples Matplotlib es muy fácil de usar, por ejemplo, para crear un gráfico lineal simple para un conjunto de x e y coordenadas puede usar la función matplotlib.pyplot.plot: Este programa muy simple genera el siguiente gráfico: pyplot.plot([1, 0.25, 0.5, 2, 3, 3.75, 3.5])
Mostrar el gráfico en una ventana
pyplot.mostrar() importar matplotlib.pyplot como pyplot
Graficar una secuencia de valores
36 5 Introducción a Matplotlib
En este ejemplo, la función plot() toma una secuencia de valores que serán tratados como los valores del eje y; los valores del eje x están implícitos en la posición de la y valores dentro de la lista. Por lo tanto, como la lista tiene seis elementos, el eje x tiene el rango 0–6. A su vez como el valor máximo contenido en la lista es 3.75, entonces el eje y va de 0 a 4. 5.3 Componentes de la trama Aunque puedan parecer simples, son numerosos los elementos que componen un Gráfico o diagrama de Matplotlib. Todos estos elementos pueden ser manipulados y modificados. independientemente. Por lo tanto, es útil estar familiarizado con la terminología de Matplotlib. asociados a estos elementos, como ticks, leyendas, etiquetas, etc. Los elementos que componen una trama se ilustran a continuación: El diagrama ilustra los siguientes elementos: • Ejes Un eje está definido por la clase matplotlib.axes.Axes. Es usado para mantener la mayoría de los elementos de una figura, a saber, los ejes X e Y, las garrapatas, los gráficos de líneas, cualquier texto y cualquier forma de polígono. • Título Este es el título de toda la figura. • Ticks (Mayor y Menor) Los Ticks están representados por la clase mat- plotlib.axis.Tick. Un Tick es la marca en el Eje que indica un nuevo 5.2 matplotlib 37
valor. Puede haber marcas principales que son más grandes y pueden estar etiquetadas. Hay también garrapatas menores que pueden ser más pequeñas (y también pueden estar etiquetadas). • Etiquetas de Tick (Mayor y Menor) Esta es una etiqueta en un Tick. • Axis La clase maplotlib.axis.Axis define un objeto Axis (como un objeto X o eje Y) dentro de una instancia principal de Axes. Puede tener formateadores utilizados para formatear las etiquetas utilizadas para las marcas principales y secundarias. También es posible configurar el ubicaciones de las marcas principales y secundarias. • Etiquetas de eje (X, Y y, en algunos casos, Z) Estas son etiquetas utilizadas para describir el Eje. • Tipos de gráficos, como gráficos de líneas y de dispersión. Varios tipos de parcelas y gráficos son compatible con Matplotlib, incluidos diagramas de líneas, gráficos de dispersión, gráficos de barras y circulares cartas • Cuadrícula Esta es una cuadrícula opcional que se muestra detrás de un diagrama, gráfico o tabla. La cuadrícula se puede mostrar con una variedad de diferentes estilos de línea (como sólida o discontinua) líneas), colores y anchos de línea. 5.4 Arquitectura matplotlib La biblioteca Matplotlib tiene una arquitectura en capas que oculta gran parte de la complejidad asociados con diferentes sistemas de ventanas y salidas gráficas. esta arquitectura tiene tres capas principales, la capa de secuencias de comandos, la capa de artista y la capa de fondo. Cada capa tiene responsabilidades y componentes específicos. Por ejemplo, el back-end es responsable de leer e interactuar con el gráfico o trama que se está generando. En a su vez, la capa de artista es responsable de crear los objetos gráficos que serán renderizado por la capa de fondo. Finalmente, la capa de secuencias de comandos es utilizada por el desarrollador. oper para crear los gráficos. Esta arquitectura se ilustra a continuación: 38 5 Introducción a Matplotlib
5.4.1 Capa de fondo La capa de back-end de Matplotlib maneja la generación de salida a diferentes objetivos formatos. Matplotlib en sí mismo se puede usar de muchas maneras diferentes para generar muchos diferentes salidas. Matplotlib se puede usar de forma interactiva, se puede incrustar en una aplicación (o interfaz gráfica de usuario), se puede utilizar como parte de una aplicación por lotes con parcelas siendo almacenado como PNG, SVG, PDF u otras imágenes, etc. Para admitir todos estos casos de uso, Matplotlib puede apuntar a diferentes salidas y cada una de estas capacidades se denomina backend; el “frontend” es el desarrollador que enfrenta código. La capa de backend mantiene todos los diferentes backends y el programador puede usar el backend predeterminado o seleccionar un backend diferente según sea necesario. El backend que se utilizará se puede configurar a través de la función matplotlib.use(). Para ejemplo, para configurar el backend para renderizar el uso de Postscript: matplotlib.use(‘PS’) esto es ilustrado a continuación: Cabe señalar que si utiliza la función matplotlib.use(), esta debe hacerse antes de importar matplotlib.pyplot. Llamando a matplotlib.use () después de que se haya importado matplotlib.pyplot no tendrá ningún efecto. Tenga en cuenta que el argumento pasado a la función matplotlib.use() distingue entre mayúsculas y minúsculas. El renderizador por defecto es el ‘Agg’ que usa la Geometría Anti-Grano C++ librería para crear una imagen rasterizada (píxel) de la figura. Esto produce raster de alta calidad. imágenes basadas en gráficos de las parcelas de datos. El backend ‘Agg’ fue elegido como el backend predeterminado ya que funciona en una amplia selección de sistemas Linux ya que sus requisitos de soporte son bastante pequeños; otro Los backends pueden ejecutarse en un sistema en particular, pero es posible que no funcionen en otro sistema. Esto ocurre si un sistema en particular no tiene todas las dependencias cargadas que el en el que se basa el backend de Matplotlib especificado. importar matplotlib si ‘matplotlib.backends’ no está en sys.modules: matplotlib.use(‘PS’) importar matplotlib.pyplot como pyplot 5.4 Arquitectura matplotlib 39
La capa de fondo se puede dividir en dos categorías: • Backends de interfaz de usuario (interactivos) que admiten varias ventanas de Python sistemas como wxWidgets (discutido en el próximo capítulo), Qt, TK, etc. • Backends impresos (no interactivos) que admiten gráficos vectoriales y de trama salidas. Los backends de la interfaz de usuario y la copia impresa se basan en abstracciones comunes denominadas clases base de backend. 5.4.2 La capa del artista La capa Artista proporciona la mayoría de las funciones que podría considerar ser lo que Matplotlib realmente hace; esa es la generación de las parcelas y gráficos que se representan/muestran al usuario (o se emiten en un formato particular). La capa del artista se ocupa de cosas como las líneas, las formas, el eje y los ejes, texto, etc. que componen una trama. Las clases utilizadas por la capa de artista se pueden clasificar en una de las siguientes tres grupos; primitivas, contenedores y colecciones: • Las primitivas son clases que se utilizan para representar objetos gráficos que se dibujarán en a un lienzo de figuras. • Los contenedores son objetos que contienen primitivas. Por ejemplo, típicamente una figura se crearía una instancia y se usaría para crear uno o más ejes, etc. • Las colecciones se utilizan para manejar de manera eficiente grandes cantidades de tipos similares de objetos. Aunque es útil estar al tanto de estas clases; en muchos casos no lo harás necesita trabajar con ellos directamente ya que la API de pyplot oculta gran parte de los detalles. Sin embargo, es posible trabajar a nivel de figuras, ejes, ticks, etc. si es necesario. 40 5 Introducción a Matplotlib
5.4.3 La capa de secuencias de comandos La capa de secuencias de comandos es la interfaz orientada al desarrollador que simplifica la tarea de trabajando con las otras capas. Tenga en cuenta que, desde el punto de vista de los programadores, la capa de secuencias de comandos se representa por el módulo pyplot. Debajo de las cubiertas, pyplot usa objetos a nivel de módulo para rastrear el estado de los datos, manejar el dibujo de los gráficos, etc. Cuando pyplot importado selecciona el backend predeterminado para el sistema o el uno que ha sido configurado; por ejemplo a través de la función matplotlib.use(). Luego llama a una función setup() que: • crea una función de fábrica de administrador de figuras, que cuando se llama creará una nueva administrador de figuras apropiado para el backend seleccionado, • prepara la función de dibujo que se debe utilizar con el backend seleccionado, • identifica la función invocable que se integra con el backend mainloop función, • proporciona el módulo para el backend seleccionado. La interfaz pyplot simplifica las interacciones con los contenedores internos al proporcionando métodos como plot(), pie(), bar(), title(), savefig(), dibujar() y figura() etc. La mayoría de los ejemplos presentados en el próximo capítulo usarán las funciones pro- proporcionado por el módulo pyplot para crear los gráficos requeridos; ocultando así la parte inferior detalles de nivel. 5.4 Arquitectura matplotlib 41
5.5 Recursos en línea Consulte la documentación en línea para: • https://matplotlib.org La biblioteca Matplotlib. Esto incorpora numerosas ejemplos con listados completos, documentación, galerías y un usuario detallado guía y preguntas frecuentes. • https://pythonprogramming.net/matplotlib-python-3-basics-tutorial Python Mat- Curso intensivo de plotlib. 42 5 Introducción a Matplotlib
Capítulo 6 Graficando con Matplotlib pyplot 6.1 Introducción En este capítulo exploraremos la API pyplot de Matplotlib. Este es el más común forma en que los desarrolladores generan diferentes tipos de gráficos o diagramas utilizando Matplotlib. 6.2 La API de pyplot El propósito del módulo pyplot y la API que presenta es simplificar la generación y manipulación de diagramas y gráficos de Matplotlib. Como un todo el La biblioteca Matplotlib intenta hacer que las cosas simples sean fáciles y las cosas complejas posibles. El La forma principal en que logra el primero de estos objetivos es a través de la API pyplot. ya que esta API tiene funciones de alto nivel como bar(), plot(), scatter() y pie() que facilita la creación de gráficos de barras, diagramas de líneas, gráficos de dispersión y gráficos circulares. Un punto a tener en cuenta sobre las funciones proporcionadas por la API de pyplot es que a menudo puede tomar muchos parámetros; sin embargo, la mayoría de estos parámetros tendrán valores predeterminados que en muchas situaciones le darán un comportamiento predeterminado razonable/ representación visual predeterminada. Por lo tanto, puede ignorar la mayoría de los parámetros disponible hasta el momento en que realmente necesite hacer algo diferente; en el cual punto, debe consultar la documentación de Matplotlib ya que tiene una amplia material así como numerosos ejemplos. Por supuesto, es necesario importar el módulo pyplot; ya que es un módulo dentro la biblioteca Matplotlib (por ejemplo, matplotlib.pyplot). A menudo se le da un alias dentro de un programa para que sea más fácil de referencia. Los alias comunes para este módulo son pyplot o plt. A continuación se muestra una importación típica para el módulo pyplot: importar matplotlib.pyplot como pyplot © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_6 43
La API plyplot se puede utilizar para • construir la trama, • configurar etiquetas y ejes, • administrar colores y estilos de línea, • maneja eventos/permite que las tramas sean interactivas, • mostrar (mostrar) la trama. Veremos ejemplos del uso de la API de pyplot en las siguientes secciones. 6.3 Gráficos de líneas Un gráfico de líneas o diagrama de líneas es un gráfico con los puntos en el gráfico (a menudo denominado como marcadores) conectados por líneas para mostrar cómo algo cambia en valor como algunos cambia el conjunto de valores (típicamente el eje x); por ejemplo, sobre una serie a tiempo intervalos (también conocido como una serie de tiempo). Los gráficos de líneas de series temporales normalmente se dibujan en orden cronológico; dichos gráficos se conocen como gráficos de ejecución. El siguiente gráfico es un ejemplo de un gráfico de ejecución; traza el tiempo en la parte inferior (eje x) contra la velocidad (representada por el eje y). 44 6 Graficando con Matplotlib pyplot
El programa utilizado para generar este gráfico se muestra a continuación: importar matplotlib.pyplot como pyplot
Configurar los datos
X = [0, 1, 2, 3, 4, 5, 6] y = [0, 2, 6, 14, 30, 43, 75]
Establecer los encabezados de los ejes
pyplot.ylabel(‘Velocidad’, tamaño de fuente=12) pyplot.xlabel(‘Tiempo’, tamaño de fuente=12)
Establecer el título
pyplot.title(“Velocidad frente a tiempo”)
Traza y muestra el gráfico
Usar círculos azules como marcadores (‘bo’)
y una línea continua (’-’)
pyplot.plot(x, y, ‘bo-’) pyplot.mostrar() Lo primero que hace este programa es importar el archivo matplotlib.pyplot módulo y asígnele un alias de pyplot (como este es un nombre más corto, hace que el código más fácil de leer). A continuación, se crean dos listas de valores para las coordenadas x e y de cada marcador o punto de trama. Luego, el gráfico en sí se configura con etiquetas que se proporcionan para x e y. eje (usando las funciones pyplot xlabel() e ylabel()). El título del gráfico. luego se establece (nuevamente usando una función pyplot). Después de esto, los valores de x e y se trazan como un gráfico de líneas en el gráfico. Esto es hecho usando la función pyplot.plot(). Esta función puede tomar una amplia gama de parámetros, siendo los únicos parámetros obligatorios los datos utilizados para definir la gráfica puntos. En el ejemplo anterior se proporciona un tercer parámetro; esta es una cadena ‘bo-’. Esta es una cadena de formato codificado en la que cada elemento de la cadena es significativo para el función pyplot.plot(). Los elementos de la cadena son: • b: indica el color que se utilizará al dibujar la línea; en este caso la letra ‘b’ indica el color azul (de la misma forma ‘r’ indicaría rojo y ‘g’ indicaría verde). • o: esto indica que cada marcador (cada punto que se traza) debe ser representado enviado por un cirlce. Las líneas entre los marcadores luego crean el gráfico de líneas. • ‘–’: esto indica el estilo de línea a utilizar. Un solo guión (’-’) indica una línea sólida, donde como un doble guión (’-’) indica una línea discontinua. 6.3 Gráficos de líneas 45
Finalmente, el programa usa la función show() para representar la figura en el pantalla; alternativamente, se podría haber usado savefig() para guardar la figura en un archivo. 6.3.1 Cadenas de formato codificadas Hay numerosas opciones que se pueden proporcionar a través de la cadena de formato, las siguientes Las siguientes tablas resumen algunos de estos: Las siguientes abreviaturas de color son compatibles con la cadena de formato: Personaje Color ‘b’ azul ‘gramo’ verde ‘r’ rojo ‘C’ cian ‘metro’ magenta ‘y’ amarillo ‘k’ negro ‘w’ blanco Diferentes formas de representar los marcadores (puntos en el gráfico) conectados por las líneas también son compatibles, incluyendo: Personaje Descripción ‘.’ marcador de puntos ‘,’ marcador de píxeles ‘o’ marcador circular ‘v’ marcador de triángulo_abajo ‘^’ marcador de triángulo_arriba ’ < ' marcador triángulo_izquierda ’ > ' marcador triángulo_derecho ’s’ marcador cuadrado ‘pag’ marcador del pentágono ‘*’ marcador de estrella ‘h’ marcador hexagonal1 ’ + ' más marcador ‘X’ x marcador ‘D’ marcador de diamantes 46 6 Graficando con Matplotlib pyplot
Finalmente, la cadena de formato admite diferentes estilos de línea: Personaje Descripción ‘-’ estilo de línea continua ‘-’ estilo de línea discontinua ‘-.’ estilo de línea de puntos y guiones ‘:’ estilo de línea punteada Algunos ejemplos de cadenas de formato: • Línea roja ‘r’ con marcadores predeterminados y estilo de línea. • ‘g-’ línea continua verde. • Línea discontinua ‘–’ con el color predeterminado y los marcadores predeterminados. • ‘yo:’ línea punteada amarilla con marcadores circulares. 6.4 Gráfico de dispersión Un gráfico de dispersión o diagrama de dispersión es un tipo de diagrama en el que se indican los valores individuales usando coordenadas cartesianas (o x e y) para mostrar valores. Cada valor se indica mediante una marca (como un círculo o un triángulo) en el gráfico. Se pueden usar para representar valores obtenidos para dos variables diferentes; uno trazado en el eje x y el otro trazado en el eje y. A continuación se muestra un ejemplo de un gráfico de dispersión con tres conjuntos de valores de dispersión. 6.3 Gráficos de líneas 47
En este gráfico cada punto representa la cantidad de tiempo que pasan personas de diferentes edades gastar en tres actividades diferentes. El programa que se utilizó para generar el gráfico anterior se muestra a continuación: En el ejemplo anterior, la función plot.scatter() se usa para generar el gráfico de dispersión para los datos definidos por las tuplas de equitación, natación y vela. Los colores de los marcadores se han especificado usando el parámetro nombrado c. Este parámetro puede tomar una cadena que representa el nombre de un color o dos matriz dimensional con una sola fila en la que cada valor en la fila representa un Código de colores RGB. El marcador Indica el estilo del marcador, como ‘o’ para un círculo, una ‘^’ para un triángulo y ‘*’ para una forma de estrella. La etiqueta se utiliza en la leyenda del gráfico para el marcador. Otras opciones disponibles en la función pyplot.scatter() incluyen: • alfa: indica el valor de mezcla alfa, entre 0 (transparente) y 1 (opaco). importar matplotlib.pyplot como pyplot
Crear datos
montando = ((17, 18, 21, 22, 19, 21, 25, 22, 25, 24), (3, 6, 3.5, 4, 5, 6.3, 4.5, 5, 4.5, 4)) natación = ((17, 18, 20, 19, 22, 21, 23, 19, 21, 24), (8, 9, 7, 10, 7.5, 9, 8, 7, 8.5, 9)) vela = ((31, 28, 29, 36, 27, 32, 34, 35, 33, 39), (4, 6.3, 6, 3, 5, 7.5, 2, 5, 7, 4))
Graficar los datos
pyplot.scatter(x=montando[0], y=montando[1], c=‘rojo’, marcador=‘o’, etiqueta = ‘montando’) pyplot.scatter(x=natación[0], y=natación[1], c=‘verde’, marcador=’^’, etiqueta=‘natación’) pyplot.scatter(x=navegación[0], y=navegación[1], c=‘azul’, marcador=’*’, etiqueta=‘navegando’)
Configurar gráfico
pyplot.xlabel(‘Edad’) pyplot.ylabel(‘Horas’) pyplot.title(‘Gráfico de dispersión de actividades’) pyplot.leyenda() pyplot.mostrar()
Mostrar el gráfico
48 6 Graficando con Matplotlib pyplot
• anchos de línea: que se utiliza para indicar el ancho de línea de los bordes del marcador. • edgecolors : indica el color a usar para los bordes del marcador si es diferente de el color de relleno utilizado para el marcador (indicado por el parámetro ‘c’). 6.4.1 Cuándo usar gráficos de dispersión Una pregunta útil a considerar es ¿cuándo se debe usar un diagrama de dispersión? En general Los planos de dispersión se utilizan cuando es necesario mostrar la relación entre dos variables Los diagramas de dispersión a veces se denominan diagramas de correlación porque muestran cómo se correlacionan dos variables. En muchos casos se puede discernir una tendencia entre los puntos trazados en una dispersión gráfico (aunque puede haber valores atípicos). Para ayudar a visualizar la tendencia puede ser útil para dibujar una línea de tendencia junto con el gráfico de dispersión. La línea de tendencia ayuda a hacer la relación de los diagramas de dispersión con la tendencia general es más clara. El siguiente gráfico representa un conjunto de valores como un gráfico de dispersión y dibuja el línea de tendencia de este gráfico de dispersión. Como puede verse, algunos valores están más cerca del línea de tendencia que otros. La línea de tendencia se ha creado en este caso utilizando la función numérica polyfit(). 6.4 Gráfico de dispersión 49
La función polyfit() realiza un ajuste polinomial de mínimos cuadrados para los datos que dado. Luego se crea una clase poly1d basada en la matriz devuelta por polyfit(). Esta clase es una clase polinomial unidimensional. Es una clase de conveniencia, utilizada para encapsular operaciones “naturales” en polinomios. Luego se usa el objeto poly1d para generar un conjunto de valores para usar con el conjunto de valores x para la función py- parcela.parcela(). importar numpy como np importar matplotlib.pyplot como pyplot x = (5, 5,5, 6, 6,5, 7, 8, 9, 10) y = (120, 115, 100, 112, 80, 85, 69, 65)
Generar el gráfico de dispersión
pyplot.dispersión(x, y)
Generar la línea de tendencia
z = np.polyfit(x, y, 1) p = np.poli1d(z) pyplot.plot(x, p(x), ‘r’)
Mostrar la figura
pyplot.mostrar() 6.5 Gráficos circulares Un gráfico circular es un tipo de gráfico en el que un círculo se divide en sectores (o cuñas) que cada uno representa una proporción del todo. Una cuña del círculo representa un contribución de la categoría al total general. Como tal, el gráfico se asemeja a un pastel que se ha cortado en rodajas de diferentes tamaños. Por lo general, los diferentes sectores del gráfico circular se presentan en diferentes colores. y están dispuestos en el sentido de las agujas del reloj alrededor del gráfico en orden de magnitud. Sin embargo, si hay una porción que no contiene una categoría única de datos pero resume varios, por ejemplo, “otros tipos” u “otras respuestas”, incluso si no es el categoría más pequeña, es habitual mostrarlo en último lugar para que no desmerezca las categorías de interés nombradas. 50 6 Graficando con Matplotlib pyplot
El siguiente gráfico ilustra un gráfico circular utilizado para representar el lenguaje de programación. uso de calibre dentro de una organización en particular. El gráfico circular se crea con la función pyplot.pie(). importar matplotlib.pyplot como pyplot etiquetas = (‘Python’, ‘Java’, ‘Scala’, ‘C#’) tallas = [45, 30, 15, 10] pyplot.pie(tamaños, etiquetas = etiquetas, autopct=’%1.f%%’, contrarreloj=falso, ángulo de inicio=90) pyplot.mostrar() La función pyplot.pie() toma varios parámetros, la mayoría de los cuales son opcional. El único parámetro requerido es el primero que proporciona los valores a ser utilizado para los tamaños de cuña o segmento. Los siguientes parámetros opcionales se utilizan en el ejemplo anterior: 6.5 Gráficos circulares 51
• El parámetro etiquetas es un parámetro opcional que puede tomar una secuencia de cadenas que se utilizan para proporcionar etiquetas para cada cuña. • El parámetro autopct toma una cadena (o función) que se usará para formatear el valores numéricos utilizados con cada cuña. • El parámetro en sentido contrario a las agujas del reloj. Por defecto, las cuñas se trazan en el contador en el sentido de las agujas del reloj en pyplot y así garantizar que el diseño se parezca más al tradicional enfoque en el sentido de las agujas del reloj, el parámetro de contrarreloj se establece en Falso. • El parámetro de ángulo de inicio. El ángulo inicial también se ha movido 90° usando el parámetro startangle para que el primer segmento comience en la parte superior de el gráfico. 6.5.1 Segmentos en expansión Puede ser útil enfatizar un segmento particular del gráfico circular desglosándolo; eso lo está separando del resto del gráfico circular. Esto se puede hacer usando el parámetro de explosión de la función pie() que toma una secuencia de valores determinando cuánto debe explotarse un segmento. El impacto visual del gráfico circular también se puede mejorar en este caso agregando un shadow a los segmentos usando el parámetro booleano shadow nombrado. El efecto de estos se muestran a continuación: 52 6 Graficando con Matplotlib pyplot
El programa que generó este gráfico modificado se proporciona a continuación como referencia: importar matplotlib.pyplot como pyplot etiquetas = (‘Python’, ‘Java’, ‘Scala’, ‘C#’) tallas = [45, 30, 15, 10]
solo “explotar” el primer segmento (es decir, ‘Python’)
explotar = (0.1, 0, 0, 0) pyplot.pie(tamaños, explotar = explotar, etiquetas = etiquetas, autopct=’%1.f%%’, sombra = Verdadero, contrarreloj=falso, ángulo de inicio=90) pyplot.mostrar() 6.5.2 Cuándo usar gráficos circulares Es útil considerar qué datos se pueden/deben presentar mediante un gráfico circular. En Los gráficos circulares generales son útiles para mostrar datos que se pueden clasificar en valores nominales. o categorías ordinales. Los datos nominales se clasifican según descriptivos o información cualitativa como los idiomas del programa, el tipo de coche, el país de nacimiento, etc. Los datos ordinales son similares, pero las categorías también se pueden clasificar, por ejemplo, en un Se puede pedir a los encuestados que digan si clasificaron algo como muy pobre, pobre, regular, bueno, muy bueno. Los gráficos circulares también se pueden usar para mostrar datos porcentuales o proporcionales y, por lo general, el porcentaje que representa cada categoría se proporciona junto a la correspondiente rebanada de pastel. Los gráficos circulares también suelen limitarse a presentar datos para seis categorías o menos. Cuando hay más categorías es difícil que el ojo distinga entre las tamaños relativos de los diferentes sectores, por lo que el gráfico se vuelve difícil de interpretar. 6.5 Gráficos circulares 53
6.6 Gráfica de barras Un gráfico de barras es un tipo de cuadro o gráfico que se utiliza para presentar diferentes categorías de datos. Los datos se suelen presentar de forma vertical aunque en algunos casos se pueden utilizar gráficos de barras horizontales. Cada categoría está representada por una barra cuyo altura (o longitud) representa los datos para esa categoría. Debido a que es fácil interpretar los gráficos de barras y cómo cada categoría se relaciona con otra, son uno de los tipos de gráficos más utilizados. También hay varios diferentes variaciones comunes, como gráficos de barras agrupadas y gráficos de barras apiladas. El siguiente es un ejemplo de un gráfico de barras típico. Cinco categorías de pro- Los lenguajes de gramática se presentan a lo largo del eje x mientras que el eje y indica porcentaje de uso. Cada barra representa el porcentaje de uso asociado con cada lenguaje de programación. El programa utilizado para generar la figura anterior se muestra a continuación: 54 6 Graficando con Matplotlib pyplot
Configurar los datos
etiquetas = (‘Python’, ‘Scala’, ‘C#’, ‘Java’, ‘PHP’) índice = (1, 2, 3, 4, 5) # proporciona ubicaciones en el eje x tallas = [45, 10, 15, 30, 22]
Configurar el gráfico de barras
pyplot.bar(índice, tamaños, tick_label=etiquetas)
Configurar el diseño
pyplot.ylabel(‘Uso’) pyplot.xlabel(‘Lenguajes de programación’)
Mostrar el gráfico
pyplot.mostrar() importar matplotlib.pyplot como pyplot El gráfico está construido de tal manera que las longitudes de las diferentes barras son proporcionales dependiendo del tamaño de la categoría que representan. El eje x representa los diferentes categorías y por lo tanto no tiene escala. Para enfatizar el hecho de que las categorías son discreta, se deja un espacio entre las barras en el eje x. El eje y tiene una escala. y esto indica las unidades de medida. 6.6.1 Gráficos de barras horizontales Los gráficos de barras normalmente se dibujan de modo que las barras sean verticales, lo que significa que la más alta la barra, más grande la categoría. Sin embargo, también es posible dibujar gráficos de barras de modo que las barras queden horizontales, lo que significa que cuanto más larga sea la barra, más grande será categoría. Esta es una manera particularmente eficaz de presentar un gran número de diferentes categorías cuando no hay suficiente espacio para colocar todas las columnas requeridas para un gráfico de barras verticales a lo largo de la página. En Matplotlib, la función pyplot.barh() se puede usar para generar un hori- gráfico de barras zonales: 6.6 Gráfica de barras 55
En este caso, la única línea de código a cambiar del ejemplo anterior es: pyplot.barh(x_values, tamaños, tick_label = etiquetas) 6.6.2 Barras de colores También es común colorear diferentes barras en el gráfico en diferentes colores o usando diferentes matices Esto puede ayudar a distinguir una barra de otra. un ejemplo es dada a continuación: 56 6 Graficando con Matplotlib pyplot
El color que se utilizará para cada categoría se puede proporcionar a través del color parámetro a la función bar() (y barh()). Esta es una secuencia de los colores. Aplicar. Por ejemplo, el gráfico de barras de colores anterior se puede generar usando: 6.6.3 Gráficos de barras apiladas Los gráficos de barras también se pueden apilar. Esta puede ser una forma de mostrar los valores totales (y lo que contribuye a esos valores totales) en varias categorías. Es decir, es una forma de ver los totales generales, para varias categorías diferentes en función de cómo los diferentes elementos mentos contribuyen a esos totales. Se utilizan diferentes colores para los diferentes subgrupos que contribuyen a la barra general. En tales casos, se suele proporcionar una leyenda o clave para indicar qué subgrupo que representan cada uno de los matices/colores. La leyenda se puede colocar en el área de la parcela o puede estar ubicado debajo del gráfico. Por ejemplo, en el siguiente gráfico, el uso total de una programación en particular el lenguaje se compone de su uso en juegos y desarrollo web, así como datos análisis de la ciencia. A partir de esta figura podemos ver cuánto cada uso de un lenguaje de programación contribuye al uso general de ese idioma. El programa que generó este cuadro se da a continuación: pyplot.bar(valores_x, tamaños, tick_label=etiquetas, color=(‘rojo’, ‘verde’, ‘azul’, ‘amarillo’, ’naranja’)) 6.6 Gráfica de barras 57
Una cosa a tener en cuenta de este ejemplo es que después de agregar el primer conjunto de valores usando la función pyplot.bar(), es necesario especificar las ubicaciones inferiores para el siguiente conjunto de barras utilizando el parámetro inferior. Podemos hacer esto simplemente usando el valores ya utilizados para web_usage para el segundo gráfico de barras; sin embargo para el tercero gráfico de barras debemos agregar los valores utilizados para web_usage y data_- science_usage juntos (en este caso usando una comprensión de lista for). 6.6.4 Gráficos de barras agrupadas Por último, los gráficos de barras agrupadas son una forma de mostrar información sobre diferentes subgrupos de las categorías principales. En tales casos, se suele proporcionar una leyenda o clave. para indicar qué subgrupo representa cada uno de los tonos/colores. La leyenda se puede colocar en el área de trazado o se puede ubicar debajo del gráfico. Para una categoría en particular, se dibujan gráficos de barras separados para cada uno de los subgrupos. Por ejemplo, en el siguiente gráfico, los resultados obtenidos para dos conjuntos de equipos en importar matplotlib.pyplot como pyplot
Configurar los datos
etiquetas = (‘Python’, ‘Scala’, ‘C#’, ‘Java’, ‘PHP’) índice = (1, 2, 3, 4, 5) uso_web = [20, 2, 5, 10, 14] uso_ciencia_datos = [15, 8, 5, 15, 2] juegos_uso = [10, 1, 5, 5, 4]
Configurar el gráfico de barras
pyplot.bar(índice, uso_web, tick_label=etiquetas, etiqueta=‘web’) pyplot.bar(índice, data_science_usage, tick_label=etiquetas, label=‘ciencia de datos’, bottom=uso_web) web_and_games_usage = [web_usage[i] + data_science_usage[i] para i en el rango (0, len (uso_web))] pyplot.bar(índice, juegos_uso, tick_label=etiquetas, label=‘juegos’, bottom=web_and_games_usage)
Configurar el diseño
pyplot.ylabel(‘Uso’) pyplot.xlabel(‘Lenguajes de programación’) pyplot.leyenda()
Mostrar el gráfico
pyplot.mostrar() 58 6 Graficando con Matplotlib pyplot
se muestra una serie de ejercicios de laboratorio. Así cada equipo tiene una barra para lab1, lab2, lab3 etc. Se deja un espacio entre cada categoría para que sea más fácil comparar el sub categorías. El siguiente programa genera el gráfico de barras agrupadas para los ejercicios de laboratorio ejemplo: Observe en el programa anterior que ha sido necesario calcular el índice para el segundo equipo ya que queremos que las barras se presenten una al lado de la otra. Así el índice para los equipos incluye el ancho de la barra para cada punto de índice, por lo que la primera barra es en la posición de índice 1.35, el segundo en la posición de índice 2.35, etc. Finalmente, la marca por lo tanto, las posiciones deben estar entre las dos barras y, por lo tanto, se calcula tomando en cuenta los anchos de barra. importar matplotlib.pyplot como pyplot ANCHO_BARRA = 0,35
configurar gráficos de barras agrupados
resultado_equipo = (60, 75, 56, 62, 58) equipob_resultados = (55, 68, 80, 73, 55)
Configurar el índice para cada barra
índice_equipo = (1, 2, 3, 4, 5) index_teamb = [i + BAR_WIDTH para i en index_teama]
Determinar el punto medio de los ticks
marcas = [i + BAR_WIDTH / 2 para i en index_teama] tick_labels = (‘Laboratorio 1’, ‘Laboratorio 2’, ‘Laboratorio 3’, ‘Laboratorio 4’, ‘Laboratorio 5’)
Graficar los gráficos de barras
pyplot.bar(index_teama, teama_results, BAR_WIDTH, color=‘b’, etiqueta = ‘Equipo A’) pyplot.bar(index_equipo, equipob_resultados, BAR_ANCHO, color=‘g’, etiqueta = ‘Equipo B’)
Configurar el gráfico
pyplot.xlabel(‘Laboratorios’) pyplot.ylabel(‘Puntuaciones’) pyplot.title(‘Puntuaciones por laboratorio’) pyplot.xticks(ticks, tick_labels) pyplot.leyenda()
Mostrar el gráfico
pyplot.mostrar() 6.6 Gráfica de barras 59
Este programa genera el siguiente gráfico de barras agrupadas: 6.7 Figuras y subtramas Una figura de Matplotlib es el objeto que contiene todos los elementos gráficos que se muestran en una parcela Estos son los ejes, la leyenda, el título y el gráfico de líneas o de barras. sí mismo. Por lo tanto, representa la ventana o página general y es la parte superior, gráfica componente. En muchos casos, la figura está implícita cuando el desarrollador interactúa con el pyplot. API; sin embargo, se puede acceder directamente a la figura si es necesario. La función matplotlib.pyplot.figure() genera un objeto de figura. Esta función devuelve un objeto matplotlib.figura.Figura. entonces es posible para interactuar directamente con la figura objeto. Por ejemplo, es posible agregar ejes a la figura, para agregar subparcelas a un gráfico, etc. Es necesario trabajar directamente con la figura si desea agregar múltiples sub- parcelas a una figura. Esto puede ser útil si lo que se requiere es poder comparar diferentes vistas de los mismos datos una al lado de la otra. Cada subtrama tiene sus propios ejes que pueden coexistir dentro de la figura. Se pueden agregar una o más subparcelas a una figura usando el comando figure.add_ método subtrama(). Este método agrega un Eje a la figura como uno de un conjunto de uno. o más subtramas. Se puede agregar una subparcela usando un número entero de 3 dígitos (o tres enteros) que describen la posición de la subparcela. Los dígitos representan el número de filas, columnas y el índice de la subparcela dentro de la matriz resultante. 60 6 Graficando con Matplotlib pyplot
Por lo tanto, 2, 2, 1 (y 221) indican que la subparcela tomará el primer índice dentro de un cuadrícula de parcelas de dos por dos. A su vez 2, 2, 3 (223) indica que la trama secundaria estará en índice 3, que será la fila 2 y la columna 1 dentro de la cuadrícula de parcelas de 2 por 2. Mientras 2, 2, 4 (o 224) indica que la trama debe agregarse como en el índice 4 o el cuarto subparcela dentro de la cuadrícula (por lo tanto, posición 2 por 2), etc. Por ejemplo, la siguiente figura ilustra cuatro subparcelas presentadas dentro de una sola figura. Cada subtrama se agrega a través del método fifigure.add_subplot(). 6.7 Figuras y subtramas 61
Esta figura es generada por el siguiente programa: importar matplotlib.pyplot como pyplot t = rango (0, 20) s = rango (30, 10, -1)
Configure la cuadrícula de subparcelas para que sea de 2 por 2
grid_size=‘22’
Inicializar una Figura
figura = pyplot.figura()
Añadir primera subparcela
posición = grid_size + ‘1’ print(‘Agregando la primera subtrama a la posición’, posición) eje1 = figura.add_subplot(posición) eje1.set(título=‘subtrama(2,2,1)’) eje1.plot(t, s)
Agregar segunda subtrama
posición = grid_size + ‘2’ print(‘Agregando segunda subtrama a posición’, posición) eje2 = figura.add_subplot(posición) eje2.set(título=‘subtrama(2,2,2)’) eje2.plot(t, s, ‘r-’)
Agregar tercera subtrama
posición = grid_size + ‘3’ print(‘Agregando tercera subtrama a posición’, posición) eje3 = figura.add_subplot(posición) eje3.set(título=‘subtrama(2,2,3)’) eje3.plot(t, s, ‘g-’)
Agregar cuarta subparcela
posición = grid_size + ‘4’ print(‘Agregando cuarta subtrama a posición’, posición) eje4 = figura.add_subplot(posición) eje4.set(título=‘subtrama(2,2,4)’) eje4.plot(t, s, ‘y-’)
Mostrar el gráfico
pyplot.mostrar() 62 6 Graficando con Matplotlib pyplot
La salida de la consola de este programa se muestra a continuación: Agregando la primera subparcela a la posición 221 Agregando la segunda subparcela a la posición 222 Agregando la tercera subparcela a la posición 223 Agregar cuarta subtrama a la posición 224 6.8 Gráficos 3D Se utiliza un gráfico tridimensional para trazar las relaciones entre tres conjuntos de (en lugar de los dos utilizados en los ejemplos presentados hasta ahora en este capítulo). en un gráfico tridimensional, además de los ejes x e y, también hay un eje z. El siguiente programa crea un gráfico 3D simple usando dos conjuntos de valores generado usando la función de rango numpy. Estos luego se convierten en un coordenadas de matrices usando la función numpy meshgrid(). Los valores del eje z son creado usando la función numpy sin(). La superficie del gráfico 3D se traza usando el función plot_surface() del objeto de ejes de futuros. Esto toma la x, y y z coordenadas La función también recibe un mapa de color para usar al renderizar el superficie (en este caso se utiliza el mapa de colores fríos a cálidos de Matplotlib).
Hacer que los datos se muestren
x_values = np.arange(-6, 6, 0.3) valores_y = np.arange(-6, 6, 0.3)
Generar matrices de coordenadas a partir de vectores de coordenadas
valores_x, valores_y = np.meshgrid(valores_x, valores_y)
Generar valores Z como seno de x más valores y
valores_z = np.sin(valores_x + valores_y) importar matplotlib.pyplot como pyplot
Importar mapa de color matplotlib
desde matplotlib importar cm como mapa de colores
Requerido para proyecciones de £D
desde mpl_toolkits.mplot3d importar Axes3D
Proporcionar acceso a funciones numpy
importar numpy como np 6.7 Figuras y subtramas 63
Este programa genera el siguiente gráfico 3D:
Obtener el objeto figura
figura = pyplot.figura()
Obtener el objeto de ejes para el gráfico 3D
ejes = figura.gca(proyección=‘3d’)
Trazar la superficie.
surf = ejes.plot_surface(x_values, y_valores, valores_z, cmap=colormap.coolwarm)
Agregue una barra de color que mapee los valores a los colores.
figura.colorbar(navegar)
Agregar etiquetas al gráfico
pyplot.title(“Gráfico 3D”) ejes.set_ylabel(‘valores y’, tamaño de fuente=8) ejes.set_xlabel(‘valores x’, tamaño de fuente=8) ejes.set_zlabel(‘valores z’, tamaño de fuente=8)
Mostrar el gráfico
pyplot.mostrar() 64 6 Graficando con Matplotlib pyplot
Un punto a tener en cuenta acerca de los gráficos tridimensionales es que no son universalmente aceptado como una buena manera de presentar los datos. Una de las máximas de la visualización de datos ización es mantenerlo simple/mantenerlo limpio. Muchos consideran que un gráfico tridimensional no hace esto y que puede ser difícil ver lo que realmente se muestra o que puede ser difícil interpretar los datos adecuadamente. Por ejemplo, en el gráfico anterior ¿Cuáles son los valores asociados con cualquiera de los picos? Esto es difícil de determinar ya que es difícil ver dónde están los picos en relación con los ejes X, Y y Z. Muchos considere tales gráficos 3D como atractivos para la vista; bonito a la vista, pero no proporciona mucho información. Como tal, el uso de un gráfico 3D debe minimizarse y solo usarse cuando sea realmente necesario. 6.9 Ejercicios La siguiente tabla proporciona información sobre las ciudades del Reino Unido y sus poblaciones. (Tenga en cuenta que se ha omitido Londres porque su población es mucho mayor que la de cualquier otra ciudad y esto distorsionaría el gráfico). Ciudad Población Brístol 617,280 Cardiff 447,287 Baño 94,782 Liverpool 864,122 Glasgow 591,620 Edimburgo 464,990 Leeds 455,123 Lectura 318,014 Swansea 300,352 Manchester 395,515 Usando estos datos crea:
- Un diagrama de dispersión de la ciudad a los datos de población.
- Un gráfico de barras de la ciudad a los datos de población. 6.8 Gráficos 3D sesenta y cinco
Capítulo 7 Interfaces gráficas de usuario 7.1 Introducción Una interfaz gráfica de usuario puede capturar la esencia de una idea o una situación, a menudo evitando la necesidad de un largo pasaje de texto. Estas interfaces pueden salvar a un usuario de la necesidad de aprender comandos complejos. Es menos probable que intimiden a la computadora usuarios y puede proporcionar una gran cantidad de información rápidamente en una forma que puede ser fácilmente asimilable por el usuario. El uso generalizado de interfaces gráficas de alta calidad ha llevado a muchas computadoras los usuarios esperan tales interfaces para cualquier software que utilicen. La mayoría de los lenguajes de programación Los instrumentos incorporan una biblioteca de interfaz gráfica de usuario (GUI) o tienen una tercera bibliotecas de fiestas disponibles. Python es, por supuesto, un lenguaje de programación multiplataforma y esto trae complejidades adicionales ya que el sistema operativo subyacente puede proporcionar diferentes instalaciones de ventanas dependiendo de si el programa se ejecuta en Unix, Sistemas operativos Linux, Mac OS o Windows. En este capítulo, primero presentaremos lo que entendemos por GUI y por WIMP. IU basadas en particular. A continuación, consideraremos la gama de bibliotecas disponibles para Python antes de seleccionar uno para usar. Este capítulo describirá cómo crear pantallas gráficas de cliente enriquecido (aplicación de escritorio) utilizando una de estas bibliotecas GUI. Por lo tanto, en este capítulo consideramos cómo las ventanas, los botones, los campos de texto y las etiquetas, etc. se crean, se añaden a las ventanas, se colocan y se organizan. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_7 67
7.2 GUI y WIMPS GUI (interfaces gráficas de usuario) y WIMP (ventanas, íconos, ratones y ventanas emergentes). Menús) las interfaces de estilo han estado disponibles dentro de los sistemas informáticos durante muchos años, pero siguen siendo uno de los desarrollos más significativos que han ocurrido. Estas interfaces se desarrollaron originalmente por el deseo de abordar muchas de las debilidades percibidas de las interfaces puramente textuales. La interfaz textual a un sistema operativo fue tipificada por un perentorio inmediato. En los sistemas Unix/Linux, por ejemplo, el aviso suele ser simplemente un único caracteres como %, > o $, que pueden resultar intimidantes. Esto es cierto incluso para usuarios experimentados de computadoras si no están familiarizados con la familia de Unix/Linux de sistemas operativos. Por ejemplo, un usuario que desee copiar un archivo de un directorio a otro podría hay que teclear algo como:
cp file.pdf ~otheruser/projdir/srcdir/newfile.pdf Esta larga secuencia debe ingresarse sin errores para ser aceptada. Cualquier error en este comando hará que el sistema genere un mensaje de error que puede o no ser esclarecedor. Incluso cuando los sistemas intentan ser más “fácil de usar” a través de funciones como historiales de comandos, mucho tipeo de flechas normalmente se necesitan claves y nombres de archivo. El problema principal tanto en la entrada como en la salida es el ancho de banda. Por ejemplo, en situaciones donde las relaciones entre grandes cantidades de información deben ser descrito, es mucho más fácil de asimilar esto si la salida se muestra gráficamente que si se muestra como una tabla de cifras. En la entrada, las combinaciones de acciones del mouse pueden recibir un significado que de otro modo sólo podría transmitirse mediante varias líneas de texto. WIMP significa Windows (o Administradores de ventanas), Iconos, Ratones y Ventanas emergentes menús Las interfaces WIMP permiten al usuario superar al menos algunas de las debilidades. nesses de sus contrapartes textuales-es posible proporcionar una imagen pictórica de la sistema operativo que puede basarse en un concepto con el que el usuario puede relacionarse, los menús pueden se puede utilizar en lugar de comandos textuales y la información en general se puede mostrar gráficamente. Los conceptos fundamentales presentados a través de una interfaz WIMP fueron originalmente desarrollado en el Centro de Investigación de Palo Alto de XEROX y utilizado en Xerox Star máquina, pero ganó una aceptación mucho más amplia primero a través de Apple Macintosh y luego implementaciones de IBM PC de interfaces WIMP. La mayoría de los entornos de estilo WIMP usan una analogía de escritorio (aunque esto es menos cierto de dispositivos móviles como teléfonos y tabletas): • toda la pantalla representa una superficie de trabajo (un escritorio), • las ventanas gráficas que pueden superponerse representan hojas de papel en ese escritorio, 68 7 Interfaces gráficas de usuario
• los objetos gráficos se utilizan para conceptos específicos, por ejemplo, archivadores para discos o un cubo de basura para la eliminación de archivos (estos podrían considerarse como escritorio accesorios), • varios programas de aplicación se muestran en la pantalla, estos representan herramientas que podrías usar en tu escritorio. Para interactuar con esta pantalla, el usuario de WIMP cuenta con un mouse (o un lápiz óptico o una pantalla sensible al tacto), que se puede utilizar para seleccionar iconos y menús o para manipular ventanas. La base del software de cualquier entorno de estilo WIMP es el administrador de ventanas. Él controla las ventanas e iconos múltiples, posiblemente superpuestos, que se muestran en el pantalla. También maneja la transferencia de información sobre eventos que ocurren en esos ventanas a la aplicación adecuada y genera los distintos menús y indicaciones utilizadas. Una ventana es un área de la pantalla gráfica en la que una página o parte de una página de se puede mostrar información; puede mostrar texto, gráficos o una combinación de ambos. Estas ventanas pueden estar superpuestas y asociadas con el mismo proceso, o pueden estar asociados con procesos separados. Por lo general, se pueden crear ventanas, abierto, cerrado, movido y redimensionado. Un icono es un pequeño objeto gráfico que suele simbolizar una operación o un entidad más grande, como un programa de aplicación o un archivo. La apertura de un icono provoca ya sea la aplicación asociada a ejecutar o la ventana asociada a ser desplegado. En el corazón de la capacidad de los usuarios para interactuar con tales programas basados en WIMP está el bucle de eventos. Este ciclo escucha eventos como el usuario haciendo clic en un botón o seleccionando un elemento de menú o ingresando un campo de texto. Cuando tal evento ocurre, desencadena el comportamiento asociado (como ejecutar una función vinculada con un botón). 7.3 Marcos de ventanas para Python Python es un lenguaje de programación multiplataforma. Como tales programas de Python pueden ser escrito en una plataforma (como una caja de Linux) y luego se ejecuta en esa plataforma o otra plataforma de sistema operativo (como Windows o Mac OS). Esto puede sin embargo, genera problemas para las bibliotecas que deben estar disponibles en múltiples plataformas de sistemas operativos. El área de las GUI es particularmente un problema como biblioteca escrito para aprovechar las características disponibles en el sistema de Microsoft Windows puede no ser disponible (o puede verse diferente) en sistemas Mac OS o Linux. Cada sistema operativo en el que se ejecuta Python puede tener una o más ventanas sistemas escritos para él y estos sistemas pueden o no estar disponibles en otros sistemas operativos. Esto hace que el trabajo de proporcionar una biblioteca GUI para Python que mucho más difícil. 7.2 GUI y WIMPS 69
Los desarrolladores de GUI de Python han tomado uno de dos enfoques para manejar esto: • Un enfoque es escribir un envoltorio que resuma las instalaciones de GUI subyacentes para que el desarrollador trabaje a un nivel superior al de un sistema de ventanas específico. instalaciones. Luego, la biblioteca de Python asigna (lo mejor que puede) las instalaciones al sistema subyacente que se está utilizando actualmente. • El otro enfoque es proporcionar una envoltura más cercana a un conjunto particular de instalaciones. en el sistema GUI subyacente y solo a los sistemas de destino que admiten esos instalaciones. Algunas de las bibliotecas disponibles para Python se enumeran a continuación y se han clasificado. egorizados en bibliotecas independientes de la plataforma y bibliotecas específicas de la plataforma: 7.3.1 Bibliotecas GUI independientes de la plataforma • Tkinter. Esta es la biblioteca GUI estándar de Python integrada. Está construido encima de la Conjunto de widgets Tcl/Tk que ha existido durante muchos años para muchos sistemas operativos. Tcl significa Tool Command Language mientras que Tk es el kit de herramientas de interfaz gráfica de usuario para Tcl. • wxPython. wxWidgets es una biblioteca GUI gratuita y altamente portátil. Está escrito en C+
- y puede proporcionar una apariencia nativa en sistemas operativos como Windows, Mac OS, Linux, etc. wxPython es un conjunto de enlaces de Python para wxWidgets. Esta es la biblioteca que usaremos en este capítulo. • PyQT o PySide, ambas bibliotecas envuelven las funciones del kit de herramientas de Qt. qt es un sistema de desarrollo de software multiplataforma para la implementación de GUI y aplicaciones. 7.3.2 Bibliotecas GUI específicas de la plataforma • PyObjc es una biblioteca específica de Mac OS que proporciona un puente Objective-C para el Bibliotecas GUI de Apple Mac Cocoa. • PythonWin proporciona un conjunto de envolturas alrededor de Microsoft Windows Clases de base y se pueden usar para crear GUI basadas en Windows. 70 7 Interfaces gráficas de usuario
7.4 Recursos en línea Existen numerosas referencias en línea que respaldan el desarrollo de GUI y de Las GUI de Python en particular, que incluyen: • https://www.wxpython.org página de inicio de wxPython. • https://www.tcl.tk para obtener información sobre Tcl/Tk. • https://www.qt.io Para obtener información sobre el software y la interfaz de usuario multiplataforma de Qt biblioteca de desarrollo • https://wiki.python.org/moin/PyQt Para obtener información sobre PyQt. • https://pypi.org/project/PySide/ que proporciona información del proyecto para PySide. • https://en.wikipedia.org/wiki/Cocoa_(API) para la página de Wikipedia en el Biblioteca MacOS Cocoa. • https://pythonhosted.org/pyobjc/ para obtener información sobre Python para Objective-C puente. • https://docs.microsoft.com/en-us/cpp/mfc/mfc-desktop-applications?view=vs- 2019 Proporciona una introducción a las clases de Microsoft Foundation. • https://www.cgl.ucsf.edu/Outreach/pc204/pythonwin.html para obtener información sobre Python Win. 7.4 Recursos en línea 71
Capítulo 8 La biblioteca de GUI de wxPython 8.1 La biblioteca wxPython La biblioteca wxPython es una biblioteca GUI multiplataforma (o kit de herramientas) para Python. Él permite a los programadores desarrollar interfaces de usuario altamente gráficas para sus programas usando conceptos comunes como barras de menú, menús, botones, campos, paneles y marcos En wxPython, todos los elementos de una GUI están contenidos dentro de las ventanas de nivel superior como un wx.Frame o un wx.Dialog. Estas ventanas contienen comandos gráficos. componentes conocidos como widgets o controles. Estos widgets/controles se pueden agrupar juntas en Paneles (que pueden o no tener una representación visible). Por lo tanto, en wxPython podríamos construir una GUI a partir de: • Marcos que proporcionan la estructura básica de una ventana: bordes, una etiqueta y alguna funcionalidad básica (por ejemplo, cambiar el tamaño). • Diálogos que son como Marcos pero proporcionan menos controles de borde. • Widgets/Controles que son objetos gráficos que se muestran en un marco. Algún otro los idiomas se refieren a ellos como componentes de la interfaz de usuario. Ejemplos de widgets son botones, casillas de verificación, listas de selección, etiquetas y campos de texto. • Los contenedores son componentes que se componen de uno o más componentes (o contenedores). Todos los componentes dentro de un contenedor (como un panel) se pueden tratado como una sola entidad. Así, una GUI se construye jerárquicamente a partir de un conjunto de widgets, contenedores y uno o más Cuadros (o en el caso de un cuadro de diálogo emergente, Diálogos). Esto es se ilustra a continuación para una ventana que contiene varios paneles y widgets: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_8 73
Ventanas como Marcos y Diálogos tienen una jerarquía de componentes que se utiliza (entre otras cosas) para determinar cómo y cuándo los elementos de la ventana son dibujado y redibujado. La jerarquía de componentes está enraizada con el marco, dentro qué componentes y contenedores se pueden agregar. La figura anterior ilustra una jerarquía de componentes para un marco, con dos componentes Paneles de contenedores y algunos widgets/componentes de interfaz de usuario básicos que se encuentran dentro de los paneles. Nota que un panel puede contener otro subpanel con diferentes widgets. 8.1.1 Módulos de wxPython La biblioteca wxPython se compone de muchos módulos diferentes. Estos módulos proporcionar diferentes funciones desde el módulo central de wx hasta el wx.html orientado a html y módulos wx.html2. Estos módulos incluyen: • wx, que contiene los widgets y las clases principales de la biblioteca wx. • wx.adv que proporciona widgets y widgets menos utilizados o más avanzados clases • wx.grid contiene widgets y clases que admiten la visualización y edición de datos tabulados. • wx.richtext consta de widgets y clases que se utilizan para mostrar múltiples estilos de texto e imágenes. • wx.html comprende widgets y clases de soporte para un renderizador html genérico. • wx.html2 proporciona widgets adicionales y clases de soporte para un html nativo renderizador, con soporte para CSS y javascript. 74 8 La biblioteca de GUI de wxPython
8.1.2 Ventanas como Objetos En wxPython, los marcos y los diálogos, así como sus contenidos, son instancias de adecuado clases (semejante como Marco, Diálogo, Panel, Botón o Texto estático). Así, cuando creas una ventana, creas un objeto que sabe cómo mostrarse en la pantalla de la computadora. Debe indicarle qué mostrar y luego dígale que muestre su contenido al usuario. Debe tener en cuenta los siguientes puntos durante la lectura de este capítulo; lo ayudarán a comprender lo que debe hacer: • Una ventana se crea instanciando un objeto Frame o Dialog. • Usted define lo que muestra la ventana creando un widget que tiene un componente padre priado. Esto agrega el widget a un contenedor, como un tipo de panel o un marco. • Puede enviar mensajes a la ventana para cambiar su estado, realizar una operación, y mostrar un objeto gráfico. • La ventana, o los componentes dentro de la ventana, pueden enviar mensajes a otros objetos en respuesta a las acciones del usuario (o del programa). • Todo lo que se muestra en una ventana es una instancia de una clase y es potencialmente sujeto a todo lo anterior. • wx.App maneja el bucle de eventos principal de la aplicación GUI. 8.1.3 Un ejemplo sencillo A continuación se muestra un ejemplo de cómo crear una ventana muy simple usando wxPython. El El resultado de ejecutar este breve programa se muestra aquí tanto para Mac como para Windows. ORDENADOR PERSONAL: Este programa crea una ventana de nivel superior (el wx.Frame) y le da un título. Él también crea una etiqueta (un objeto wx.StaticText) que se mostrará dentro del marco. 8.1 La biblioteca wxPython 75
Para utilizar la librería wxPython es necesario importar el módulo wx. El programa también crea una nueva instancia del objeto de aplicación llamada wx. aplicación(). Cada programa GUI de wxPython debe tener un objeto de aplicación. Es el equivalente a la función main() en muchas aplicaciones que no son GUI, ya que ejecutará el aplicación GUI para usted. También proporciona funciones predeterminadas para definir el inicio y operaciones de apagado y se pueden subclasificar para crear un comportamiento personalizado. La clase wx.StaticText se usa para crear una etiqueta de línea única (o múltiple). En este caso, la etiqueta muestra la cadena ‘Hello Python’. El objeto StaticText es construido con referencia a su contenedor principal. Este es el recipiente dentro del cual se mostrará el texto. En este caso, el StaticText se muestra directamente dentro del marco y, por lo tanto, el objeto marco es el objeto principal que lo contiene. En contraste el marco que es una ventana de nivel superior, no tiene un contenedor principal. También tenga en cuenta que el marco debe mostrarse (mostrar) para que el usuario lo vea. Este es porque puede haber varias ventanas diferentes que deben mostrarse (o oculto) en diferentes situaciones para una aplicación. Finalmente, el programa inicia el bucle de eventos principal de las aplicaciones; dentro de este bucle el El programa escucha cualquier entrada del usuario (como solicitar que se cierre la ventana). 8.2 La clase wx.App La clase wx.App representa la aplicación y se utiliza para: • iniciar el sistema wxPython e inicializar el juego de herramientas GUI subyacente, • establecer y obtener propiedades para toda la aplicación, • implementar el mensaje principal del sistema de ventanas nativas o el ciclo de eventos, y enviar eventos a instancias de ventana. importar wx
Crear el objeto de la aplicación
aplicación = wx.Aplicación()
Ahora crea un marco (que representa la ventana)
frame = wx.Frame(padre=Ninguno, title=‘Simple Hello World’)
Y añádele una etiqueta de texto
texto = wx.StaticText(padre=marco, etiqueta=‘Hola Python’)
Mostrar la ventana (marco)
marco.Mostrar()
Iniciar el bucle de eventos
aplicación.MainLoop() 76 8 La biblioteca de GUI de wxPython
Cada aplicación de wxPython debe tener una sola instancia de wx.App. La creación de todos los objetos de la interfaz de usuario debe retrasarse hasta después de que el objeto wx.App se haya creado para garantizar que la plataforma GUI y wxWidgets hayan sido completamente inicializado Es común subclasificar la clase wx.App y anular métodos como OnPreInit y OnExit para proporcionar un comportamiento personalizado. Esto asegura que el el comportamiento requerido se ejecuta en los momentos apropiados. Los métodos que se pueden anular para este fin son: • OnPreInit, este método se puede anular para definir el comportamiento que debe ser se ejecuta una vez que se crea el objeto de la aplicación, pero antes de que se haya ejecutado el método OnInit. llamado. • OnInit Esto se espera para crear la ventana principal de aplicaciones, mostrar que ventana etc • OnRun, Este es el método utilizado para iniciar la ejecución del programa principal. • OnExit, esto se puede anular para proporcionar cualquier comportamiento que deba llamarse justo antes de que la aplicación salga. Como ejemplo, si deseamos configurar una aplicación GUI tal que el marco principal se inicializa y se muestra después de que se haya creado una instancia de wx.App, entonces la forma más segura es anular el método OnInit() de la clase wx.App en una subclase adecuada. El método debe devolver Verdadero o Falso; donde True se usa para indicar que el procesamiento de la solicitud debe continuar y Falso indica que el la aplicación debe terminar inmediatamente (generalmente como resultado de algún evento inesperado). asunto). A continuación se muestra un ejemplo de subclase de wx.App: Ahora se puede crear una instancia de esta clase y se puede iniciar MainLoop, por ejemplo: También es posible anular OnExit() para limpiar cualquier cosa inicializada en el método OnInit(). clase MainApp(wx.App): def OnInit(auto): """ Inicializar la aplicación GUI principal""" marco = Marco de Bienvenida() marco.Mostrar()
Indicar si el procesamiento debe continuar o no
volver verdadero
Ejecutar la aplicación GUI
aplicación = aplicación principal () aplicación.MainLoop() 8.2 La clase wx.App 77
8.3 Clases de ventana Las clases de contenedores de ventanas o widgets que se usan comúnmente dentro de un aplicación wxPython son: • wx.Dialog Un diálogo es una ventana de nivel superior que se utiliza para las ventanas emergentes donde el usuario tiene una capacidad limitada para interactuar con la ventana. En muchos casos, el usuario sólo puede ingrese algunos datos y/o acepte o rechace una opción. • wx.Frame Un marco es una ventana de nivel superior cuyo tamaño y posición se pueden configurar y puede (generalmente) ser controlado por el usuario. • wx.Panel Es un contenedor (ventana de nivel no superior) en el que los controles/widgets se puede colocar. Esto se usa a menudo junto con un Diálogo o un Marco para administrar el posicionamiento de los widgets dentro de la GUI. La jerarquía de herencia para estas clases se proporciona a continuación como referencia: Como ejemplo de uso de un Marco y un Panel, la siguiente aplicación crea dos Paneles y los muestra dentro de un Marco de nivel superior. El fondo el color del marco es el gris predeterminado; mientras que el color de fondo para la primera El panel es azul y para el segundo panel es rojo. La pantalla resultante se muestra abajo: 78 8 La biblioteca de GUI de wxPython
El programa que generó esta GUI se muestra a continuación: importar wx clase SampleFrame(wx.Frame): def init(uno mismo): super().init(padre=Ninguno, title=‘Aplicación de muestra’, tamaño=(300, 300))
Configure el primer Panel para que esté en la posición 1, 1
(el predeterminado) y de tamaño 300 por 100
con fondo azul
self.panel1 = wx.Panel(self) self.panel1.SetSize(300, 100) self.panel1.SetBackgroundColour(wx.Colour(0, 0, 255))
Configure el segundo Panel para que esté en la posición 1, 110
y de tamaño 300 por 100 con fondo rojo
self.panel2 = wx.Panel(self) self.panel2.SetSize(1, 110, 300, 100) self.panel2.SetBackgroundColor(wx.Colour(255, 0, 0)) clase MainApp(wx.App): def OnInit(auto): """ Inicializar la aplicación GUI principal""" marco = Marco de Muestra() marco.Mostrar() volver verdadero
Ejecutar la aplicación GUI
aplicación = aplicación principal () aplicación.MainLoop() 8.3 Clases de ventana 79
SampleFrame es una subclase de la clase wx.Frame; por lo tanto hereda todo la funcionalidad de un marco de nivel superior (ventana). Dentro del init() del SampleFrame se llama al método init() de las superclases. Esto se usa para establecer el tamaño del Marco y para darle un título al Marco. Tenga en cuenta que el Frame también indica que no tiene una ventana principal. Cuando se crea el Panel es necesario especificar la ventana (o en este caso Frame) dentro del cual se mostrará. Este es un patrón común dentro wxPython. También tenga en cuenta que el método SetSize de la clase Panel también permite la posición que se especificará y que la clase Color es la clase Color de wxPython. 8.4 Widget/clases de control Aunque hay muchos widgets/controles disponibles para el desarrollador, la mayoría comúnmente utilizados incluyen: • wx.Button/wx.ToggleButton/wx.RadioButton Estos son widgets que proporcionan un comportamiento similar al de un botón dentro de una GUI. • wx.TextCtrl Este widget permite mostrar y editar texto. puedo ser un Widget de una sola línea o de múltiples líneas dependiendo de la configuración. • wx.StaticText Se utiliza para mostrar una o más líneas de texto de solo lectura. En muchos bibliotecas este widget se conoce como una etiqueta. • wx.StaticLine Una línea utilizada en los diálogos para separar grupos de widgets. El la línea puede ser vertical u horizontal. • wx.ListBox Este widget se utiliza para permitir que un usuario seleccione una opción de un lista de opciones. • wx.MenuBar/wx.Menu/wx.MenuItem. Los componentes que se pueden utilizar para construir un conjunto de menús para una interfaz de usuario. • wx.ToolBar Este widget se utiliza para mostrar una barra de botones y/u otros los widgets generalmente se colocan debajo de la barra de menú en un wx.Frame. La jerarquía de herencia de estos widgets se proporciona a continuación. Tenga en cuenta que todos ellos heredan de la clase Control (de ahí que a menudo se les llame Controles). así como Widgets o componentes GUI). 80 8 La biblioteca de GUI de wxPython
Cada vez que se crea un widget, es necesario proporcionar la ventana del contenedor clase que lo contendrá, como un marco o un panel, por ejemplo: En este fragmento de código se está creando un wx.Button que tendrá una etiqueta ‘Enter’ y se mostrará dentro del Panel dado. 8.5 Diálogos La clase genérica wx.Dialog se puede utilizar para crear cualquier cuadro de diálogo personalizado que necesite. Se puede utilizar para crear diálogos modales y no modales: • Un cuadro de diálogo modal bloquea el flujo del programa y la entrada del usuario en otras ventanas hasta que es despedido. • Un diálogo no modal se comporta más como un marco en el que el flujo del programa continúa, y la entrada en otras ventanas todavía es posible. • La clase wx.Dialog proporciona dos versiones del método show para admitir diálogos modales y no modales. El método ShowModal() se utiliza para mostrar un diálogo modal, mientras que Show() se usa para mostrar un diálogo no modal. Además de la clase genérica wx.Dialog, la biblioteca wxPython proporciona numerosos diálogos preconstruidos para situaciones comunes. Estos diálogos preconstruidos incluyen: • wx.ColourDialog Esta clase se utiliza para generar un cuadro de diálogo de selección de color. • wx.DirDialog Esta clase proporciona un diálogo de selección de directorios. • wx.FileDialog Esta clase proporciona un diálogo de selección de archivos. • wx.FontDialog Esta clase proporciona un diálogo de selección de fuente. • wx.MessageDialog Esta clase se puede utilizar para generar un mensaje de una o varias líneas. mensaje o diálogo de información. Puede admitir las opciones Sí, No y Cancelar. Él se puede utilizar para mensajes genéricos o para mensajes de error. • wx.MultiChoiceDialog Este cuadro de diálogo se puede utilizar para mostrar una serie de cadenas y permite al usuario seleccionar uno o más valores para la lista. • wx.PasswordEntryDialog Esta clase representa un diálogo que permite una usuario para ingresar una cadena de contraseña de una línea del usuario. • wx.ProgressDialog Si es compatible con la plataforma GUI, entonces esta clase proporcionar el cuadro de diálogo de progreso nativo de la plataforma, de lo contrario, utilizará el puro Pitón wx.GenericProgressDialog. El wx. GenericProgressDialog muestra un mensaje corto y una barra de progreso. • wx.TextEntryDialog Esta clase proporciona un cuadro de diálogo que solicita una línea cadena de texto del usuario. enter_button = wx.Button(panel, etiqueta=‘Entrar’) 8.4 Widget/clases de control 81
La mayoría de los diálogos que devuelven un valor siguen el mismo patrón. este patrón devuelve un valor del método ShowModel() que indica si el usuario seleccionó Aceptar o CANCELAR (utilizando el valor de retorno wx.ID_OK o wx.ID_CANCEL). El El valor seleccionado/ingresado se puede obtener a partir de un método de obtención adecuado, como ObtenerDatosColor() para el ColorDiálogo o ObtenerRuta() para el DirDiálogo. 8.6 Organizar widgets dentro de un contenedor Los widgets se pueden ubicar dentro de una ventana utilizando coordenadas específicas (como 10 píxeles hacia abajo y 5 píxeles de ancho). Sin embargo, esto puede ser un problema si usted está considerando las aplicaciones multiplataforma, esto se debe a cómo se representa un botón (dibujado) en una Mac es diferente a Windows y diferente nuevamente de la ventana sistemas en Linux/Unix, etc. Esto significa que se debe dar una cantidad diferente de espacio en diferentes plataformas. formularios Además, las fuentes utilizadas con cuadros de texto y etiquetas difieren entre diferentes plataformas que también requieren diferencias en el diseño de los widgets. Para superar esto, wxPython proporciona Sizers. Los calibradores trabajan con un recipiente como como un marco o un panel para determinar cómo deben distribuirse los widgets contenidos. Los widgets se agregan a un medidor que luego se configura en un contenedor como un Panel. Un Sizer es, por lo tanto, un objeto que funciona con un contenedor y la ventana del host. plataforma para determinar la mejor manera de mostrar los objetos en la ventana. El el desarrollador no necesita preocuparse por lo que sucede si un usuario cambia el tamaño de una ventana o si el programa se ejecuta en una plataforma de ventanas diferente. Por lo tanto, los dimensionadores ayudan a producir interfaces de usuario portátiles y presentables. de hecho uno Sizer se puede colocar dentro de otro Sizer para crear diseños de componentes complejos. Hay varios tamaños disponibles, incluyendo: • wx.BoxSizer Este dimensionador se puede utilizar para colocar varios widgets en una fila o organización de las columnas según la orientación. Cuando el BoxSizer es creado, la orientación se puede especificar usando wx.VERTICAL o wx, HORIZONTAL. • wx.GridSizer Este dimensionador dispone los widgets en una cuadrícula bidimensional. Cada celda dentro de la cuadrícula tiene el mismo tamaño. Cuando se crea el objeto GridSizer, es posible especificar el número de filas y columnas que tiene la grilla. Tambien es posible especificar el espacio entre las celdas tanto horizontal como verticalmente • wx.FlexGridSizer Este dimensionador es una versión un poco más flexible del GridSizer. En esta versión, no todas las columnas y filas deben tener el mismo tamaño (aunque todas las celdas de la misma columna tienen el mismo ancho y todas las celdas de la misma fila tienen la misma altura). 82 8 La biblioteca de GUI de wxPython
• wx.GridBagSizer es el medidor de tamaño más flexible. Permite que los widgets sean posi- en relación con la cuadrícula y también permite que los widgets abarquen varias filas y/o columnas Para usar un Sizer, primero debe ser instanciado. Cuando se crean los widgets, deben agregarse al calibrador y luego colocar el calibrador en el recipiente. Por ejemplo, el siguiente código usa un GridSizer usado con un Panel para diseño de cuatro widgets compuestos por dos botones, una etiqueta StaticText y una Campo de entrada TextCtrl: La pantalla resultante se muestra a continuación:
Crear el panel
panel = wx.Panel(self)
Crea el medidor para usar con 4 filas y 1 columna
Y 5 espacios alrededor de cada celda
rejilla = wx.GridSizer(4, 1, 5, 5)
Crea los widgets
texto = wx.TextCtrl(panel, tamaño=(150, -1)) enter_button = wx.Button(panel, etiqueta=‘Entrar’) etiqueta = wx.StaticText(panel,etiqueta=‘Bienvenido’) botón_mensaje = wx.Button(panel, etiqueta=‘Mostrar mensaje’)
Agregue los widgets al medidor de cuadrícula
grid.AddMany([texto, botón_ingresar, etiqueta, botón_mensaje])
Establecer el medidor en el panel
panel.SetSizer(cuadrícula) 8.6 Organizar widgets dentro de un contenedor 83
8.7 Dibujo Gráficos En capítulos anteriores vimos la API de gráficos Turtle para generar gráficos vectoriales y gráficos de trama en Python. La biblioteca wxPython proporciona sus propias instalaciones para generar multiplataforma visualizaciones gráficas utilizando líneas, cuadrados, círculos, texto, etc. Esto se proporciona a través de la Contexto del dispositivo. Un contexto de dispositivo (a menudo abreviado como solo DC) es un objeto en el que los gráficos y el texto se puede dibujar. Está destinado a permitir que diferentes dispositivos de salida tengan gráficos comunes. API (también conocida como GDI o interfaz de dispositivo gráfico). Contextos de dispositivos específicos puede ser instanciado dependiendo de si el programa va a usar una ventana en un pantalla de computadora o algún otro medio de salida (como una impresora). Hay varios tipos de contexto de dispositivo disponibles, como wx.WindowDC, wx. PaintDC y wx.ClientDC: • El wx.WindowDC se usa si queremos pintar en toda la ventana (Solo Windows). Esto incluye decoraciones de ventanas. • El wx.ClientDC se utiliza para dibujar en el área de cliente de una ventana. El cliente área es el área de una ventana sin sus decoraciones (título y borde). • El wx.PaintDC también se usa para dibujar en el área del cliente, pero está destinado a Admite el mecanismo de manejo de eventos de pintura de actualización de ventana. Tenga en cuenta que wx.PaintDC debe usarse solo desde un wx.PaintEvent controlador mientras que wx.ClientDC nunca debe usarse desde un wx.PaintEvent manipulador. Cualquiera que sea el contexto de dispositivo que se utilice, todos admiten un conjunto similar de métodos que se utilizan para generar gráficos, tales como: • DrawCircle (x, y, radio) Dibuja un círculo con el centro dado y radio. • DrawEllipse (x, y, ancho, alto) Dibuja una elipse contenida en el rectángulo especificado con la esquina superior izquierda dada y el tamaño dado o directamente. • DrawPoint (x, y) Dibuja un punto utilizando el color de la pluma actual. • DrawRectangle (x, y, ancho, alto) Dibuja un rectángulo con el coordenada de esquina y tamaño dados. • DibujarTexto (texto, x, y) Dibuja una cadena de texto en el punto especificado, usando el fuente de texto actual y los colores de fondo y primer plano del texto actual. • DrawLine (pt1, pt2)/DrawLine (x1, y1, x2, y2) Este método dibuja una línea desde el primer punto hasta el segundo. También es importante comprender cuándo se actualiza/redibuja el contexto del dispositivo. Por ejemplo, si cambia el tamaño de una ventana, la maximiza, la minimiza, la mueve o modifica su contenido se vuelve a dibujar la ventana. Esto genera un evento, un PaintEvent. Puede vincular un método al PaintEvent (usando wx.EVT_PAINT) que puede ser llamado cada vez que se actualiza la ventana. 84 8 La biblioteca de GUI de wxPython
Este método se puede usar para dibujar cualquier contenido de la ventana. ser. Si no vuelve a dibujar el contenido del contexto del dispositivo en tal método que lo que dibujó previamente se mostrará cuando se actualice la ventana. El siguiente programa simple ilustra el uso de algunos de los métodos Draw enumerados anteriormente y cómo se puede vincular un método al evento de pintura para que la pantalla sea actualizado apropiadamente cuando se usa un contexto de dispositivo: Cuando se ejecuta este programa, se genera la siguiente pantalla: importar wx clase DrawingFrame(wx.Frame): def init(uno mismo, titulo): super().init(Ninguno, titulo=titulo, tamaño=(300, 200)) self.Bind(wx.EVT_PAINT, self.on_paint) def on_paint(self, evento): “““configurar el contexto del dispositivo (DC) para pintar””” dc = wx.PaintDC(self) dc.DrawLine(10, 10, 60, 20) dc.DibujarRectángulo(20, 40, 40, 20) dc.DrawText(“Hola mundo”, 30, 70) dc.DrawCircle(130, 40, radio=15) clase GraphicApp(wx.App): def OnInit(auto): """ Inicializar la pantalla GUI""" marco = MarcoDibujo(título=‘PyDraw’) marco.Mostrar() volver verdadero
Ejecutar la aplicación GUI
app = Aplicación Gráfica() aplicación.MainLoop() 8.7 Dibujo Gráficos 85
8.8 Recursos en línea Existen numerosas referencias en línea que respaldan el desarrollo de GUI y de Las GUI de Python en particular, que incluyen: • https://docs.wxpython.org para obtener documentación sobre wxPython. • https://www.wxpython.org página de inicio de wxPython. • https://www.wxwidgets.org Para obtener información sobre los wxWidgets subyacentes Biblioteca GUI multiplataforma. 8.9 Ejercicios 8.9.1 Aplicación de GUI simple En este ejercicio, implementará su propia aplicación GUI simple. La aplicación debe generar la pantalla para una interfaz de usuario simple. Un ejemplo de la interfaz de usuario se da a continuación: Observe que hemos agregado una etiqueta a los campos de entrada para el nombre y la edad; puede gestionar su visualización mediante un panel anidado. En el próximo capítulo agregaremos el manejo de eventos a esta aplicación para que el la aplicación puede responder a los clics de los botones, etc. 86 8 La biblioteca de GUI de wxPython
Capítulo 9 Eventos en las interfaces de usuario de wxPython 9.1 Manejo de eventos Los eventos son una parte integral de cualquier GUI; representan las interacciones del usuario con el interfaz como hacer clic en un botón, ingresar texto en un campo, seleccionar un menú opción etc El bucle de eventos principal escucha un evento; cuando ocurre uno procesa ese evento (lo que generalmente da como resultado que se llame a una función o método) y luego espera el próximo evento a suceder. Este bucle se inicia en wxPython a través de la llamada a la Método MainLoop() en el objeto wx.App. Esto plantea la pregunta “¿qué es un evento?”. Un objeto de evento es una pieza de información que representa alguna interacción que ocurrió típicamente con la GUI (aunque un evento puede ser generado por cualquier cosa). Un evento es procesado por un Controlador de eventos. Este es un método o función que se llama cuando ocurre el evento. El evento se pasa al controlador como un parámetro. Se utiliza un Carpeta de eventos para enlazar un evento a un controlador de eventos. 9.2 Definiciones de eventos Es útil resumir las definiciones en torno a los eventos, ya que la terminología utilizada puede ser confuso y es muy similar: • El evento representa información del marco de GUI subyacente que describe algo que ha sucedido y cualquier dato asociado. el especifico los datos disponibles diferirán dependiendo de lo que haya ocurrido. Por ejemplo, si un la ventana se ha movido, entonces los datos asociados se relacionarán con la ventana nueva ubicacion. Donde como un CommandEvent generado por una acción de selección de un ListBox proporciona el índice de elementos para la selección. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_9 87
• Bucle de eventos: el bucle de procesamiento principal de la GUI que espera que se produzca un evento. ocurrir. Cuando ocurre un evento, se llama al controlador de eventos asociado. • Controladores de eventos, estos son métodos (o funciones) que se llaman cuando un evento ocurre. • Los enlazadores de eventos asocian un tipo de evento con un controlador de eventos. Hay diferentes carpetas de eventos para diferentes tipos de eventos. Por ejemplo, el archivador de eventos asociado con wx.MoveEvent se denomina wx.EVT_MOVE. La relación entre el evento, el controlador de eventos a través del archivador de eventos es ilustrado a continuación: Los tres recuadros superiores ilustran los conceptos, mientras que los 3 recuadros inferiores proporcionan una ejemplo concreto de vincular un Move_Event a un método on_move() a través del Carpeta EVT_MOVE. 9.3 Tipos de eventos Hay numerosos tipos diferentes de eventos, incluyendo: • wx.CloseEvent utilizado para indicar que se ha cerrado un cuadro o diálogo. El El archivador de eventos para este evento se llama wx.EVT_CLOSE. • wx.CommandEvent utilizado con widgets como botones, cuadros de lista, menú elementos, botones de opción, barras de desplazamiento, controles deslizantes, etc. Según el tipo de widget que generó el evento se puede proporcionar información diferente. por ejemplo, para un botón un CommandEvent indica que se ha hecho clic en un botón donde como para un ListBox indica que se ha seleccionado una opción, etc. Evento diferente Los aglutinantes se utilizan para diferentes situaciones de eventos. Por ejemplo, para vincular un comando evento a un controlador de eventos para un botón, entonces se usa el enlazador wx.EVT_BUTTON; mientras que para un ListBox se puede usar un archivador wx.EVT_LISTBOX. • wx.FocusEvent Este evento se envía cuando el foco de una ventana cambia (pierde o gana el foco). Puede seleccionar una ventana ganando foco usando el wx. Carpeta de eventos EVT_SET_FOCUS. El wx.EVT_KILL_FOCUS se utiliza para enlazar un controlador de eventos que se llamará cuando una ventana pierda el foco. 88 9 Eventos en las interfaces de usuario de wxPython
• wx.KeyEvent Este evento contiene información relacionada con la pulsación de una tecla o liberar. • wx.MaximizeEvent Este evento se genera cuando se abre una ventana de nivel superior. maximizado • wx.MenuEvent Este evento se usa para acciones orientadas al menú, como el menú ser abierto o cerrado; sin embargo, debe tenerse en cuenta que este evento no se utiliza cuando se selecciona un elemento de menú (MenuItems genera CommandEvents). • wx.MouseEvent Esta clase de evento contiene información sobre los eventos generado por el mouse: esto incluye información sobre qué botón del mouse se presionado (y soltado) y si se hizo doble clic con el mouse, etc. • wx.WindowCreateEvent Este evento se envía justo después de que se abra la ventana real. creado. • wx.WindowDestoryedEvent Este evento se envía lo antes posible durante el proceso de destrucción de la ventana. 9.4 Vinculación de un evento a un controlador de eventos Un evento está vinculado a un controlador de eventos mediante el método Bind() de un evento generar un objeto (como un botón, campo, elemento de menú, etc.) a través de un Evento con nombre Aglutinante. Por ejemplo: botón.Bind(wx.EVT_BUTTON, self.event_handler_method) 9.5 Implementación del manejo de eventos Hay cuatro pasos involucrados en la implementación del manejo de eventos para un widget o ventana, estos son:
- Identificar el evento de interés. Muchos widgets generarán diferentes eventos en Diferentes situaciones; por lo tanto, puede ser necesario determinar qué evento están interesadas en.
- Busque el nombre correcto del Carpeta de eventos, p. wx.EVT_CLOSE, wx.EVT_MOVE o wx.EVT_BUTTON, etc. De nuevo, puede encontrar que el widget que está Ested in admite numerosos aglutinantes de eventos diferentes que se pueden utilizar en diferentes diferentes situaciones (incluso para el mismo evento).
- Implemente un controlador de eventos (es decir, un método o función adecuado) que será llama cuando ocurre el evento. El controlador de eventos se proporcionará con el evento. objeto.
- Vincule el evento al controlador de eventos a través del nombre del Binder usando Bind() método del widget o ventana. 9.3 Tipos de eventos 89
Para ilustrar esto utilizaremos un ejemplo sencillo. Escribiremos una aplicación de manejo de eventos muy simple. Esta aplicación tener un Marco que contenga un Panel. El Panel contendrá una etiqueta usando el wx. Clase de texto estático. Definiremos un controlador de eventos llamado on_mouse_click() que moverá la etiqueta StaticText a la ubicación actual del mouse cuando se presiona el botón izquierdo del mouse. presionado. Esto significa que podemos mover la etiqueta por la pantalla. Para hacer esto, primero necesitamos determinar el widget que se usará para generar el evento. En este caso es el panel que contiene la etiqueta de texto. Habiendo hecho esto nos puede ver la clase Panel para ver qué eventos y enlaces de eventos admite. Él vueltas afuera eso el Panel clase solo directamente define apoyo para Eventos clave de navegación. Esto no es realmente lo que queremos; sin embargo, el La clase Panel extiende la clase Ventana. La clase Window admite numerosos enlaces de eventos, desde los asociados con establecer el enfoque (wx.EVT_SET_FOCUS y wx.EVT_KILL_FOCUS) para pulsaciones de teclas (wx.EVT_KEY_DOWN y wx.EVT_KEY_UP), así como el mouse eventos. Sin embargo, existen numerosos enlaces de eventos de ratón diferentes. Estos permiten clics con el botón izquierdo, medio y derecho del mouse para ser recogido, clics hacia abajo para ser identificadas, situaciones como la entrada o salida del ratón de la ventana, etc. Sin embargo, el enlace que nos interesa para un MouseEvent es el wx. enlace EVT_LEFT_DOWN; esto se activa en el MoueEvent cuando el mouse izquierdo se presiona el botón (también existe el enlace wx.EVT_LEFT_UP que se puede usar para recoger un evento que ocurre cuando se suelta el botón izquierdo del ratón). Ahora sabemos que necesitamos vincular el controlador de eventos on_mouse_click() a MouseEvent a través del archivador de eventos wx.EVT_LEFT_DOWN, por ejemplo: self.panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_click) Todos los métodos de manejo de eventos toman dos parámetros, uno mismo y el evento del mouse. Por lo tanto, la firma del método on_mouse_click() es: def on_mouse_click(self, mouse_event): El objeto de evento del mouse tiene numerosos métodos definidos que permiten que la información sobre el mouse que se obtendrá, como el número de clics del mouse involucrados (GetClickCount()), qué botón se presionó (GetButton()) y el posición actual del mouse dentro del widget o ventana que lo contiene (GetPosition ()). Por lo tanto, podemos usar este último método para obtener la ubicación actual del mouse y luego use el método SetPosition(x, y) en el objeto StaticText para establecer su posición. El resultado final es el programa que se muestra a continuación: 90 9 Eventos en las interfaces de usuario de wxPython
importar wx clase WelcomeFrame(wx.Frame): """ La Ventana Principal / Marco de la aplicación """ def init(uno mismo): super().init(padre=Ninguno, title=‘Aplicación de muestra’, tamaño=(300, 200))
Configure el panel dentro del marco y la etiqueta de texto
self.panel = wx.Panel(self) self.text = wx.StaticText(self.panel, etiqueta=‘Hola’)
Vincular el método on_mouse_click al
Evento de ratón a través del
Carpeta con clic izquierdo del ratón
self.panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_click) def on_mouse_click(self, mouse_event): """ Cuando se hace clic con el botón izquierdo del ratón Este método se llama. obtendrá las coordenadas actuales del ratón, y cambiar la posición de la etiqueta de texto a esta posición. """ x, y = evento_ratón.GetPosition() imprimir (x, y) self.text.SetPosition(wx.Point(x, y)) clase MainApp(wx.App): def OnInit(auto): """ Inicializar la aplicación GUI principal""" marco = Marco de Bienvenida() marco.Mostrar()
Indicar que el procesamiento debe continuar
volver verdadero
Ejecutar la aplicación GUI
aplicación = aplicación principal () aplicación.MainLoop() 9.5 Implementación del manejo de eventos 91
Cuando se ejecuta este programa; la ventana se muestra con el ‘Hola’ etiqueta StaticText en la esquina superior izquierda del marco (en realidad se agrega a el Panel, sin embargo, el Panel llena el Marco en este ejemplo). Si el usuario entonces hace clic con el botón izquierdo del mouse en cualquier lugar dentro del marco y luego salta la etiqueta ‘Hola’ a esa ubicación. Esto se muestra a continuación para la configuración inicial y luego para dos ubicaciones dentro del ventana. 9.6 Una GUI interactiva de wxPython Un ejemplo de una aplicación GUI un poco más grande, que reúne muchas de las ideas presentadas en este capítulo, se da a continuación. En esta aplicación tenemos un campo de entrada de texto (un wx.TextCtrl) que permite una usuario para ingresar su nombre. Cuando hacen clic en el botón Entrar (wx.Button) el la etiqueta de bienvenida (un wx.StaticText) se actualiza con su nombre. El espectáculo El botón “Mensaje” se usa para mostrar un wx.MessageDialog que también contienen su nombre. La pantalla inicial se muestra a continuación para una Mac y una PC con Windows, tenga en cuenta que el color de fondo predeterminado para un marco es diferente en una PC con Windows que en una Mac y, por lo tanto, aunque la GUI se ejecuta en ambas plataformas, el aspecto difiere entre el dos: 92 9 Eventos en las interfaces de usuario de wxPython
El código utilizado para implementar esta aplicación GUI se proporciona a continuación:
importar wx
clase HelloFrame(wx.Frame):
def init(uno mismo, titulo):
super().init(Ninguno, título=título, tamaño=(300, 200))
self.name = ‘
Crear el BoxSizer para usar en el Marco
vertical_box_sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(vertical_box_sizer)
Crea el panel para contener los widgets
panel = wx.Panel(self)
Agregue el panel al dimensionador de marcos
vertical_box_sizer.Añadir(panel, wx.ID_ANY, wx.AMPLIAR | wx.TODO, 20)
Crea el GridSizer para usar con el Panel
rejilla = wx.GridSizer(4, 1, 5, 5)
Configurar el campo de entrada
self.texto = wx.TextCtrl(panel, tamaño=(150, -1)) 9.6 Una GUI interactiva de wxPython 93
Establecer el medidor en el panel
panel.SetSizer(cuadrícula)
Centre el marco en la pantalla de la computadora
auto.Centro() def mostrar_mensaje(auto, evento): Controlador de eventos """ para mostrar el cuadro de diálogo Mensaje utilizando el valor actual del atributo de nombre. """ diálogo = wx.MessageDialog(Ninguno, mensaje = ‘Bienvenido a Python’ + self.name, título = ‘Hola’, estilo=wx.OK) diálogo.MostrarModal() def set_name(self, evento): """ Controlador de eventos para el botón Intro. Recupera el texto ingresado en el campo de entrada y establece el atributo self.name. esto es entonces utilizado para establecer la etiqueta de texto """ self.nombre = self.text.GetLineText(0) self.label.SetLabelText(‘Bienvenido’ + self.nombre)
Ahora configura el botón enter
enter_button = wx.Button(panel, etiqueta=‘Entrar’) enter_button.Bind(wx.EVT_BUTTON, self.set_name)
Luego configure la etiqueta de texto
self.label = wx.StaticText(panel, etiqueta=‘Bienvenido’, estilo=wx.ALIGN_LEFT)
Ahora configure el botón Mostrar mensaje
botón_mensaje = wx.Button(panel, etiqueta=‘Mostrar mensaje’) botón_mensaje.Enlazar(wx.EVT_BUTTON, self.mostrar_mensaje)
Agregue los widgets al medidor de cuadrícula para manejar el diseño
grid.AddMany([self.text, boton_enter, etiqueta propia, botón_mensaje]) 94 9 Eventos en las interfaces de usuario de wxPython
clase MainApp(wx.App): def OnInit(auto): """ Inicializar la pantalla GUI""" frame = HelloFrame(título=‘Aplicación de muestra’) marco.Mostrar()
Indicar si el procesamiento debe continuar o no
volver verdadero def OnExit(auto): """ Se ejecuta cuando la aplicación GUI se cierra""" imprimir(‘Adiós’)
Necesidad de indicar éxito o fracaso
volver verdadero
Ejecutar la aplicación GUI
aplicación = aplicación principal () aplicación.MainLoop() Si el usuario ingresa su nombre en el campo TextCtrl superior, por ejemplo, ‘Phoebe’, entonces cuando hacen clic en el botón ‘Entrar’, la etiqueta de bienvenida cambia a ‘Bienvenido Phoebe’: Si ellos ahora hacer clic en el ‘Espectáculo Mensaje’ botón entonces el wx. MessageDialog (un tipo específico de wx.Dialog) mostrará un mensaje de bienvenida Mensaje para Phoebe: 9.6 Una GUI interactiva de wxPython 95
9.7 Recursos en línea Existen numerosas referencias en línea que respaldan el desarrollo de GUI y de Las GUI de Python en particular, que incluyen: • https://docs.wxpython.org para obtener documentación sobre wxPython. • https://www.wxpython.org página de inicio de wxPython. • https://www.wxwidgets.org Para obtener información sobre los wxWidgets subyacentes Biblioteca GUI multiplataforma. 9.8 Ejercicios 9.8.1 Aplicación de GUI simple Este ejercicio se basa en la GUI que creó en el último capítulo. La aplicación debe permitir que un usuario ingrese su nombre y edad. Necesitaras verifique que el valor ingresado en el campo de edad sea un valor numérico (por ejemplo, usando esnumérico()). Si el valor no es un número, debería aparecer un cuadro de diálogo con un mensaje de error. se mostrará. 96 9 Eventos en las interfaces de usuario de wxPython
Se debe proporcionar un botón con la etiqueta “Cumpleaños”; al hacer clic debería incrementa la edad en uno y muestra un mensaje de feliz cumpleaños. La edad debe ser actualizado dentro de la GUI. A continuación se muestra un ejemplo de la interfaz de usuario que creó en el último capítulo: Como ejemplo, el usuario podría ingresar su nombre y edad como se muestra a continuación: Cuando el usuario hace clic en el botón “cumpleaños”, aparece el mensaje de feliz cumpleaños. se muestra el cuadro de diálogo: 9.8 Ejercicios 97
9.8.2 Interfaz GUI para un juego de tres en raya El objetivo de este ejercicio es implementar un juego simple de Tic Tac Toe. El juego debería permitir que dos usuarios jueguen de forma interactiva con el mismo ratón. El primer usuario se tenga el juego como el jugador ‘X’ y el segundo usuario como el jugador ‘0’. Cuando cada usuario selecciona un botón, puede establecer la etiqueta del botón para su símbolo. Necesitará dos controles después de cada movimiento para ver si alguien ha ganado (o si el el juego es un empate). Todavía necesitará una representación interna de la cuadrícula para que pueda determinar el mío que, si alguno, ha ganado. A continuación se muestra un ejemplo de cómo podría verse la GUI para el juego TicTacToe: También puede agregar diálogos para obtener los nombres de los jugadores y notificarles quién ganó o si hubo un empate. 98 9 Eventos en las interfaces de usuario de wxPython
Capítulo 10 Aplicación de ejemplo de PyDraw wxPython 10.1 Introducción Este capítulo se basa en la biblioteca GUI presentada en los últimos dos capítulos para ilustrar cómo se puede construir una aplicación más grande. Presenta un estudio de caso de una herramienta de dibujo similar a una herramienta como Visio, etc. 10.2 La aplicación PyDraw La aplicación PyDraw permite al usuario dibujar diagramas usando cuadrados, círculos, líneas y texto Actualmente no hay ninguna opción disponible para seleccionar, cambiar el tamaño, reposicionar o eliminar (aunque estos podrían agregarse si es necesario). PyDraw se implementa usando el conjunto de componentes wxPython como se define en la versión 4.0.6. Cuando un usuario inicia la aplicación PyDraw, ve la interfaz que se muestra arriba (para los sistemas operativos Microsoft Windows y Apple Mac). Dependiendo de © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_10 99
el sistema operativo tiene una barra de menú en la parte superior (en una Mac, esta barra de menú está en la parte superior de la pantalla Mac), una barra de herramientas debajo de la barra de menú y un dibujo desplazable área debajo de eso. El primer botón de la barra de herramientas borra el área de dibujo. La segunda y tercera los botones solo se implementan para que impriman un mensaje en Python consola, pero están destinados a permitir que un usuario cargue y guarde dibujos. Los botones de la barra de herramientas se duplican en los menús definidos para la aplicación, junto con un menú de selección de herramientas de dibujo, como se muestra a continuación: 10.3 La estructura de la aplicación La interfaz de usuario creada para la aplicación PyDraw se compone de una serie de elementos (ver más abajo): el PyDrawMenuBar, el PyDrawToolbar que contiene un secuencia de botones en la parte superior de la ventana, el panel de dibujo y el marco de ventana (implementado por la clase PyDrawFrame). El siguiente diagrama muestra la misma información que la presentada anteriormente, pero como una jerarquía de contención, esto significa que el diagrama ilustra cómo un objeto es contenida dentro de otra. Los objetos de nivel inferior están contenidos dentro de los de nivel superior. objetos de nivel. 100 10 Aplicación de ejemplo de PyDraw wxPython
Es importante visualizar esto ya que la mayoría de las interfaces de wxPython están construidas de esta forma, utilizando recipientes y calibradores. La estructura de herencia entre las clases utilizadas en la aplicación PyDraw es ilustrado a continuación. Esta jerarquía de clases es típica de una aplicación que incorpora clasifica las características de la interfaz de usuario con elementos gráficos. 10.3.1 Arquitectura de modelo, vista y controlador La aplicación adopta el bien establecido Model-View-Controller (o MVC) patrón de diseño para separar las responsabilidades entre el elemento de vista (p. el marco o el panel), el elemento de control (para manejar la entrada del usuario) y el modelo elemento (que contiene los datos que se mostrarán). Esta separación de preocupaciones no es una idea nueva y permite la construcción de GUI aplicaciones que reflejan la arquitectura Modelo-Vista-Controlador. la intención de la arquitectura MVC es la separación de la pantalla del usuario, del control del usuario entrada, del modelo de información subyacente como se ilustra a continuación. 10.3 La estructura de la aplicación 101
Hay varias razones por las que esta separación es útil: • reutilización de la aplicación y/o los componentes de la interfaz de usuario, • capacidad para desarrollar la aplicación y la interfaz de usuario por separado, • capacidad de heredar de diferentes partes de la jerarquía de clases. • capacidad para definir clases de estilo de control que proporcionan características comunes por separado de cómo se pueden mostrar estas características. Esto significa que se pueden utilizar diferentes interfaces con la misma aplicación, sin que la aplicación lo sepa. También significa que cualquier parte del sistema se puede cambiar sin afectar el funcionamiento del otro. Por ejemplo, la manera que la interfaz gráfica (el aspecto) muestra la información podría cambiarse sin modificar la aplicación real o cómo se maneja la entrada (la sensación). En efecto la aplicación no necesita saber qué tipo de interfaz está conectada actualmente a ella en todo. 10.3.2 Arquitectura PyDraw MVC La estructura MVC de la aplicación PyDraw tiene una clase de controlador de nivel superior PyDrawController y una clase de vista de nivel superior PyDrawFrame (no hay modelo como la tríada MVC de nivel superior no contiene ningún dato explícito en sí mismo). Esto es mostrado a continuación: En el siguiente nivel hay otra estructura MVC; esta vez por el dibujo elemento de el solicitud. Allá es a controlador de dibujo, con a DrawingModel y un DrawingPanel (la vista) como se ilustra a continuación: 102 10 Aplicación de ejemplo de PyDraw wxPython
Las clases DrawingModel, DrawingPanel y DrawingController exhibir el clásico MVC estructura. El vista y el controlador clases (DrawingPanel y DrawingController) se conocen entre sí y el modelo de dibujo, mientras que DrawingModel no sabe nada sobre la vista o el controlador. La vista recibe una notificación de los cambios en el dibujo a través del evento de pintura. 10.3.3 Clases Adicionales También hay cuatro tipos de objetos de dibujo (de Figura): Círculo, Línea, Cuadrado y Cifras de texto. La única diferencia entre estas clases es lo que se dibuja en el contexto del dispositivo gráfico dentro del método on_paint(). La clase de figura, de la que todos heredan, define los atributos comunes utilizados por todos los objetos dentro un dibujo (por ejemplo, un punto que representa una ubicación y tamaño de x e y). El PyDrawFrame clase también usos a PyDrawMenuBar y a Clase PyDrawToolBar. El primero de ellos extiende wx.MenuBar con menu artículos para usar dentro de la aplicación PyDraw. A su vez, PyDrawToolBar se extiende wx.ToolBar y proporciona íconos para usar en PyDraw. 10.3 La estructura de la aplicación 103
La clase final es la clase PyDrawApp que amplía la clase wx.App. 10.3.4 Relaciones de objetos Sin embargo, la jerarquía de herencia es solo una parte de la historia para cualquier sistema orientado a objetos. solicitud. La siguiente figura ilustra cómo los objetos se relacionan entre sí. dentro de la aplicación de trabajo. El PyDrawFrame es responsable de configurar el controlador y el DibujoPanel. El PyDrawController es responsable de manejar el menú y la barra de herramientas del usuario. interacciones. Esto separa los elementos gráficos del comportamiento desencadenado por el usuario. 104 10 Aplicación de ejemplo de PyDraw wxPython
El DrawingPanel es el archivo responsable de mostrar cualquier figura sostenida por el DibujoModelo. El DrawingController gestiona todas las interacciones del usuario con DrawingPanel, incluida la adición de figuras y la eliminación de todas las figuras del modelo. El modelo de dibujo contiene una lista de figuras que se mostrarán. 10.4 Las interacciones entre objetos Ahora hemos examinado la estructura física de la aplicación, pero no cómo la los objetos dentro de esa aplicación interactúan. En muchas situaciones esto se puede extraer del código fuente de la aplicación (con diferentes grados de dificultad). Sin embargo, en el caso de una aplicación como PyDraw, que se compone de una serie de diferentes componentes que interactúan, es útil para describir las interacciones del sistema explícitamente. Los diagramas que ilustran las interacciones entre los objetos utilizan el siguiente convenciones: • una flecha continua indica un mensaje enviado, • un cuadro cuadrado indica una clase, • un nombre entre paréntesis indica el tipo de instancia, • los números indican la secuencia de envío de mensajes. Estos diagramas se basan en los diagramas de colaboración que se encuentran en el UML (Lenguaje de modelado unificado) notación. 10.4.1 La aplicación PyDraw Cuando se crea una instancia de PyDrawApp, PyDrawFrame se crea y se muestra utilizando el método OnInit(). Luego se invoca el método MainLoop(). Esto es mostrado a continuación: def OnInit(auto): """ Inicializar la pantalla GUI""" marco = PyDrawFrame (título = ‘PyDraw’) marco.Mostrar() volver verdadero
Ejecutar la aplicación GUI
aplicación = PyDrawApp() aplicación.MainLoop() clase PyDrawApp(wx.App): 10.3 La estructura de la aplicación 105
10.4.2 El constructor PyDrawFrame El método constructor PyDrawFrame configura la pantalla principal de la interfaz de usuario aplicación y también inicializa los controladores y elementos de dibujo. Esto es mostrado a continuación usando un diagrama de colaboración: El constructor PyDrawFrame configura el entorno para la aplicación. Él crea el PyDrawController de nivel superior. Crea el DrawingPanel y inicializa el diseño de la pantalla. Inicializa la barra de menús y la barra de herramientas. Se une a la controlador de menú de controladores a los menús y se centra en sí mismo. 10.4.3 Cambiar el modo de aplicación Una cosa interesante a tener en cuenta es lo que sucede cuando el usuario selecciona una opción de el menú Dibujo. Esto permite cambiar el modo a un cuadrado, círculo, línea o texto. Las interacciones involucradas se muestran a continuación para la situación en la que un usuario selecciona el elemento de menú ‘Círculo’ en el menú Dibujo (usando una colaboración diagrama): 106 10 Aplicación de ejemplo de PyDraw wxPython
Cuando el usuario selecciona uno de los elementos del menú, command_menu_handler () se invoca el método PyDrawController. Este método determina qué elemento del menú se ha seleccionado; luego llama a un método setter apropiado (como como set_circle_mode() o set_line_mode() etc.). Estos métodos establecen la atributo de modo del controlador a un valor apropiado. 10.4.4 Adición de un objeto gráfico Un usuario agrega un objeto gráfico al dibujo que muestra el Panel de dibujo al presionando el botón del ratón. Cuando el usuario hace clic en el panel de dibujo, el DrawingController responde como se muestra a continuación: 10.4 Las interacciones entre objetos 107
Lo anterior ilustra lo que sucede cuando el usuario presiona y suelta el mouse sobre el panel de dibujo, para crear una nueva figura. Cuando el usuario presiona el botón del mouse, se envía un mensaje de clic del mouse al DrawingController, que decide qué acción realizar en respuesta (ver arriba). En PyDraw, obtiene el punto del cursor en el que se generó el evento por llamando al método GetPosition() en el mouse_event. Luego, el controlador llama a su propio método add() pasando al modo actual y la ubicación actual del ratón. El controlador obtiene el modo actual (del PyDrawController usando el método llamar de vuelta proporcionó cuando el DrawingController es instanciado) y agrega el tipo apropiado de figura a el modelo de dibujo. El método add() luego agrega una nueva figura al modelo de dibujo basado en el modo especificado. 10.5 Las clases Esta sección presenta las clases en la aplicación PyDraw. A medida que estas clases construyen sobre conceptos ya presentados en los últimos capítulos, se presentarán en en su totalidad con comentarios que destacan puntos específicos de sus implementaciones. Tenga en cuenta que el código importa el módulo wx de la biblioteca wxPython, p. importar wx 10.5.1 La clase PyDrawConstants El propósito de esta clase es proporcionar un conjunto de constantes a las que se pueda hacer referencia en el resto de la solicitud. Se utiliza para proporcionar constantes para los ID utilizados. con elementos de menú y herramientas de la barra de herramientas. También proporciona constantes utilizadas para representar el modo actual (para indicar si se debe agregar una línea, un cuadrado, un círculo o una prueba al mostrar). clase PyDrawConstants: ID_LINEA = 100 CUADRADO_ID = 102 CIRCLE_ID = 103 TEXTO_ID = 104 SQUARE_MODE = ‘cuadrado’ LINE_MODE = ’línea’ CIRCLE_MODE = ‘círculo’ TEXT_MODE = ‘Texto’ 108 10 Aplicación de ejemplo de PyDraw wxPython
10.5.2 La clase PyDrawFrame La clase PyDrawFrame proporciona la ventana principal de la aplicación. Tenga en cuenta que debido a la separación de preocupaciones introducida a través de la arquitectura MVC, la vista La clase solo se preocupa por el diseño de los componentes: clase PyDrawFrame(wx.Frame): """ Estructura principal responsable de la diseño de la interfaz de usuario.""" def init(uno mismo, titulo): super().init(Ninguno, titulo=titulo, tamaño=(300, 200))
Configurar el controlador
self.controller = PyDrawController(self)
Configurar el diseño de la interfaz de usuario
self.vertical_box_sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.vertical_box_sizer)
Configurar la barra de menú
self.SetMenuBar(PyDrawMenuBar())
Configurar la barra de herramientas
self.vertical_box_sizer.Add(PyDrawToolBar(self), wx.ID_ANY, wx.AMPLIAR | wx.TODO, )
Configuración del panel de dibujo
self.drawing_panel = DrawingPanel(self, self.controller.get_mode) self.dibujo_controlador = self.dibujo_panel.controlador
Agregue el panel al dimensionador de marcos
self.vertical_box_sizer.Add(self.drawing_panel, wx.ID_ANY, wx.AMPLIAR | wx.TODO)
Configure el manejo de eventos de comando para la barra de menú
y barra de herramientas self.Bind(wx.EVT_MENU, self.controller.command_menu_handler) auto.Centro() 10.5 Las clases 109
10.5.3 La clase PyDrawMenuBar La clase PyDrawMenuBar es una subclase de la clase wx.MenuBar que define el contenido de la barra de menú de la aplicación PyDraw. Lo hace creando dos wx.Menu objetos y agregarlos a la barra de menú. Cada wx.Menu implementa un menú desplegable de la barra de menú. Para agregar elementos de menú individuales, el archivo wx. Se utiliza la clase MenuItem. Estos elementos de menú se adjuntan al menú. los menús se adjuntan a la barra de menú. Tenga en cuenta que cada elemento del menú tiene una identificación que se puede utilizar para identificar el origen de un evento de comando en un controlador de eventos. Este permite que un solo controlador de eventos se ocupe de los eventos generados por varios elementos de menú. clase PyDrawMenuBar(wx.MenuBar): def init(uno mismo): super().init() menúArchivo = wx.Menú() newMenuItem = wx.MenuItem(fileMenu, wx.ID_NUEVO, texto=“Nuevo”, tipo=wx.ITEM_NORMAL) nuevoMenúElemento.SetBitmap(wx.Bitmap(“nuevo.gif”)) fileMenu.Append (nuevo elemento de menú) loadMenuItem = wx.MenuItem(fileMenu, wx.ID_OPEN, texto=“Abrir”, tipo=wx.ITEM_NORMAL) loadMenuItem.SetBitmap(wx.Bitmap(“load.gif”)) fileMenu.Append(loadMenuItem) menúArchivo.AppendSeparator() saveMenuItem = wx.MenuItem(fileMenu, wx.ID_SAVE, texto=“Guardar”, tipo=wx.ITEM_NORMAL) saveMenuItem.SetBitmap(wx.Bitmap(“save.gif”)) fileMenu.Append(saveMenuItem) menúArchivo.AppendSeparator() salir = wx.MenuItem(fileMenu, wx.ID_EXIT, ‘&Salir\tCtrl+Q’) fileMenu.Append (salir) self.Append(fileMenu, ‘&File’) dibujoMenu = wx.Menu() lineMenuItem = wx.MenuItem(drawingMenu, PyDraw_Constants.LINE_ID, text=“Line”, kind=wx.ITEM_NORMAL) dibujoMenu.Append(lineMenuItem) elementomenúcuadrado = wx.MenúElemento(menúdibujo, PyDraw_Constants.SQUARE_ID, text=“Cuadrado”, tipo=wx.ITEM_NORMAL) dibujoMenu.Append(squareMenuItem) circleMenuItem = wx.MenuItem(drawingMenu, 110 10 Aplicación de ejemplo de PyDraw wxPython
PyDraw_Constants.CIRCLE_ID, text=“Circle”, kind=wx.ITEM_NORMAL) dibujoMenu.Append(circleMenuItem) textMenuItem = wx.MenuItem(drawingMenu, PyDraw_Constants.TEXT_ID, text=“Text”, kind=wx.ITEM_NORMAL) dibujoMenu.Append(textMenuItem) self.Append(dibujoMenú, ‘&Dibujo’) 10.5.4 La clase PyDrawToolBar La clase DrawToolBar es una subclase de wx.ToolBar. El constructor de clases inicializa tres herramientas que se muestran en la barra de herramientas. El darse cuenta() se utiliza para garantizar que las herramientas se representen correctamente. Tenga en cuenta que se han utilizado identificadores apropiados para permitir que un controlador de eventos identifique qué herramientas generó un evento de comando en particular. Al reutilizar las mismas identificaciones para el menú relacionado elementos y herramientas de comando, se puede usar un solo controlador para administrar eventos de ambos tipos de fuentes. clase PyDrawToolBar(wx.ToolBar): def init(uno mismo, padre): super().init(padre) self.AddTool(toolId=wx.ID_NUEVO, etiqueta=“Nuevo”, bitmap=wx.Bitmap(“nuevo.gif”), shortHelp=‘Abrir dibujo’, kind=wx.ARTÍCULO_NORMAL) self.AddTool(toolId=wx.ID_OPEN, label=“Abrir”, bitmap=wx.Bitmap(“load.gif”), shortHelp=‘Abrir dibujo’, kind=wx.ARTÍCULO_NORMAL) self.AddTool(toolId=wx.ID_SAVE, label=“Guardar”, bitmap=wx.Bitmap(“save.gif”), shortHelp=‘Guardar dibujo’, kind=wx.ARTÍCULO_NORMAL) self.Darse cuenta() 10.5.5 La clase PyDrawController Esta clase proporciona el elemento de control de la vista de nivel superior. mantiene el modo actual e implementa un controlador que puede manejar eventos de los elementos del menú y desde la barra de herramientas herramientas. Se utiliza una identificación para identificar cada menú o herramienta individual lo que permite registrar un solo controlador con el marco. 10.5 Las clases 111
def init(uno mismo, vista): self.vista = vista
Establecer el modo inicial
self.mode = PyDrawConstants.SQUARE_MODE def set_circle_mode(self): self.mode = PyDrawConstants.CIRCLE_MODE def set_line_mode(self): self.mode = PyDrawConstants.LINE_MODE def set_square_mode(self): self.mode = PyDrawConstants.SQUARE_MODE def set_text_mode(self): self.mode = PyDrawConstants.TEXT_MODE def clear_drawing(auto): self.view.drawing_controller.clear() def get_mode(self): volver self.mode def command_menu_handler(self, command_event): id = comando_evento.GetId() si id == wx.ID_NUEVO: print(‘Borrar el área de dibujo’) self.clear_drawing() elif id == wx.ID_OPEN: print(‘Abrir un archivo de dibujo’) elif id == wx.ID_GUARDAR: print(‘Guardar un archivo de dibujo’) elif id == wx.ID_EXIT: print(‘Toda la aplicación’) self.view.Close() elif id == PyDrawConstants.LINE_ID: imprimir (’establecer el modo de dibujo en línea’) self.set_line_mode() elif id == PyDrawConstants.SQUARE_ID: imprimir (’establecer el modo de dibujo en cuadrado’) self.set_square_mode() elif id == PyDrawConstants.CIRCLE_ID: imprimir (’establecer el modo de dibujo en círculo’) self.set_circle_mode() elif id == PyDrawConstants.TEXT_ID: imprimir (’establecer el modo de dibujo en Texto’) self.set_text_mode() demás: print(‘Opción desconocida’, id) clase PyDrawController: 112 10 Aplicación de ejemplo de PyDraw wxPython
10.5.6 La clase DrawingModel La clase DrawingModel tiene un atributo de contenido que se utiliza para contener todos los figuras en el dibujo. También proporciona algunos métodos convenientes para restablecer el contenido y añadir una figura al contenido. clase DibujoModelo: def init(uno mismo): self.contents = [] def clear_figures(auto): self.contents = [] def add_figure(self, figura): self.contents.append(figura) El DrawingModel es un modelo relativamente simple que simplemente registra un conjunto de figuras gráficas en una Lista. Estos pueden ser cualquier tipo de objeto y se pueden mostrar en de cualquier manera, siempre y cuando implementen el método on_paint(). son los objetos ellos mismos que determinan cómo se ven cuando se dibujan. 10.5.7 La clase DrawingPanel La clase DrawingPanel es una subclase de la clase wx.Panel. Proporciona la vista para el modelo de datos de dibujo. Esto utiliza la arquitectura MVC clásica y tiene un modelo (DrawingModel), una vista (el DrawingPanel) y un controlador (el controlador de dibujo). El DrawingPanel crea una instancia de su propio DrawingController para manejar eventos de ratón. También registra eventos de pintura para saber cuándo actualizar la pantalla. clase PanelDibujo(wx.Panel): def init(self, padre, get_mode): super().init(padre, -1) self.SetBackgroundColour(wx.Colour(255, 255, 255)) self.modelo = DibujoModelo() self.controller = DrawingController(self, self.model, get_mode) self.Bind(wx.EVT_PAINT, self.on_paint) self.Bind(wx.EVT_LEFT_DOWN, self.controller.on_mouse_click) 10.5 Las clases 113
def on_paint(self, evento): “““configurar el contexto del dispositivo (DC) para pintar””” dc = wx.PaintDC(self) para figura en self.model.contents: figura.en_pintura(cc) 10.5.8 La clase DrawingController La clase DrawingController proporciona la clase de control para el nivel superior Arquitectura MVC utilizada con DrawingModel (modelo) y DrawingPanel (ver) clases. En particular, maneja los eventos del mouse en el DrawingPanel a través del método on_mouse_click(). También define un método de adición que se usa para agregar una figura al modelo de dibujo. (La cifra real depende del modo actual del PyDrawController). Un método final, el método clear(), elimina todas las figuras del modelo de dibujo y actualiza la pantalla. controlador de dibujo de clase: def init(self, vista, modelo, get_mode): self.vista = vista self.modelo = modelo self.get_mode = get_mode def on_mouse_click(self, mouse_event): punto = mouse_event.GetPosition() self.add(self.get_mode(), punto) def add(self, modo, punto, tamaño=30): si modo == PyDrawConstants.SQUARE_MODE: fig = Square(self.view, point, wx.Size(size, size)) modo elif == PyDrawConstants.CIRCLE_MODE: fig = Circle(self.view, point, size) modo elif == PyDrawConstants.TEXT_MODE: fig = Text(self.view, point, size) modo elif == PyDrawConstants.LINE_MODE: fig = Line(self.view, point, size) self.model.add_figure(fig) def claro(auto): self.model.clear_figures() self.view.Refresh() 114 10 Aplicación de ejemplo de PyDraw wxPython
10.5.9 La clase de figura La clase Figure (una superclase abstracta de la jerarquía de clases Figure) captura los elementos que son comunes a los objetos gráficos mostrados dentro de un dibujo. El el punto define la posición de la figura, mientras que el atributo de tamaño define el tamaño de la figura. Tenga en cuenta que la figura es una subclase de wx.Panel y, por lo tanto, el La pantalla se construye a partir de paneles interiores en los que se colocan las distintas formas de las figuras. dibujado. La clase Figure define un único método abstracto on_paint(dc) que debe ser implementado por todas las subclases concretas. Este método debe definir cómo la forma se dibuja en el panel de dibujo. Figura de clase (wx.Panel): def init(self, padre, id=wx.ID_ANY, pos=Ninguno, tamaño=Ninguno, estilo=wx.TAB_TRAVERSAL): wx.Panel.init(self, padre, id=id, pos=pos, tamaño=tamaño, estilo=estilo) self.punto = pos self.tamaño = tamaño @metodoabstracto def on_paint(self, dc): Aprobar 10.5.10 La clase cuadrada Esta es una subclase de Figura que especifica cómo dibujar una forma cuadrada en un dibujo. Implementa el método on_paint() heredado de Figure. clase Cuadrado (Figura): def init(self, padre, pos, tamaño): super().init(padre=padre, pos=pos, tamaño=tamaño) def on_paint(self, dc): dc.DrawRectangle(punto propio, tamaño propio) 10.5 Las clases 115
10.5.11 La clase del círculo Esta es otra subclase de Figure. Implementa el método on_paint() por dibujando un círculo. Tenga en cuenta que la forma se dibujará dentro del tamaño del panel definido mediante la clase Figure (usando la llamada a super). Por lo tanto, es necesario ver el círculo para encajar dentro de estos límites. Esto significa que el atributo de tamaño debe usarse para generar un radio apropiado. También tenga en cuenta que el método DrawCircle() del dispositivo El contexto toma un punto que es el centro del círculo, por lo que también se debe calcular. círculo de clase (figura): def init(self, padre, pos, tamaño): super().init(padre=padre, pos=pos, tamaño=wx.Tamaño(tamaño, tamaño)) auto.radio = (tamaño - 10) / 2 self.circle_center = wx.Point(self.point.x + auto.radio, auto.punto.y + auto.radio) def on_paint(self, dc): dc.DrawCircle(pt=self.circle_center, radio=auto.radio) 10.5.12 La clase de línea Esta es otra subclase de Figure. En este ejemplo muy simple, un final predeterminado se genera el punto para la línea. Alternativamente, el programa podría buscar un ratón evento lanzado y levante el mouse en esta ubicación y utilícelo como el punto final de la línea. línea de clase (figura): def init(self, padre, pos, tamaño): super().init(padre=padre, pos=pos, tamaño=wx.Tamaño(tamaño, tamaño)) self.end_point = wx.Point(self.point.x + tamaño, self.point.y + tamaño) def on_paint(self, dc): dc.DrawLine(pt1=self.point, pt2=self.end_point)25.1.4 116 10 Aplicación de ejemplo de PyDraw wxPython
10.5.13 La clase de texto Esta es también una subclase de Figure. Se utiliza un valor predeterminado para que se muestre el texto; sin embargo, se podría presentar un cuadro de diálogo al usuario que le permita ingresar el texto que desea. desea mostrar: Texto de clase (Figura): def init(self, padre, pos, tamaño): super().init(padre=padre, pos=pos, tamaño=wx.Tamaño(tamaño, tamaño)) def on_paint(self, dc): dc.DrawText(text=‘Texto’, pt=self.point) 10.6 Referencias A continuación, se proporcionan algunos antecedentes sobre la arquitectura Modelo-Vista-Controlador. tectura en interfaces de usuario. • G. E. Krasner, ST. Pope, un libro de cocina para usar el usuario del controlador de vista de modelo paradigma de interfaz en smalltalk-80. JOOP 1(3), 26–49 (1988). 10.7 Ejercicios Puede desarrollar aún más la aplicación PyDraw agregando lo siguiente características: • Una opción de eliminación Puede agregar un botón etiquetado como Eliminar a la ventana. Debería establezca el modo en “eliminar”. El panel de dibujo debe modificarse para que el El método mouseReleased envía un mensaje de eliminación al dibujo. El dibujo debe encontrar y eliminar el objeto gráfico apropiado y enviar el cambio mensaje a sí mismo. • Una opción de cambio de tamaño Esto implica identificar cuál de las formas ha sido seleccionada y luego usando un cuadro de diálogo para ingresar el nuevo tamaño o brindando alguna opción que permite indicar el tamaño de la forma con el ratón. 10.5 Las clases 117
Parte II Juegos de computadora
Capítulo 11 Introducción a la programación de juegos 11.1 Introducción La programación de juegos es realizada por desarrolladores/codificadores que implementan la lógica que impulsa un juego. Históricamente, los desarrolladores de juegos hicieron todo; escribieron el código, diseñaron el sprites e íconos, manejó el juego, lidió con sonidos y música, generó cualquier animación requerida, etc. Sin embargo, como la industria de los juegos ha madurado juegos compañías tener desarrollado específico roles incluido Computadora Gráficos (CG) animadores, artistas, desarrolladores de juegos y motor de juegos y motor de física desarrolladores etc Aquellos involucrados en el desarrollo de código pueden desarrollar un motor de física, un juego motor, los juegos en sí, etc. Dichos desarrolladores se centran en diferentes aspectos de un juego. Por ejemplo, un desarrollador de motores de juegos se enfoca en crear el marco dentro del cual se ejecutará el juego. A su vez, un desarrollador de motores de física se centrará en implementar las matemáticas detrás de la física del mundo de los juegos simulados (como el efecto de la gravedad en los personajes y componentes dentro de ese mundo). En En muchos casos, también habrá desarrolladores trabajando en el motor de IA para un juego. Estos desarrolladores se centrarán en proporcionar instalaciones que permitan que el juego o los personajes en el juego para operar inteligentemente. Quienes desarrollen el juego real utilizarán estos motores y marcos para crear el resultado final general. Son ellos quienes dan vida al juego y lo convierten en un experiencia agradable (y jugable). 11.2 Marcos de juegos y bibliotecas Hay muchos marcos y bibliotecas disponibles que le permiten crear cualquier cosa desde juegos simples hasta grandes juegos de rol complejos con mundos infinitos. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_11 121
Un ejemplo es el marco Unity que se puede usar con la programación C# idioma. Otro framework de este tipo es el motor Unreal usado con el programa C++. lenguaje de gramática. Python también se ha utilizado para el desarrollo de juegos con varios conocidos títulos de juegos dependiendo de ello de una forma u otra. Por ejemplo, Battlefield 2 de Digital Illusions CE es un simulador militar de disparos en primera persona. campo de batalla Heroes maneja partes de la lógica del juego que involucran modos de juego y puntuación usando Pitón. Otros juegos que usan Python incluyen Civilization IV (para muchas de las tareas), Pirates of the Caribbean Online y Overwatch (que toma sus decisiones con Pitón). Python también está integrado como motor de secuencias de comandos en herramientas como Autodesk. Maya, que es un conjunto de herramientas de animación por computadora que a menudo se usa con juegos. 11.3 Desarrollo de juegos Python Para aquellos que quieran aprender más sobre el desarrollo de juegos; Python tiene mucho que oferta. Hay muchos ejemplos disponibles en línea, así como varios juegos orientados marcos Los marcos/bibliotecas disponibles para el desarrollo de juegos en Python incluyen: • Arcada. Esta es una biblioteca de Python para crear videojuegos de estilo 2D. • pyglet es una biblioteca multimedia y de ventanas para Python que también se puede utilizar para el desarrollo de juegos. • Cocos2d es un marco para crear juegos en 2D construido sobre pyglet. • pygame es probablemente la biblioteca más utilizada para crear juegos dentro de la Mundo pitón. También hay muchas extensiones disponibles para pygame que ayudan a crear una amplia gama de diferentes tipos de juegos. Nos centraremos en pygame en los próximos dos capítulos de este libro. Otras bibliotecas de interés para los desarrolladores de juegos de Python incluyen: • PyODE. Este es un enlace Python de código abierto para Open Dynamics Engine que es un motor de física de código abierto. • pymunk Pymunk es una biblioteca de física 2D fácil de usar que se puede usar siempre que necesitas física de cuerpo rígido 2d con Python. Es muy bueno cuando necesitas 2D. física en su juego, demostración u otra aplicación. Está construido sobre el 2D. biblioteca de física Chipmunk. • pyBox2D pybox2d es una biblioteca de física 2D para tus juegos y simulación simple. laciones Se basa en la biblioteca Box2D escrita en C++. Admite varias formas 122 11 Introducción a la programación de juegos
tipos (círculo, polígono, segmentos de línea fina), así como una serie de tipos de juntas (revoluta, prismática, rueda, etc.). • Licuadora. Este es un conjunto de herramientas de software de gráficos por computadora en 3D de código abierto que se utiliza para creación de películas animadas, efectos visuales, arte, modelos impresos en 3D, 3D interactivo aplicaciones y videojuegos. Las características de Blender incluyen modelado 3D, texturización turing, edición de gráficos de trama, rigging y skinning, etc. Python se puede utilizar como herramienta de secuencias de comandos para creación, creación de prototipos, lógica de juegos y más. • Quake Army Knife, que es un entorno para desarrollar mapas en 3D para Juegos basados en el motor Quake. Está escrito en Delphi y Python. 11.4 Usando Pygame En los próximos dos capítulos exploraremos la biblioteca principal de pygame y cómo puede ser Se utiliza para desarrollar juegos de computadora interactivos. El próximo capítulo explora Pygame en sí mismo y las instalaciones que ofrece. El siguiente capítulo desarrolla un sencillo juego interactivo en el que el usuario mueve una nave estelar esquivando meteoritos que desplazarse verticalmente hacia abajo en la pantalla. 11.5 Recursos en línea Para más información sobre la programación de juegos y las bibliotecas mencionadas en este capítulo ver: • https://unity.com/ el marco C# para el desarrollo de juegos. • https://www.unrealengine.com para el desarrollo de juegos en C++. • http://arcade.academy/ proporciona detalles sobre el marco de los juegos Arcade. • http://www.pyglet.org/ para obtener información sobre la biblioteca de lechones. • http://cocos2d.org/ es la página de inicio del marco Cocos2d. • https://www.pygame.org para obtener información sobre pygame. • http://pyode.sourceforge.net/ para obtener detalles de los enlaces de PyODE a Open Motor Dinámico. • http://www.pymunk.org/ proporciona información sobre pymunk. • https://github.com/pybox2d/pybox2d cual es a Git centro repositorio para pyBox2d. • https://git.blender.org/gitweb/gitweb.cgi/blender.git Git Centro repositorio para Licuadora. • https://sourceforge.net/p/quark/code Repositorio de SourceForge para Quake Army Cuchillo. • https://www.autodesk.co.uk/products/maya/overview para información en Software de animación por computadora Autodesks Maya. 11.3 Desarrollo de juegos Python 123
Capítulo 12 Juegos de construcción con pygame 12.1 Introducción pygame es una biblioteca de Python multiplataforma, gratuita y de código abierto diseñada para hacer la construcción de aplicaciones multimedia como juegos fácil. desarrollo de pygame comenzó en octubre de 2000 con el lanzamiento de la versión 1.0 de pygame seis meses más tarde. La versión de pygame discutida en este capítulo es la versión 1.9.6. Si tienes un verifique la versión posterior para ver qué cambios se han realizado para ver si tienen algún impacto en los ejemplos presentados aquí. pygame está construido sobre la biblioteca SDL. SDL (o Simple Directmedia Layer) es una biblioteca de desarrollo multiplataforma diseñada para proporcionar acceso a audio, teclado tableros, mouse, joystick y hardware de gráficos a través de OpenGL y Direct3D. A promover la portabilidad, pygame también es compatible con una variedad de backends adicionales incluyendo WinDIB, X11, Linux Frame Buffer, etc. SDL es oficialmente compatible con Windows, Mac OS X, Linux, iOS y Android (aunque otras plataformas no son oficialmente compatibles). SDL mismo está escrito en C y pygame proporciona un contenedor alrededor de SDL. Sin embargo, pygame agrega funcionalidad no que se encuentra en SDL para facilitar la creación de gráficos o videojuegos. Estos las funciones incluyen matemáticas vectoriales, detección de colisiones, gráfico de escena de sprites 2D gestión, soporte MIDI, cámara, manipulación de matriz de píxeles, transformaciones, filtrado, soporte avanzado de fuentes de tipo libre y dibujo. El resto de este capítulo presenta pygame, los conceptos clave; la clave módulos, clases y funciones y una primera aplicación pygame muy simple. El siguiente El capítulo muestra el desarrollo de un videojuego de estilo arcade simple que ilustra cómo se puede crear un juego usando pygame. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_12 125
12.2 La superficie de visualización La superficie de la pantalla (también conocida como la pantalla) es la parte más importante de un juego de pygame. Es la pantalla de la ventana principal de su juego y puede ser de cualquier tamaño, sin importar cómo lo desee. solo puede tener una superficie de visualización. En muchos sentidos, la superficie de visualización es como una hoja de papel en blanco en la que poder dibujar. La superficie en sí está formada por píxeles que están numerados de 0,0 en la esquina superior izquierda con las ubicaciones de píxeles indexadas en el eje x y el eje y. Esto se muestra a continuación: El diagrama anterior ilustra cómo se indexan los píxeles dentro de una superficie. De hecho un La superficie se puede utilizar para dibujar líneas, formas (como rectángulos, cuadrados, círculos y elipses), mostrar imágenes, manipular píxeles individuales, etc. Las líneas se dibujan desde una ubicación de píxel a otra (por ejemplo, desde la ubicación 0,0 a la ubicación 9,0 que dibujaría una línea en la parte superior de la superficie de visualización anterior). Las imágenes pueden ser mostrado dentro de la superficie de visualización dado un punto de partida como 1, 1. La superficie de visualización es creada por pygame.display.set_mode() función. Esta función toma una tupla que se puede usar para especificar el tamaño del Mostrar Superficie a devolver. Por ejemplo: superficie_de_pantalla = pygame.display.set_mode((400, 300)) Esto creará una superficie de visualización (ventana) de 400 por 300 píxeles. Una vez que tenga la superficie de visualización, puede llenarla con un respaldo adecuado. color de fondo (el valor predeterminado es negro), sin embargo, si desea un fondo diferente colorear o querer borrar todo lo que se ha dibujado previamente en la superficie, entonces puedes usar el método fill() de la superficie: BLANCO = (255, 255, 255) display_surface.fill(BLANCO) El método de relleno toma una tupla que se usa para definir un color en términos de rojo, Colores verde y azul (o RGB). Aunque los ejemplos anteriores usan un significado nombre de la tupla que representa los valores RGB utilizados para el blanco; por supuesto que no hay requisito para hacer esto (aunque se considera una buena práctica). 126 12 Juegos de construcción con pygame
Para ayudar en el rendimiento, cualquier cambio que realice en la superficie de visualización realmente suceder en segundo plano y no se representará en la pantalla real que el el usuario ve hasta que llama a los métodos update() o flip() en la superficie. Para ejemplo: • pygame.display.update() • pygame.display.flip() El método update() volverá a dibujar la pantalla con todos los cambios realizados en el mostrar en el fondo. Tiene un parámetro opcional que le permite especificar solo una región de la pantalla para actualizar (esto se define usando un Rect que representa un área rectangular en la pantalla). El método flip() siempre refresca todo el la pantalla (y como tal hace exactamente lo mismo que el método update() sin parámetros). Otro método, que no es específicamente un método de superficie de visualización, pero que se utiliza a menudo cuando se crea la superficie de visualización, proporciona una leyenda o título para la ventana de nivel superior. Esta es la función pygame.display.set_caption(). Por ejemplo: pygame.display.set_caption(‘Hola mundo’) Esto le dará a la ventana de nivel superior el título (o título) “Hola mundo”. 12.3 Eventos Así como los sistemas de interfaz gráfica de usuario descritos en capítulos anteriores tienen un bucle de eventos que permite al programador averiguar qué está haciendo el usuario (en esos casos, esto suele ser seleccionar un elemento de menú, hacer clic en un botón o ingresar datos, etc.); pygame tiene un bucle de eventos que permite que el juego determine qué jugador es haciendo. Por ejemplo, el usuario puede presionar la tecla de flecha izquierda o derecha. Esto es repre- enviado por un evento. 12.3.1 Tipos de eventos Cada evento que ocurre tiene información asociada como el tipo de ese evento. Para ejemplo: • Presionar una tecla resultará en un tipo de evento KEYDOWN, mientras que soltar una tecla dará como resultado un tipo de evento KEYUP. • Seleccionar el botón de cerrar ventana generará un tipo de evento SALIR, etc. • Usando el ratón poder generar MOVIMIENTO DEL RATÓN eventos como Bueno como Tipos de evento MOUSEBUTTONDOWN y MOUSEBUTTONUP. 12.2 La superficie de visualización 127
• El uso de un Joystick puede generar varios tipos diferentes de eventos, incluidos JOYAXISMOMOTION, MOVIMIENTO DE JOYBALL, BOTÓN DE JOY y JOYBU TTONUP. Estos tipos de eventos le indican qué ocurrió para generar el evento. Esto significa que puede elegir con qué tipos de eventos quiere tratar e ignorar otros eventos. 12.3.2 Información del Evento Cada tipo de objeto de evento proporciona información asociada con ese evento. Para ejemplo, un objeto de evento orientado a clave proporcionará la tecla real presionada mientras El objeto de evento orientado al mouse proporcionará información sobre la posición del mouse, qué botón se presionó, etc. Si intenta acceder a un atributo en un evento que no no admite ese atributo, se generará un error. A continuación se enumeran algunos de los atributos disponibles para diferentes tipos de eventos: • KEYDOWN y KEYUP, el evento tiene un atributo clave y un atributo mod (indicando si también se están presionando otras teclas de modificación, como Shift). • MOUSEBUTTONUP y MOUSEBUTTONDOWN tienen un atributo pos que contiene un tupla que indica la ubicación del mouse en términos de coordenadas x e y en el superficie subyacente. También tiene un atributo de botón que indica qué mouse fue presionado. • MOUSEMOTION tiene atributos pos, rel y botones. La pos es una tupla indi- indicando la ubicación x e y del cursor del ratón. El atributo real indica el cantidad de movimiento del mouse y botones indica el estado del mouse botones. Como ejemplo, si queremos verificar un tipo de evento de teclado y luego verificar que la tecla presionada fue la barra espaciadora, entonces podemos escribir: if event.type == pygame.KEYDOWN:
Comprobar para ver qué tecla se presiona
si evento.clave == pygame.K_SPACE: imprimir(’espacio’) Esto indica que si se trata de un evento de pulsación de tecla y que la tecla real era la barra espaciadora; luego imprima la cadena ’espacio’. Hay muchas constantes de teclado que se utilizan para representar las teclas en el keyboard y la constante pygame.K_SPACE utilizada anteriormente es solo una de ellas. Todas las constantes del teclado tienen el prefijo ‘K_’ seguido de la tecla o el nombre de la clave, por ejemplo: 128 12 Juegos de construcción con pygame
• K_TAB, K_ESPACIO, K_PLUS, K_0, K_1, K_AT, K_a, K_b, K_z, K_DELTE, K_ABAJO, K_IZQUIERDA, K_DERECHA, K_IZQUIERDA, etc. Se proporcionan más constantes de teclado para estados modificadores que se pueden combinar con el arriba semejante como KMOD_SHIFT, KMOD_CAPS, KMOD_CTRL y KMOD_ALT. 12.3.3 La cola de eventos Los eventos se suministran a una aplicación pygame a través de la cola de eventos. La cola de eventos se utiliza para recopilar eventos a medida que ocurren. Por ejemplo, Supongamos que un usuario hace clic dos veces con el ratón y dos veces con una tecla antes de el programa tiene la oportunidad de procesarlos; entonces habrá cuatro eventos en el Evento Cola como se muestra a continuación: Luego, la aplicación puede obtener un iterable de la cola de eventos y procesar a través de los eventos a su vez. Mientras el programa sigue procesando estos eventos pueden ocurrir eventos y se agregarán a la cola de eventos. Cuando el programa tiene terminado de procesar la colección inicial de eventos, puede obtener el siguiente conjunto de eventos para procesar. Una ventaja significativa de este enfoque es que nunca se pierde ningún evento; eso es si el usuario hace clic con el mouse dos veces mientras el programa está procesando un conjunto anterior de eventos; se grabarán y agregarán a la cola de eventos. Otra ventaja es que los eventos se presentarán al programa en el orden en que ocurrieron. La función pygame.event.get() leerá todos los eventos actualmente en el Cola de eventos (eliminándolos de la cola de eventos). El método devuelve un EventList, que es una lista iterable de los eventos leídos. Cada evento puede entonces ser procesada a su vez. Por ejemplo: para evento en pygame.event.get(): if event.type == pygame.QUIT: print(‘Evento de salida recibido:’) elif event.type == pygame.MOUSEBUTTONDOWN: print(‘Evento de ratón recibido’) elif event.type == pygame.KEYDOWN: print(‘Evento KeyDown recibido’) 12.3 Eventos 129
En el fragmento de código anterior, se obtiene una lista de eventos de la cola de eventos. que contiene el conjunto actual de eventos. El bucle for luego procesa cada evento a su vez comprobando el tipo e imprimiendo un mensaje apropiado. Puede utilizar este enfoque para desencadenar un comportamiento adecuado, como mover un imagen alrededor de la pantalla o calcular la puntuación de los jugadores, etc. Sin embargo, tenga en cuenta que si este comportamiento toma demasiado tiempo puede hacer que el juego sea difícil de jugar (aunque los ejemplos en este capítulo y el siguiente son lo suficientemente simples como para que este no sea un problema). 12.4 Una primera aplicación de pygame Ahora estamos en el punto donde podemos juntar lo que hemos visto hasta ahora. y cree una aplicación pygame simple. Es común crear un programa de estilo hola mundo cuando se utiliza un nuevo programa. lenguaje de programación o usando un nuevo marco de aplicación, etc. La intención es que se exploran los elementos centrales del lenguaje o marco para generar la forma más básica de una aplicación usando el lenguaje o framework. Lo haremos por lo tanto, implemente la aplicación más básica posible usando pygame. La aplicación que crearemos mostrará una ventana de pygame, con un ‘Hola título mundial. Entonces podremos salir del juego. Aunque técnicamente hablando esto no es un juego, posee la arquitectura básica de una aplicación pygame. El sencillo juego HelloWorld inicializará pygame y la pantalla gráfica jugar. Luego tendrá un ciclo de juego principal que continuará hasta que el usuario selecciona salir de la aplicación. Luego cerrará pygame. La pantalla creada por el programa se muestra a continuación para los sistemas operativos Mac y Windows: Para salir del programa, haga clic en el botón de salida del sistema de ventanas que está utilizando. usando. 130 12 Juegos de construcción con pygame
El sencillo juego HelloWorld se muestra a continuación: importar pygame def principal(): imprimir(‘Partida Inicial’) print(‘Inicializando pygame’) pygame.init() # Requerido por cada aplicación pygame print(‘Inicializando HelloWorldGame’) pygame.display.set_mode((200, 100)) pygame.display.set_caption(‘Hola mundo’) print(‘Actualizar pantalla’) pygame.display.update() print(‘Comenzando el bucle de reproducción del juego principal’) corriendo = Verdadero mientras corre: para evento en pygame.event.get(): if event.type == pygame.QUIT: print(‘Evento de salida recibido:’, evento) corriendo = Falso imprimir(‘Se acabó el juego’) pygame.quit() si nombre == ‘principal’: principal() Hay varios pasos clave resaltados en este ejemplo, estos pasos son:
- Importar pygame. pygame, por supuesto, no es uno de los módulos predeterminados disponibles dentro de Python. Primero debe importar pygame en su código. la importación declaración pygame importa el módulo pygame en su código y hace que el funciones y clases en pygame disponibles para usted (tenga en cuenta las mayúsculas - pygame no es el mismo nombre de módulo que PyGame). También es común encontrar que programas importan • desde la importación de pygame.locals * • Esto agrega varias constantes y funciones al espacio de nombres de su pro- gramo. En este ejemplo muy simple no hemos necesitado hacer esto.
- Inicializar pygame. Casi todos los módulos de pygame deben inicializarse en algún y la forma más sencilla de hacerlo es llamar a pygame.init(). Esto lo hara lo que se requiere para configurar el entorno pygame para su uso. Si te olvidas de llamar esta función normalmente obtendrá un mensaje de error como pygame.error: sistema de video no inicializado (o algo similar). Si obtienes tal 12.4 Una primera aplicación de pygame 131
verifique que haya llamado a pygame.init(). Tenga en cuenta que puede inicializar módulos individuales de pygame (por ejemplo, el módulo pygame.font se puede inicializar usando pygame.font.init()) si es necesario. Sin embargo pygame.init() es el enfoque más utilizado para configurar pygame. 3. Configure la pantalla. Una vez que haya inicializado el marco de pygame, puede configurar la pantalla. En el ejemplo de código anterior, la pantalla se configura usando el función pygame.display.set_mode(). Esta función toma una tupla especificando el tamaño de la ventana a crear (en este caso 200 píxeles de ancho por 100 píxeles de alto). Tenga en cuenta que si intenta invocar esta función pasando dos parámetros en lugar de una tupla, obtendrá un error. Esta función devuelve la superficie de dibujo o pantalla/ventana que se puede usar para mostrar elementos dentro el juego como íconos, mensajes, formas, etc. Como nuestro ejemplo es tan simple, no se moleste en guardarlo en una variable. Sin embargo, nada más complejo que esto tendrá que hacerlo. También establecemos el título (o título) de la ventana/marco. Esto es que se muestra en la barra de título de la ventana. 4. Renderice la pantalla. Ahora llamamos a la función pygame.display.update() ción Esta función hace que se dibujen los detalles actuales de la pantalla. En el momento esta es una ventana en blanco. Sin embargo, es común en los juegos realizar una serie de actualizaciones a la pantalla en segundo plano y luego cuando el programa es listo para actualizar la pantalla para llamar a esta función. Esto agrupa una serie de actualizaciones. y hace que la pantalla se actualice. En una pantalla compleja es posible indicar qué partes de la pantalla se deben volver a dibujar en lugar de volver a dibujar la ventana entera. Esto se hace pasando un parámetro a la actualización () para indicar el rectángulo a redibujar. Sin embargo, nuestro ejemplo es tan simple, estamos de acuerdo con volver a dibujar toda la ventana y, por lo tanto, no necesita pasar cualquier parámetro a la función. 5. Bucle de juego principal. Es común tener un bucle de juego principal que impulsa el procesamiento de las entradas del usuario, modifica el estado del juego y actualiza la pantalla. Esto está representado arriba por el bucle while running:. El local la ejecución variable se inicializa a True. Esto significa que el ciclo while asegura que el juego continúa hasta que el usuario selecciona salir del juego en el que point, la variable en ejecución se establece en False, lo que hace que el ciclo se cierre. En en muchos casos, este bucle llamará a update() para actualizar la pantalla. Lo anterior El ejemplo no hace esto ya que nada cambia en la pantalla. Sin embargo, el El ejemplo desarrollado más adelante en este capítulo ilustrará esta idea. 6. Supervise los eventos que impulsan el juego. Como se mencionó anteriormente, la cola de eventos se utiliza para permitir que las entradas del usuario se pongan en cola y luego sean procesadas por el juego. En el El ejemplo simple que se muestra arriba está representado por un bucle for que recibe eventos usando pygame.event.get() y luego verificando si el evento es un evento pygame.QUIT. Si es así, establece el indicador de ejecución en Falso. Cual hará que el ciclo while principal del juego termine. 7. Salga de pygame una vez que termine. En pygame, cualquier módulo que tenga un init() La función también tiene una función quit() equivalente que se puede usar para realizar cualquier operación de limpieza. Como llamamos a init() en el módulo pygame en el 132 12 Juegos de construcción con pygame
inicio de nuestro programa, por lo tanto, necesitaremos llamar a pygame.quit() al final del programa para asegurar que todo esté ordenado apropiadamente. El resultado generado a partir de una ejecución de muestra de este programa se proporciona a continuación: pygame 1.9.6 Hola desde la comunidad de pygame. https://www.pygame.org/contribute.html Empieza el juego Inicializando pygame Inicializar HelloWorldGame Actualizar pantalla Comenzando el bucle de reproducción del juego principal Evento de salida recibido: <Evento(12-Salir {})> Juego terminado 12.5 Otros conceptos Hay muchas facilidades en pygame que van más allá de lo que podemos cubrir en este libro, sin embargo, algunos de los más comunes se discuten a continuación. Las superficies son una jerarquía. La superficie de visualización de nivel superior puede contener otros superficies que se pueden utilizar para dibujar imágenes o texto. A su vez contenedores como Paneles puede renderizar superficies para mostrar imágenes o texto, etc. Otros tipos de superficie. La superficie de visualización principal no es la única superficie en pygame. Por ejemplo, cuando una imagen, como una imagen PNG o JPEG, se carga en un juego, luego se representa en una superficie. Esta superficie se puede mostrar dentro de otra superficie, como la superficie de visualización. Esto significa que cualquier cosa que pueda hacer para la superficie de visualización que puede hacer con cualquier otra superficie, como dibujar en ella, poner texto en él, coloréalo, agrega otro icono en la superficie, etc. fuentes. El objeto pygame.font.Font se usa para crear una fuente que se puede Se utiliza para representar texto en una superficie. El método render devuelve una superficie con el texto. renderizado en él que se puede mostrar dentro de otra superficie, como la pantalla Superficie. Tenga en cuenta que no puede escribir texto en una superficie existente, siempre debe obtenga una nueva superficie (usando renderizado) y luego agréguela a una superficie existente. El El texto solo se puede mostrar en una sola línea y la superficie que sostiene el texto será de las dimensiones requeridas para representar el texto. Por ejemplo: fuente_texto = pygame.font.Font(‘freesansbold.ttf’, 18) text_surface = text_font.render(‘Hello World’, antialias=True, color=AZUL) Esto crea un nuevo objeto Font usando la fuente especificada con la fuente especificada tamaño (en este caso 18). Luego mostrará la cadena ‘Hello World’ en una nueva superficie utilizando la fuente y el tamaño de fuente especificados en azul. Especificar que antialias es True indica que nos gustaría suavizar los bordes del texto en la pantalla. 12.4 Una primera aplicación de pygame 133
Rectángulos (o Rects). La clase pygame.Rect es un objeto que se usa para representar coordenadas rectangulares. Se puede crear un Rect a partir de una combinación de la parte superior izquierda coordenadas de esquina más un ancho y alto. Para mayor flexibilidad, muchas funciones que esperar que a un objeto Rect también se le pueda dar una lista Rectlike; esta es una lista que contiene los datos necesarios para crear un objeto Rect. Rects son muy útiles en un juego Pygame como se pueden usar para definir los bordes de un objeto del juego. Esto significa que pueden ser se utiliza dentro de los juegos para detectar si dos objetos han colisionado. Esto se hace particularmente fácil porque la clase Rect proporciona varios métodos de detección de colisiones: La clase también proporciona varios otros métodos de utilidad como move() que mueve el rectángulo e inflate() que puede aumentar o reducir el tamaño de los rectángulos. Dibujar formas. El módulo pygame.draw tiene numerosas funciones que pueden utilizarse para dibujar líneas y formas en una superficie, por ejemplo: pygame.draw.rect(superficie_de_pantalla, AZUL, [x, y, ANCHO, ALTO]) Esto dibujará un rectángulo azul relleno (predeterminado) en la superficie de la pantalla. El El rectángulo se ubicará en la ubicación indicada por x e y (en la superficie). Este indica la esquina superior izquierda del rectángulo. El ancho y alto de la rectángulo indican su tamaño. Tenga en cuenta que estas dimensiones se definen dentro de una lista que es una estructura denominada como recta (ver más abajo). Si no desea un relleno rectángulo (es decir, solo quiere el contorno), entonces puede usar el ancho opcional parámetro para indicar el grosor del borde exterior. Otros métodos disponibles incluir: • pygame.draw.polygon() dibuja una forma con cualquier número de lados • pygame.draw.circle() dibuja un círculo alrededor de un punto • pygame.draw.ellipse() dibuja una forma redonda dentro de un rectángulo • pygame.draw.arc() dibuja una sección parcial de una elipse • pygame.draw.line() dibuja un segmento de línea recta • pygame.draw.lines() dibuja varios segmentos de línea contiguos • pygame.draw.aaline() dibuja líneas finas suavizadas • pygame.draw.aalines() dibuja una secuencia conectada de líneas suavizadas • pygame.Rect.contains() prueba si un rectángulo está dentro de otro • pygame.Rect.collidepoint() prueba si un punto está dentro de un rectángulo • pygame.Rect.colliderect() prueba si dos rectángulos se superponen • pygame.Rect.collidelist() prueba si un rectángulo en una lista se cruza • pygame.Rect.collidelistall() prueba si todos los rectángulos en una lista se cruzan • pygame.Rect.collidedict() prueba si un rectángulo en un diccionario se cruza • pygame.Rect.collidedictall() prueba si todos los rectángulos en un diccionario se cruzan 134 12 Juegos de construcción con pygame
Imágenes. El módulo pygame.image contiene funciones para cargar, guardar y transformando imágenes. Cuando se carga una imagen en pygame, se representa por un objeto de superficie. Esto significa que es posible dibujar, manipular y procesar una imagen exactamente de la misma manera que cualquier otra superficie que proporciona una gran cantidad de flexibilidad. Como mínimo, el módulo solo admite la carga de imágenes BMP sin comprimir, pero normalmente también es compatible con JPEG, PNG, GIF (no animado), BMP, TIFF y otros formatos. Sin embargo, solo admite un conjunto limitado de formatos al guardar imágenes; estos son BMP, TGA, PNG y JPEG. Se puede cargar una imagen desde un archivo usando: image_surface = pygame.image.load(nombre de archivo).convert() Esto cargará la imagen del archivo especificado en una superficie. Una cosa de la que podría preguntarse es el uso del método convert() en el objeto devuelto por la función pygame.image.load(). Esta función devuelve una superficie que se utiliza para mostrar la imagen contenida en el archivo. llamamos al método convert () en esta superficie, no para convertir la imagen de un particular formato de archivo (como PNG o JPEG) en su lugar, este método se utiliza para convertir el píxel formato utilizado por la superficie. Si el formato de píxel utilizado por la superficie no es el mismo que el formato de visualización, será necesario convertirlo sobre la marcha cada vez que se la imagen se muestra en la pantalla; esto puede llevar bastante tiempo (y innecesario) proceso. Por lo tanto, hacemos esto una vez cuando se carga la imagen que significa que no debería obstaculizar el rendimiento del tiempo de ejecución y puede mejorar el rendimiento significativamente en algunos sistemas. Una vez que tenga una superficie que contenga una imagen, se puede representar en otra superficie, como la superficie de visualización mediante el método Surface.blit(). Para ejemplo: superficie_de_pantalla.blit(superficie_de_imagen, (x, y)) Tenga en cuenta que el argumento de posición es una tupla que especifica las coordenadas x e y para la imagen en la superficie de visualización. Estrictamente hablando, el método blit() dibuja una superficie (la superficie de origen) sobre otra superficie en las coordenadas de destino. Por lo tanto, la superficie objetivo no Beed para ser la superficie de visualización de nivel superior. Reloj. Un objeto Clock es un objeto que se puede utilizar para realizar un seguimiento del tiempo. En particular se puede utilizar para definir la velocidad de fotogramas del juego. ese es el numero de fotogramas prestado por segundo. Esto se hace usando el método Clock.tick(). Este método debe llamarse una vez (y solo una vez) por cuadro. Si pasas el opcional argumento framerate a tick() la función, entonces pygame se asegurará de que 12.5 Otros conceptos 135
la frecuencia de actualización de los juegos es más lenta que los tics dados por segundo. Esto puede ser se utiliza para ayudar a limitar la velocidad de tiempo de ejecución de un juego. Llamando a clock.tick (30) una vez por fotograma, el programa nunca se ejecutará a más de 30 fotogramas por segundo. 12.6 Una aplicación pygame más interactiva La primera aplicación de pygame que vimos anteriormente solo mostraba una ventana con el título ‘Hola mundo’. Ahora podemos extender esto un poco jugando con algunos de los características que hemos visto anteriormente. La nueva aplicación agregará algo de manejo de eventos del mouse. Esto nos permitirá recoger la ubicación del ratón cuando el usuario hizo clic en la ventana y dibujar un pequeña caja azul en ese punto. Si el usuario hace clic con el mouse varias veces, aparecerán varios cuadros azules. dibujado. Esto se muestra a continuación. Esto todavía no es un gran juego, pero hace que la aplicación pygame sea más interactivo. El programa utilizado para generar esta aplicación se presenta a continuación: importar pygame FRAME_REFRESH_RATE = 30 AZUL = (0, 0, 255) FONDO = (255, 255, 255) # Blanco ANCHO = 10 ALTURA = 10 def principal(): print(‘Inicializando PyGame’) pygame.init() # Requerido por cada aplicación PyGame 136 12 Juegos de construcción con pygame
print(‘Inicializando Box Game’) superficie_de_pantalla = pygame.display.set_mode((400, 300)) pygame.display.set_caption(‘Juego de caja’) print(‘Actualizar pantalla’) pygame.display.update() print(‘Configurar el reloj’) reloj = pygame.hora.Reloj()
Borrar la pantalla de contenidos actuales
display_surface.fill(FONDO) print(‘Comenzando el bucle de reproducción del juego principal’) corriendo = Verdadero mientras corre: para evento en pygame.event.get(): if event.type == pygame.QUIT: print(‘Evento de salida recibido:’, evento) corriendo = Falso elif event.type == pygame.MOUSEBUTTONDOWN: print(‘Evento de ratón recibido’, evento) x, y = evento.pos pygame.draw.rect(superficie_de_pantalla, AZUL, [x, y, ANCHO, ALTO])
Actualizar la pantalla
pygame.display.update()
Define la velocidad de fotogramas: el número de fotogramas por
segundo
Debe llamarse una vez por cuadro (pero solo una vez)
reloj.tick(FRAME_REFRESH_RATE) imprimir(‘Se acabó el juego’)
Ahora ordene y salga de Python
pygame.quit() si nombre == ‘principal’: principal() Tenga en cuenta que ahora necesitamos registrar la superficie de visualización en una variable local para que podamos puede usarlo para dibujar los rectángulos azules. También necesitamos llamar a pygame.dis- función play.update() cada vez que se redondea el ciclo while principal para que el nuevo Los rectángulos que hemos dibujado como parte del procesamiento de eventos para bucle se muestran para el usuario. También establecemos la velocidad de fotogramas cada vez que se redondea el ciclo while principal. Esto debería sucede una vez por cuadro (pero solo una vez) y usa el objeto de reloj inicializado en el inicio del programa. 12.6 Una aplicación pygame más interactiva 137
12.7 Enfoque alternativo para el procesamiento de dispositivos de entrada En realidad, hay dos formas en que las entradas de un dispositivo como un mouse, Se puede procesar el joystick o el teclado. Un enfoque es el modelo basado en eventos. descrito anteriormente. El otro enfoque es el enfoque basado en el Estado. Aunque el enfoque basado en eventos tiene muchas ventajas, tiene dos desventajas: • Cada evento representa una sola acción y las acciones continuas no están explícitamente representado. Por lo tanto, si el usuario presiona tanto la tecla X como la tecla Z, esto generar dos eventos y será hasta el programa para determinar que tienen sido presionado al mismo tiempo. • También depende del programa determinar que el usuario todavía está presionando una tecla (al teniendo en cuenta que no se ha producido ningún evento KEYUP). • Ambos son posibles pero pueden ser propensos a errores. Un enfoque alternativo es utilizar el enfoque basado en el estado. en el estado basado enfoque, el programa puede verificar directamente el estado de un dispositivo de entrada (como una tecla o ratón o teclado). Por ejemplo, puede usar pygame.key.get_pressed() que devuelve el estado de todas las claves. Esto se puede usar para determinar si un se está presionando la tecla en este momento. Por ejemplo, pygame.key. get_pressed()[pygame.K_SPACE] se puede usar para verificar si el espacio se presiona la barra. Esto se puede utilizar para determinar qué acción tomar. Si sigues comprobando que el se presiona la tecla entonces puede seguir realizando la acción asociada. Esto puede ser muy útil para acciones continuas en un juego, como mover un objeto, etc. Sin embargo, si el usuario presiona una tecla y luego la suelta antes de que el programa verifique el estado del teclado, entonces esa entrada se perderá. 12.8 Módulos Pygame Hay numerosos módulos provistos como parte de pygame, así como asociados bibliotecas Algunos de los módulos principales se enumeran a continuación: • pygame.display Este módulo se utiliza para controlar la ventana de visualización o pantalla. Proporciona facilidades para inicializar y apagar el módulo de visualización. Puede utilizarse para inicializar una ventana o pantalla. También se puede utilizar para hacer que una ventana o pantalla para actualizar, etc. 138 12 Juegos de construcción con pygame
• pygame.event Este módulo gestiona los eventos y la cola de eventos. Por ejemplo pygame.event.get() recupera eventos de la cola de eventos, pygame.event.poll() obtiene un solo evento de la cola y pygame.event.peek() prueba para ver si hay algún tipo de evento en la cola. • pygame.draw El módulo dibujar se utiliza para dibujar formas simples en un Superficie. Por ejemplo, proporciona funciones para dibujar un rectángulo. (pygame.draw.rect), un polígono, un círculo, una elipse, una línea, etc. • pygame.font El módulo de fuentes se utiliza para crear y representar fuentes TrueType. en un nuevo objeto de superficie. La mayoría de las características asociadas con las fuentes son compatibles. portado por la clase pygame.font.Font. Funciones del módulo independiente permite inicializar y apagar el módulo, además de funciones para acceder a las fuentes como pygame.font.get_fonts() que proporciona una lista de los fuentes disponibles. • pygame.image Este módulo permite guardar y cargar imágenes. Tenga en cuenta que las imágenes se cargan en un objeto de superficie (no hay una clase de imagen a diferencia de muchas otros marcos orientados a GUI). • pygame.joystick El módulo joystick proporciona el objeto Joystick y varias funciones de apoyo. Estos se pueden usar para interactuar con joysticks, gamepads y trackballs. • pygame.key Este módulo proporciona soporte para trabajar con entradas del teclado. Esto permite obtener las claves de entrada y modificar las claves (como Control y Shift) para ser identificado. También permite el acercamiento a llaves repetitivas. por especificar. • pygame.mouse Este módulo proporciona facilidades para trabajar con la entrada del mouse como obtener la posición actual del mouse, el estado de los botones del mouse y como la imagen a utilizar para el ratón. • pygame.time Este es el módulo de pygame para administrar el tiempo dentro de un juego. Proporciona la clase pygame.time.Clock que se puede usar para realizar un seguimiento del tiempo. 12.9 Recursos en línea Hay una gran cantidad de información disponible en pygame, que incluye: • https://www.pygame.org La página de inicio de pygame. • http://www.libsdl.org/ Documentación de SDL (Simple Directmedia Layer). • news://gmane.comp.python.pygame El grupo oficial de noticias de pygame. 12.8 Módulos Pygame 139
Capítulo 13 Pygame StarshipMeteors 13.1 Crear un juego de nave espacial En este capítulo crearemos un juego en el que pilotas una nave estelar a través de un campo de meteoros Cuanto más juegues, mayor será el número de meteoros que obtendrás. encontrar. A continuación se muestra una pantalla típica del juego para un Apple Mac y un PC con Windows: Implementaremos varias clases para representar las entidades dentro del juego. El uso de clases no es una forma obligatoria de implementar un juego y debe tenerse en cuenta que muchos desarrolladores evitan el uso de clases. Sin embargo, usar una clase permite que los datos asociado con un objeto dentro del juego para ser mantenido en un solo lugar; también simplifica la creación de múltiples instancias del mismo objeto (como los meteoritos) dentro del juego © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_13 141
Las clases y sus relaciones se muestran a continuación: Este diagrama muestra que las clases Starship y Meteor extenderán una clase llamado GameObject. A su vez, también muestra que el Juego tiene una relación 1:1 con la clase Starship. Es decir, el Juego tiene una referencia a una nave estelar y, a su vez, la nave estelar tiene una única referencia al Juego. En contraste, el Juego tiene una relación de 1 a muchos con la clase Meteor. Eso es el objeto Game contiene referencias a muchos Meteoros y cada Meteor contiene un referencia de nuevo al único objeto Game. 13.2 La clase de juego principal La primera clase que veremos será la propia clase Game. La clase Juego contendrá la lista de meteoros y la nave estelar, así como la principal bucle de juego. También inicializará la visualización de la ventana principal (por ejemplo, configurando el tamaño y el título de la ventana). En este caso, almacenaremos la superficie de visualización devuelta por pygame.dis- play.set_mode() en un atributo del objeto Game llamado dis- superficie_de_juego. Esto se debe a que necesitaremos usarlo más adelante para mostrar el nave estelar y los meteoros. También conservaremos una instancia de la clase pygame.time.Clock() que usaremos para establecer la velocidad de fotogramas cada vez que se reproduce el juego principal mientras se repite. El marco básico de nuestro juego se muestra a continuación; este listado proporciona lo básico Clase de juego y el método principal que iniciará el juego. El juego también define tres constantes globales que se utilizarán para definir la frecuencia de actualización de fotogramas y el tamaño de la pantalla 142 13 Pygame StarshipMeteors
importar pygame
Configurar ‘constantes’ globales
FRAME_REFRESH_RATE = 30 ANCHO_PANTALLA = 600 PANTALLA_ALTURA = 400 Juego de clase: """ Representa el juego en sí y el juego. bucle """ def init(uno mismo): print(‘Inicializando PyGame’) pygame.init()
Configurar la pantalla
self.display_surface = pygame.display.set_mode((DISPLAY_WIDTH, DISPLAY_HEIGHT)) pygame.display.set_caption(‘Meteoritos de la nave estelar’)
Usado para cronometrar dentro del programa.
self.reloj = pygame.time.Clock() def jugar (uno mismo): is_running = Verdadero
Juego principal jugando Loop
mientras está_corriendo:
Averiguar lo que el usuario quiere hacer
para evento en pygame.event.get(): if event.type == pygame.QUIT: is_running = Falso elif event.type == pygame.KEYDOWN: si evento.clave == pygame.K_q: is_running = Falso
Actualizar la pantalla
pygame.display.update()
Define la velocidad de fotogramas
self.clock.tick(FRAME_REFRESH_RATE)
Deja que pygame se apague correctamente
pygame.quit() def principal(): imprimir(‘Partida Inicial’) juego = juego() Como se Juega() imprimir(‘Se acabó el juego’) si nombre == ‘principal’: principal() 13.2 La clase de juego principal 143
El método principal play() de la clase Game tiene un ciclo que continuará hasta el usuario selecciona salir del juego. Pueden hacer esto de una de dos maneras, ya sea por presionando la tecla ‘q’ (representada por event.key K_q) o haciendo clic en el botón de cierre de ventana. En cualquier caso, estos eventos se recogen en el evento principal. Procesamiento de bucle dentro del método principal de bucle while. Si el usuario no quiere salir del juego, la pantalla se actualiza (refresca) y luego se establece la velocidad clock.tick() (o frame). Cuando el usuario selecciona salir del juego, el ciclo while principal finaliza (el indicador is_running se establece en False) y el método pygame.quit() es llamado para cerrar pygame. Por el momento, este no es un juego muy interactivo ya que no hace nada excepto permitir que el usuario salga. En la siguiente sección agregaremos un comportamiento que nos permitirá para mostrar la nave espacial dentro de la pantalla. 13.3 La clase GameObject La clase GameObject define tres métodos: El método load_image() se puede utilizar para cargar una imagen que se utilizará para representar visualmente el tipo específico de objeto del juego. El método luego usa el ancho y la altura de la imagen para definir la anchura y la altura del objeto del juego. El método rect() devuelve un rectángulo que representa el área actual utilizada por el objeto del juego en la superficie de dibujo subyacente. Esto difiere de las imágenes. propio rect() que no está relacionado con la ubicación del objeto del juego en el superficie subyacente. Rects son muy útiles para comparar la ubicación de un objeto con otro (por ejemplo, al determinar si se ha producido una colisión). El método draw() dibuja la imagen de GameObjects en la pantalla_- superficie sostenida por el juego usando las coordenadas x e y actuales de GameObjects. Él puede ser anulado por subclases si desean dibujarse de una manera diferente. El código para la clase GameObject se presenta a continuación: clase GameObject: def load_image(self, nombre de archivo): self.image = pygame.image.load(nombre de archivo).convert() self.ancho = self.imagen.get_width() self.altura = self.imagen.obtener_altura() def rect(auto): """ Genera un rectángulo que representa los objetos ubicación y dimensiones """ 144 13 Pygame StarshipMeteors
return pygame.Rect(self.x, self.y, self.width, altura propia) def dibujar(auto): """ dibuja el objeto del juego en el coordenadas x, y actuales """ self.game.display_surface.blit(self.image, (self.x, yo.y)) La clase GameObject se amplía directamente con la clase Starship y la clase Clase de meteorito. Actualmente solo hay dos tipos de elementos de juego, la nave estelar y el meteoros; pero esto podría extenderse en el futuro a planetas, cometas, estrellas fugaces, etc. 13.4 Mostrando la nave estelar El jugador humano de este juego controlará una nave estelar que se puede mover alrededor del mostrar. El Starship estará representado por una instancia de la clase Starship. Este La clase extenderá la clase GameObject que tiene comportamientos comunes para cualquier tipo de elemento que se representa dentro del juego. La clase Starship define su propio método init() que toma una referencia al juego del que forma parte la nave estelar. Este método de inicialización establece la inicial ubicación inicial de Starship como la mitad del ancho de la pantalla para la coordenada x y la altura de la pantalla menos 40 para la coordenada y (esto da un poco de búfer antes del final de la pantalla). Luego usa el método load_image() del Clase principal de GameObject para cargar la imagen que se usará para representar el Nave estelar. Esto se guarda en un archivo llamado starship.png. Por el momento lo haremos dejar la clase Starship como está (sin embargo, volveremos a esta clase para que podamos puede convertirlo en un objeto móvil en la siguiente sección). La versión actual de la clase Starship se muestra a continuación: clase Starship(GameObject): """ Representa una nave estelar""" def init(uno mismo, juego): self.juego = juego self.x = DISPLAY_WIDTH / 2 self.y = DISPLAY_HEIGHT - 40 self.load_image(’nave estelar.png’) 13.3 La clase GameObject 145
En la clase Game ahora agregaremos una línea al método init() para inicialice el objeto Starship. Esta línea es:
Configurar la nave estelar
self.starship = Starship(self) También agregaremos una línea al ciclo while principal dentro del método play() simplemente antes de actualizar la pantalla. Esta línea llamará al método draw() en la nave estelar objeto:
Dibuja la nave estelar
self.nave.dibujar() Esto tendrá el efecto de dibujar la nave estelar en el dibujo de Windows. superficie en segundo plano antes de que se actualice la pantalla. Cuando ahora ejecutamos esta versión del juego StarshipMeteor ahora vemos el Nave estelar en la pantalla: Por supuesto, en este momento la nave estelar no se mueve; pero lo abordaremos en la siguiente sección. 13.5 Moviendo la nave espacial Queremos poder mover el Starship dentro de los límites de la pantalla. pantalla. Para hacer esto, necesitamos cambiar las coordenadas x e y de las naves estelares en respuesta a la usuario presionando varias teclas. Usaremos las teclas de flecha para movernos arriba y abajo de la pantalla o hacia la izquierda o hacia la derecha de la pantalla Para ello definiremos cuatro métodos dentro de la clase Starship; estos métodos moverán la nave estelar hacia arriba, abajo, izquierda y derecha, etc. 146 13 Pygame StarshipMeteors
La clase Starship actualizada se muestra a continuación: Esta versión de la clase Starship define los distintos métodos de movimiento. Estos métodos utilizan un nuevo valor global STARSHIP_SPEED para determinar qué tan lejos y qué tan rápido se mueve la nave estelar. Si desea cambiar la velocidad a la que Starship se mueve, entonces puede cambiar este valor global. Dependiendo de la dirección deseada, necesitaremos modificar x o y coordenada de la nave estelar. • Si la nave estelar se mueve hacia la izquierda, la coordenada x se reduce en NAVE_ESTRELLA_VELOCIDAD, • si él se mueve a el bien entonces el X coordinar es aumentó por NAVE_ESTRELLA_VELOCIDAD, • a su vez, si Starship se mueve hacia arriba en la pantalla, la coordenada y se reduce por STARSHIP_SPEED, 13.5 Moviendo la nave espacial 147
• pero si se mueve hacia abajo en la pantalla, la coordenada y aumenta en NAVE_ESTRELLA_VELOCIDAD. Por supuesto, no queremos que nuestro Starship salga volando por el borde de la pantalla, por lo que se debe hacer una prueba para ver si ha llegado a los límites de la pantalla. Así son las pruebas hecho para ver si los valores de x o y han ido por debajo de cero o por encima de la valores DISPLAY_WIDTH o DISPLAY_HEIGHT. Si alguna de estas condiciones es cumplido, los valores x o y se restablecen a un valor predeterminado apropiado. Ahora podemos usar estos métodos con la entrada del jugador. Esta entrada del jugador indicará la dirección en la que el jugador quiere mover el Starship. Como estamos usando la izquierda, teclas de flecha hacia la derecha, hacia arriba y hacia abajo para esto, podemos extender el ciclo de procesamiento de eventos que ya hemos definido para el ciclo de juego principal. Al igual que con la letra q, la las teclas de eventos tienen el prefijo de la letra K y un guión bajo, pero esta vez las teclas son llamados K_IZQUIERDA, K_DERECHA, K_ARRIBA y K_ABAJO. Cuando se presiona una de estas teclas, llamaremos al movimiento apropiado método en el objeto de la nave estelar que ya está en manos del objeto Game. El procesamiento de eventos principal para bucle es ahora:
Averiguar lo que el usuario quiere hacer
para evento en pygame.event.get(): if event.type == pygame.QUIT: is_running = Falso elif event.type == pygame.KEYDOWN:
Comprobar para ver qué tecla se presiona
si evento.key == pygame.K_RIGHT:
Se ha presionado la tecla de flecha derecha
mover al jugador a la derecha
self.nave estelar.mover_derecha() elif evento.key == pygame.K_LEFT:
Se ha presionado la flecha izquierda
mover al jugador a la izquierda
self.nave estelar.move_left() elif evento.key == pygame.K_UP: self.nave.mover_arriba() elif evento.key == pygame.K_DOWN: self.nave estelar.mover_abajo() elif evento.key == pygame.K_q: is_running = Falso Sin embargo, no hemos terminado del todo. Si intentamos ejecutar esta versión del programa obtendremos un rastro de naves espaciales dibujadas en la pantalla; Por ejemplo: 148 13 Pygame StarshipMeteors
El problema es que estamos redibujando la nave estelar en una posición diferente; pero el la imagen anterior sigue presente. Ahora tenemos dos opciones, una es simplemente llenar toda la pantalla con negro; ocultando efectivamente todo lo que se ha dibujado hasta ahora; o alternativamente podríamos simplemente dibuje sobre el área utilizada por la posición anterior de la imagen. ¿Qué enfoque se adopta depende del escenario particular representado por su juego. Como tendremos mucho de meteoros en pantalla una vez que los hemos añadido; la opción más fácil es sobre- escribe todo en la pantalla antes de volver a dibujar la nave estelar. Por lo tanto, agregaremos la siguiente línea:
Borrar la pantalla de contenidos actuales
self.display_surface.fill(FONDO) Esta línea se agrega justo antes de que dibujemos Starship dentro del juego principal. mientras bucle. Ahora, cuando movemos el Starship, la imagen anterior se elimina antes de dibujar el nueva imagen: Un punto a tener en cuenta es que también hemos definido otro valor global FONDO utilizado para contener el color de fondo de la superficie de juego. Esto se establece en negro como se muestra a continuación: 13.5 Moviendo la nave espacial 149
Definir colores RGB predeterminados
FONDO = (0, 0, 0) Si desea utilizar un color de fondo diferente, cambie este valor global. 13.6 Agregar una clase de meteorito La clase Meteor también será una subclase de la clase GameObject. De todos modos, eso solo proporcionará un método move_down() en lugar de la variedad de movimiento métodos de la nave estelar. También necesitará tener una coordenada x de inicio aleatoria para que cuando un meteoro sea agregado al juego, su posición inicial variará. Esta posición aleatoria puede ser generado usando la función random.randint() usando un valor entre 0 y el ancho de la superficie de dibujo. El meteoro también comenzará en la parte superior de la pantalla. por lo que tendrá una coordenada y inicial diferente a la de Starship. Por último, también queremos que nuestro meteoros para tener diferentes velocidades; este puede ser otro número aleatorio entre 1 y alguna velocidad máxima especificada del meteorito. Para admitir esto, necesitamos agregar aleatorio a los módulos que se importan y defina varios valores globales nuevos, por ejemplo: importar pygame, aleatorio INITIAL_METEOR_Y_LOCATION = 10 MAX_METEOR_SPEED = 5 Ahora podemos definir la clase Meteor: clase Meteoro(GameObject): """ representa un meteorito en el juego """ def init(uno mismo, juego): self.juego = juego self.x = random.randint(0, DISPLAY_WIDTH) self.y = INITIAL_METEOR_Y_LOCATION self.velocidad = random.randint(1, MAX_METEOR_SPEED) self.load_image(‘meteoro.png’) def move_down(self): """ Mueve el meteorito hacia abajo en la pantalla """ self.y = self.y + self.velocidad si self.y > DISPLAY_HEIGHT: self.y = 5 def str(uno mismo): return ‘Meteorito(’ + str(self.x) + ‘, ’ + str(self.y) + ‘)’ 150 13 Pygame StarshipMeteors
El método init() para la clase Meteor tiene los mismos pasos que el nave estelar; la diferencia es que la coordenada x y la velocidad son aleatorias generado. La imagen utilizada para Meteor también es diferente, ya que es ‘meteor.png’. También hemos implementado un método move_down(). Esta es esencialmente la igual que las naves estelares move_down(). Tenga en cuenta que en este punto podríamos crear una subclase de GameObject llamada MoveableGameObject (que extiende GameObject) y presiona el movimiento operaciones hasta esa clase y hacer que las clases Meteor y Starship se extiendan esa clase. Sin embargo, realmente no queremos permitir que los meteoritos se muevan a cualquier lugar. en la pantalla. Ahora podemos agregar los meteoros a la clase Juego. Agregaremos un nuevo valor global para indicar el número de meteoros iniciales en el juego: NÚMERO_INICIAL_DE_METEOROS = 8 A continuación, inicializaremos un nuevo atributo para la clase Juego que contendrá una lista de Meteoritos. Usaremos una lista aquí ya que queremos aumentar el número de meteoros a medida que el juego avanza. Para facilitar este proceso, utilizaremos una lista de comprensión que permite una bucle para ejecutar con los resultados de una expresión capturada por la lista:
Configurar meteoros
self.meteors = [Meteorito(self) for _ in range(0, NÚMERO_INICIAL_DE_METEOROS)] Ahora tenemos una lista de meteoros que deben mostrarse. Por lo tanto, necesitamos actualizar el ciclo while del método play() para dibujar no solo la nave estelar sino también todos los meteoros:
Dibuja los meteoros y la nave estelar.
self.nave.dibujar() para meteoro en self.meteors: meteoro.dibujar() El resultado final es que se crea un conjunto de objetos de meteorito al azar comenzando ubicaciones en la parte superior de la pantalla: 13.6 Agregar una clase de meteorito 151
13.7 Moviendo los meteoritos Ahora queremos poder mover los meteoritos hacia abajo en la pantalla para que Starship tiene algunos objetos para evitar. Podemos hacer esto muy fácilmente ya que hemos implementado un move_down() método en la clase Meteor. Por lo tanto, solo necesitamos agregar un bucle for al principal juego jugando mientras bucle que moverá todos los meteoros. Por ejemplo:
Mueve los meteoritos
para meteoro en self.meteors: meteoro.mover_abajo() Esto se puede agregar después del procesamiento de eventos para bucle y antes de que la pantalla se refrescado/redibujado o actualizado. Ahora, cuando ejecutamos el juego, los meteoritos se mueven y el jugador puede navegar por el Starship entre los meteoritos que caen. 13.8 Identificación de una colisión Por el momento, el juego se jugará para siempre, ya que no hay un estado final ni un intento de identificar si una Starship ha colisionado con un meteorito. Podemos agregar la detección de colisiones Meteor/Starship utilizando PyGame Rects. Como mencionado en el último capítulo, un Rect es una clase de PyGame que se usa para representar rect- coordenadas angulares Es particularmente útil ya que la clase pygame.Rect proporciona varios métodos de detección de colisiones que se pueden usar para probar si un rectángulo (o punto) está dentro de otro rectángulo. Por lo tanto, podemos usar uno de los métodos para probar si el rectángulo alrededor de Starship se cruza con cualquiera de los rectángulos alrededor de la Meteoritos. 152 13 Pygame StarshipMeteors
La clase GameObject ya proporciona un método rect() que devolverá un Rect objeto que representa el rectángulo actual de los objetos con respecto al dibujo superficie (esencialmente el cuadro alrededor del objeto que representa su ubicación en el pantalla). Por lo tanto, podemos escribir un método de detección de colisiones para la clase Juego usando el Rects generados por GameObject y el método colliderect() de la clase Rect: def _check_for_collision(self): """ Comprueba si alguno de los meteoros ha colisionado con la nave estelar """ resultado = Falso para meteoro en self.meteors: si self.starship.rect().colliderect(meteor.rect()): resultado = Verdadero romper resultado devuelto Tenga en cuenta que hemos seguido la convención aquí de preceder el nombre del método con un guión bajo que indica que este método debe considerarse privado para el clase. Por lo tanto, nunca debe ser llamado por nada fuera de la clase Game. Esta convención se define en PEP 8 (Propuesta de mejora de Python) pero no es impuesto por el lenguaje. Ahora podemos usar este método en el ciclo while principal del juego para buscar un colisión:
Comprobar para ver si un meteorito ha golpeado el barco
si self._check_for_collision(): starship_collided = Verdadero Este fragmento de código también presenta una nueva variable local starship_collided. Inicialmente estableceremos esto en False y es otra condición bajo la cual el principal el juego mientras el bucle terminará: is_running = Verdadero starship_collided = Falso
Juego principal jugando Loop
while is_running and not starship_collided: Por lo tanto, el bucle de reproducción del juego terminará si el usuario selecciona salir o si el Starship choca con un meteorito. 13.8 Identificación de una colisión 153
13.9 Identificar una victoria Actualmente tenemos una forma de perder el juego, pero no tenemos una forma de ganar el ¡juego! Sin embargo, queremos que el jugador pueda ganar el juego sobreviviendo durante un período de tiempo especificado. Podríamos representar esto con un temporizador de algún tipo. Sin embargo, en nuestro caso lo representaremos como un número específico de ciclos de la principal bucle de juego. Si el jugador sobrevive durante este número de ciclos, entonces tiene ganado. Por ejemplo:
Ver si el jugador ha ganado
if ciclo_recuento == MAX_NUMBER_OF_CYCLES: imprimir(’¡GANADOR!’) romper En este caso, se imprime un mensaje que indica que el jugador ganó y luego el finaliza el ciclo de juego principal (usando la instrucción break). El valor global MAX_NUMBER_OF_CYCLES se puede establecer según corresponda, por ejemplo: MAX_NUMBER_OF_CYCLES = 1000 13.10 Aumentar el número de meteoritos Podríamos dejar el juego como está en este punto, ya que ahora es posible ganar o perder. el juego. Sin embargo, hay algunas cosas que se pueden agregar fácilmente que mejorarán la experiencia de juego. Uno de ellos es aumentar el número de meteoritos en la pantalla haciéndolo más difícil a medida que avanza el juego. Podemos hacer esto usando un NEW_METEOR_CYCLE_INTERVAL. NUEVO_METEOR_CICLO_INTERVALO = 40 Cuando se alcanza este intervalo, podemos agregar un nuevo Meteoro a la lista de actuales meteoritos; luego será dibujado automáticamente por la clase Game. Por ejemplo:
Determinar si se deben agregar nuevos meteoros
si ciclo_recuento % NEW_METEOR_CYCLE_INTERVAL == 0: self.meteors.append(Meteor(self)) 154 13 Pygame StarshipMeteors
Ahora cada NEW_METEOR_CYCLE_INTERVAL se agregará otro meteoro en una coordenada x aleatoria del juego. 13.11 Pausar el juego Otra característica que tienen muchos juegos es la capacidad de pausar el juego. Esto puede ser se agrega fácilmente monitoreando una tecla de pausa (esta podría ser la letra p representada por el event_key pygame.K_p). Cuando se presiona, el juego se puede pausar hasta que se vuelve a pulsar la tecla. La operación de pausa se puede implementar como un método _pause() que consume todos los eventos hasta que se presiona la tecla apropiada. Por ejemplo: def _pause(auto): en pausa = Verdadero mientras está en pausa: para evento en pygame.event.get(): if event.type == pygame.KEYDOWN: si evento.clave == pygame.K_p: en pausa = Falso romper En este método, el ciclo while externo se repetirá hasta que la variable local en pausa sea establecer demasiado Falso. Esto solo sucede cuando se presiona la tecla ‘p’. El descanso después de la la configuración de la declaración en pausa en False garantiza que el bucle for interno termine permitiendo que el ciclo while externo verifique el valor de pausado y termine. El método _pause() puede ser invocado durante el ciclo de juego por monitoreando la tecla ‘p’ dentro del evento for loop y llamando a _pause() método a partir de ahí: elif evento.key == pygame.K_p: auto._pausa() Tenga en cuenta que nuevamente hemos indicado que no esperamos que el método pause() para ser llamado desde fuera del juego anteponiendo el nombre del método con un guión bajo (’’). 13.10 Aumentar el número de meteoritos 155
13.12 Mostrar el mensaje Game Over PyGame no viene con una manera fácil de crear un cuadro de diálogo emergente para mostrar mensajes como ‘Usted ganó’; o ‘Perdiste’, razón por la cual hemos utilizado la impresión declaraciones hasta el momento. Sin embargo, podríamos usar un marco GUI como wxPython para hacer esto o podríamos mostrar un mensaje en la superficie de la pantalla para indicar si el jugador ha ganado o perdido. Podemos mostrar un mensaje en la superficie de la pantalla usando pygame.font. Clase de fuente. Esto se puede usar para crear un objeto de fuente que se puede representar en un superficie que se puede mostrar en la superficie de visualización principal. Por lo tanto, podemos agregar un método _display_message() a la clase Game que se puede utilizar para mostrar los mensajes apropiados: def _display_message(yo, mensaje): """ Muestra un mensaje al usuario en la pantalla """ imprimir (mensaje) fuente_texto = pygame.font.Font(‘freesansbold.ttf’, 48) text_surface = text_font.render(mensaje, Verdadero, AZUL, BLANCO) rectángulo_texto = superficie_texto.get_rect() text_rectangle.center = (DISPLAY_WIDTH / 2, PANTALLA_ALTURA / 2) self.display_surface.fill(BLANCO) self.display_surface.blit(text_surface, text_rectangle) Una vez más, el subrayado inicial en el nombre del método indica que no debe ser llamado desde fuera de la clase Juego. Ahora podemos modificar el ciclo principal para que se muestren los mensajes apropiados. al usuario, por ejemplo:
Comprobar para ver si un meteorito ha golpeado el barco
si self._check_for_collision(): starship_collided = Verdadero self._display_message(‘Colisión: fin del juego’) 156 13 Pygame StarshipMeteors
El resultado de ejecutar el código anterior cuando ocurre una colisión se muestra a continuación: 13.13 El juego StarshipMeteors Se proporciona la lista completa de la versión final del juego StarshipMeteors abajo: importar pygame, aleatorio, tiempo FRAME_REFRESH_RATE = 30 ANCHO_PANTALLA = 600 PANTALLA_ALTURA = 400 BLANCO = (255, 255, 255) FONDO = (0, 0, 0) INITIAL_METEOR_Y_LOCATION = 10 NÚMERO_INICIAL_DE_METEOROS = 8 MAX_METEOR_SPEED = 5 VELOCIDAD DE NAVE_ESTRELLA = 10 MAX_NUMBER_OF_CYCLES = 1000 NUEVO_METEOR_CICLO_INTERVALO = 40 13.12 Mostrar el mensaje Game Over 157
clase GameObject: def load_image(self, nombre de archivo): self.image = pygame.image.load(nombre de archivo).convert() self.ancho = self.imagen.get_width() self.altura = self.imagen.obtener_altura() def rect(auto): """ Genera un rectángulo que representa los objetos ubicación y dimensiones """ return pygame.Rect(self.x, self.y, self.width, altura propia) def dibujar(auto): """ dibuja el objeto del juego en el coordenadas x, y actuales """ self.game.display_surface.blit(self.image, (self.x, yo.y)) clase Starship(GameObject): """ Representa una nave estelar""" def init(uno mismo, juego): self.juego = juego self.x = DISPLAY_WIDTH / 2 self.y = DISPLAY_HEIGHT - 40 self.load_image(’nave estelar.png’) def move_right(self): """ mueve la nave espacial a través de la pantalla """ self.x = self.x + STARSHIP_SPEED si self.x + self.width > DISPLAY_WIDTH: self.x = DISPLAY_WIDTH - self.ancho def move_left(self): """ Mueve la nave estelar hacia la izquierda en la pantalla """ self.x = self.x - STARSHIP_SPEED si self.x < 0: self.x = 0 def move_up(self): """ Mueve la nave espacial hacia arriba en la pantalla """ self.y = self.y - STARSHIP_SPEED si self.y < 0: self.y = 0 def move_down(self): """ Mueve la nave hacia abajo en la pantalla """ self.y = self.y + STARSHIP_SPEED if self.y + self.height > DISPLAY_HEIGHT: 158 13 Pygame StarshipMeteors
self.y = DISPLAY_HEIGHT - self.height def str(uno mismo): return ‘Nave estelar(’ + str(self.x) + ‘, ’ + str(self.y) + ‘)’ clase Meteoro(GameObject): """ representa un meteorito en el juego """ def init(uno mismo, juego): self.juego = juego self.x = random.randint(0, DISPLAY_WIDTH) self.y = INITIAL_METEOR_Y_LOCATION self.velocidad = random.randint(1, MAX_METEOR_SPEED) self.load_image(‘meteoro.png’) def move_down(self): """ Mueve el meteorito hacia abajo en la pantalla """ self.y = self.y + self.velocidad si self.y > DISPLAY_HEIGHT: self.y = 5 def str(uno mismo): return ‘Meteorito(’ + str(self.x) + ‘, ’ + str(self.y) + ‘)’ Juego de clase: """ Representa el juego en sí, mantiene el juego principal jugando bucle """ def init(uno mismo): pygame.init()
Configurar la pantalla
self.display_surface = pygame.display.set_mode((DISPLAY_WIDTH, DISPLAY_HEIGHT)) pygame.display.set_caption(‘Meteoritos de la nave estelar’)
Usado para cronometrar dentro del programa.
self.reloj = pygame.time.Clock()
Configurar la nave estelar
self.starship = Starship(self)
Configurar meteoros
self.meteors = [Meteorito(self) for _ in range(0, NÚMERO_INICIAL_DE_METEOROS)] def _check_for_collision(self): """ Verifica si alguno de los meteoros ha colisionado con la nave estelar """ resultado = Falso para meteoro en self.meteors: si self.starship.rect().colliderect(meteor.rect()): resultado = Verdadero 13.13 El juego StarshipMeteors 159
romper resultado devuelto def _display_message(yo, mensaje): """ Muestra un mensaje al usuario en la pantalla """ fuente_texto = pygame.font.Font(‘freesansbold.ttf’, 48) text_surface = text_font.render(mensaje, Verdadero, AZUL, BLANCO) rectángulo_texto = superficie_texto.get_rect() text_rectangle.center = (DISPLAY_WIDTH / 2, PANTALLA_ALTURA / 2) self.display_surface.fill(BLANCO) self.display_surface.blit(text_surface, text_rectangle) def _pause(auto): en pausa = Verdadero mientras está en pausa: para evento en pygame.event.get(): if event.type == pygame.KEYDOWN: si evento.clave == pygame.K_p: en pausa = Falso romper def jugar (uno mismo): is_running = Verdadero starship_collided = Falso cuenta_de_ciclos = 0
Juego principal jugando Loop
while is_running and not starship_collided:
Indica cuántas veces se ha repetido el bucle principal del juego.
ha sido ejecutado cuenta_de_ciclos += 1
Ver si el jugador ha ganado
if ciclo_recuento == MAX_NUMBER_OF_CYCLES: self._display_message(’¡GANADOR!’) romper
Averiguar lo que el usuario quiere hacer
para evento en pygame.event.get(): if event.type == pygame.QUIT: is_running = Falso elif event.type == pygame.KEYDOWN:
Comprobar para ver qué tecla se presiona
si evento.key == pygame.K_RIGHT:
Se ha presionado la tecla de flecha derecha
mover al jugador a la derecha
self.nave estelar.mover_derecha() elif evento.key == pygame.K_LEFT:
Se ha presionado la flecha izquierda
160 13 Pygame StarshipMeteors
mover al jugador a la izquierda
self.nave estelar.move_left() elif evento.key == pygame.K_UP: self.nave.mover_arriba() elif evento.key == pygame.K_DOWN: self.nave estelar.mover_abajo() elif evento.key == pygame.K_p: auto._pausa() elif evento.key == pygame.K_q: is_running = Falso
Mueve los meteoritos
para meteoro en self.meteors: meteoro.mover_abajo()
Borrar la pantalla de contenidos actuales
self.display_surface.fill(FONDO)
Dibuja los meteoros y la nave estelar.
self.nave.dibujar() para meteoro en self.meteors: meteoro.dibujar()
Comprobar para ver si un meteorito ha golpeado el barco
si self._check_for_collision(): starship_collided = Verdadero self._display_message(‘Colisión: fin del juego’)
Determinar si se deben agregar nuevos materiales
si ciclo_recuento % NEW_METEOR_CYCLE_INTERVAL == 0: self.meteors.append(Meteor(self))
Actualizar la pantalla
pygame.display.update()
Define la velocidad de fotogramas. El número es número de
fotogramas por
segundo. Debe llamarse una vez por cuadro (pero solo
una vez) self.clock.tick(FRAME_REFRESH_RATE) tiempo.dormir(1)
Deja que pygame se apague correctamente
pygame.quit() def principal(): imprimir(‘Partida Inicial’) juego = juego() Como se Juega() imprimir(‘Se acabó el juego’) si nombre == ‘principal’: principal() 13.13 El juego StarshipMeteors 161
13.14 Recursos en línea Hay una gran cantidad de información disponible en PyGame, que incluye: • https://www.pygame.org La página de inicio de PyGame. • https://www.pygame.org/docs/tut/PygameIntro.html Tutorial de PyGame. • https://www.python.org/dev/peps/pep-0008/ Guía de estilo PEP8 para Python Código. 13.15 Ejercicios Usando el ejemplo presentado en este capítulo, agregue lo siguiente: • Proporcionar un contador de puntuación. Esto podría basarse en el número de ciclos que el jugador sobrevive o la cantidad de meteoros que se reinician desde la parte superior de la pantalla, etc. • Agregue otro tipo de GameObject, esto podría ser una estrella fugaz que se mueve a través de la pantalla horizontalmente; tal vez usando una coordenada y inicial aleatoria. • Permitir que se especifique la dificultad del juego al principio. Esto podría afectar la número de meteoros iniciales, la velocidad máxima de un meteoro, el número de estrellas fugaces etc 162 13 Pygame StarshipMeteors
Parte III Pruebas
capitulo 14 Introducción a las pruebas 14.1 Introducción Este capítulo considera los diferentes tipos de pruebas que usted podría querer realizar con los sistemas que desarrollas en Python. También introduce Test Driven Desarrollo. 14.2 Tipos de pruebas Hay al menos dos formas de pensar acerca de las pruebas:
- Es el proceso de ejecutar un programa con la intención de encontrar errores/bugs (ver Glenford Myers, El arte de las pruebas de software).
- Es un proceso utilizado para establecer que los componentes de software cumplen con los requisitos identificado para ellos, es decir, que hacen lo que se supone que deben hacer. Estos dos aspectos de las pruebas tienden a enfatizarse en diferentes puntos de la ciclo de vida del software. La prueba de errores es una parte intrínseca del proceso de desarrollo, y Se está poniendo un énfasis cada vez mayor en hacer que las pruebas sean una parte central del software. (ver Desarrollo dirigido por pruebas). Cabe señalar que es extremadamente difícil, y en muchos casos imposible, para probar que el software funciona y está completamente libre de errores. El hecho de que un conjunto de pruebas no encuentra defectos no prueba que el software esté libre de errores. ‘Falta de prueba no es evidencia de ausencia!’. Esto se discutió a fines de la década de 1960 y principios de la de 1970. por Dijkstra y se puede resumir como: Las pruebas muestran la presencia, no la ausencia de errores Las pruebas para establecer que los componentes de software cumplen su contrato implican comprobando las operaciones contra sus requisitos. Aunque esto sucede en © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_14 165
tiempo de desarrollo, forma una parte importante de Garantía de calidad (QA) y Usuario Test de aceptación. Cabe señalar que con la llegada de Test-Driven Desarrollo, el énfasis en las pruebas contra los requisitos durante el desarrollo ha llegar a ser significativamente mayor. Por supuesto, hay muchos otros aspectos de las pruebas, por ejemplo, Rendimiento Pruebas que identifican cómo se desempeñará un sistema según varios factores que afectan ese cambio de sistema Por ejemplo, a medida que aumenta el número de solicitudes simultáneas, a medida que número de procesadores utilizados por los cambios de hardware subyacente, como el tamaño de la la base de datos crece, etc. Independientemente de cómo vea las pruebas, cuantas más pruebas se apliquen a un sistema, mayor será la nivel de confianza de que el sistema funcionará como se requiere. 14.3 ¿Qué debe probarse? Una pregunta interesante es ¿Qué aspectos de su sistema de software deben estar sujetos a la prueba?’. En general, cualquier cosa que sea repetible debe estar sujeta a formal (e idealmente) pruebas automatizadas). Esto incluye (pero no se limita a): • El proceso de construcción para todas las tecnologías involucradas. • El proceso de implementación en todas las plataformas bajo consideración. • El proceso de instalación para todos los entornos de tiempo de ejecución. • El proceso de actualización para todas las versiones compatibles (si corresponde). • El rendimiento del sistema/servidores a medida que aumentan las cargas. • La estabilidad de los sistemas que deben funcionar durante cualquier período de tiempo (por ejemplo, 24 horas al día, 7 días a la semana). sistemas). • Los procesos de copia de seguridad. • La seguridad del sistema. • La capacidad de recuperación del sistema en caso de falla. • La funcionalidad del sistema. • La integridad del sistema. Tenga en cuenta que solo los dos últimos de la lista anterior pueden ser lo que comúnmente se considera áreas consideradas que estarían sujetas a prueba. Sin embargo, para garantizar la calidad de la sistema bajo consideración, todo lo anterior es relevante. De hecho, las pruebas deben cubrir todos los aspectos del ciclo de vida del desarrollo de software y no solo la fase de control de calidad. Durante la recopilación de requisitos, la prueba es el proceso de buscar información faltante o requisitos ambiguos. Durante esta fase también se debe considerar con con respecto a cómo se probarán los requisitos generales, en el sistema de software final. 166 14 Introducción a las pruebas
La planificación de la prueba también debe considerar todos los aspectos del software bajo prueba para su funcionamiento. funcionalidad, usabilidad, cumplimiento legal, conformidad con las restricciones regulatorias, seguridad lidad, rendimiento, disponibilidad, resiliencia, etc. Las pruebas deben estar impulsadas por la necesidad para identificar y reducir el riesgo. 14.4 Pruebas de sistemas de software Como se indicó anteriormente, hay una serie de diferentes tipos de pruebas que son de uso común en la industria. Estos tipos son: • Pruebas Unitarias, que se utilizan para verificar el comportamiento de los componentes individuales. • Pruebas de integración que comprueban que cuando se combinan componentes individuales juntos para proporcionar unidades funcionales de nivel superior, que la combinación de los las unidades funcionan correctamente. • Pruebas de regresión. Cuando se agregan nuevos componentes a un sistema, o los existentes se cambian los componentes, es necesario verificar que la nueva funcionalidad no romper ninguna funcionalidad existente. Tal prueba se conoce como Regresión. Pruebas. • Las pruebas de rendimiento se utilizan para garantizar que el rendimiento de los sistemas sea el requerido y, dentro de los parámetros de diseño, y es capaz de escalar como utilización aumenta • La prueba de estabilidad representa un estilo de prueba que intenta simular el sistema operación durante un período prolongado de tiempo. Por ejemplo, para una compra en línea aplicación que se espera que esté en funcionamiento las 24 horas del día, los 7 días de la semana, una prueba de estabilidad podría asegurarse de que con una carga promedio el sistema pueda funcionar las 24 horas del día durante los 7 días de la semana. 14.3 ¿Qué debe probarse? 167
• Las pruebas de seguridad garantizan que el acceso al sistema se controle adecuadamente dados los requisitos. Por ejemplo, para un sistema de compras en línea puede haber existir diferentes requisitos de seguridad dependiendo de si está navegando por el tienda, comprando algunos productos o manteniendo el catálogo de productos. • Pruebas de usabilidad que puede realizar un grupo especializado en usabilidad y puede involucrar filmar a los usuarios mientras usan el sistema. • Las pruebas del sistema validan que el sistema como un todo cumple con el usuario requisitos y se ajusta a la integridad requerida de la aplicación. • La prueba de aceptación del usuario es una forma de prueba orientada al usuario donde los usuarios confirman que el sistema hace y se comporta de la manera que esperan. • Pruebas de instalación, implementación y actualización. Estos tres tipos de pruebas validar que un sistema se puede instalar e implementar apropiadamente incluyendo cualquier procesos de actualización que puedan ser necesarios. • Pruebas de humo utilizadas para verificar que los elementos centrales de un sistema grande funcionen correctamente. Por lo general, se pueden ejecutar rápidamente y en una fracción del tiempo necesario para ejecutar las pruebas completas del sistema. Los enfoques de prueba clave se analizan en el resto de esta sección. 14.4.1 Examen de la unidad Una unidad puede ser tan pequeña como una sola función o tan grande como un subsistema, pero por lo general es una clase, objeto, biblioteca independiente (API) o página web. Al observar un pequeño componente autónomo, se puede realizar un amplio conjunto de pruebas. desarrollado para ejercer los requisitos definidos y la funcionalidad de la unidad. Las pruebas unitarias generalmente siguen un enfoque de caja blanca (también llamado Glass Box o Pruebas estructurales), donde la prueba utiliza el conocimiento y la comprensión del código y su estructura, en lugar de solo su interfaz (que se conoce como la caja negra acercarse). En las pruebas de caja blanca, la cobertura de la prueba se mide por el número de rutas de código que han sido probados. El objetivo de las pruebas unitarias es proporcionar una cobertura del 100 %: ejercitar cada instrucción, todos los lados de cada rama lógica, todos los objetos llamados, manejo de todos estructuras de datos, terminación normal y anormal de todos los bucles, etc. Por supuesto, esto Puede que no siempre sea posible, pero es un objetivo al que se debe apuntar. Muchos auto- las herramientas de prueba combinadas incluirán una medida de cobertura de código para que sepa cómo gran parte de su código ha sido ejercitado por un conjunto dado de pruebas. Las pruebas unitarias casi siempre están automatizadas; hay muchas herramientas para ayudar con esto, quizás el más conocido sea la familia xUnit de marcos de prueba como JUnit para Java y PyUnit para Python. El marco permite a los desarrolladores: • centrarse en probar la unidad, • simular datos o resultados de llamar a otra unidad (representativa buena y mala resultados), 168 14 Introducción a las pruebas
• crear pruebas basadas en datos para una máxima flexibilidad y repetibilidad, • confiar en objetos simulados que representan elementos fuera de la unidad con los que debe interactuar con. Tener las pruebas automatizadas significa que se pueden ejecutar con frecuencia, al menos después del desarrollo inicial y después de cada cambio que afecta a la unidad. Una vez que se establece la confianza en el correcto funcionamiento de una unidad, los desarrolladores luego puede usarlo para ayudar a probar otras unidades con las que interactúa, formando unidades más grandes que también pueden ser probados por unidad o, a medida que la escala se hace más grande, pasar por Integración Pruebas. 14.4.2 Pruebas de integración La prueba de integración es donde varias unidades (o módulos) se juntan para ser probado como una entidad por derecho propio. Por lo general, las pruebas de integración tienen como objetivo garantizar que los módulos interactúan correctamente y los desarrolladores de unidades individuales han interpretado los requisitos de manera consistente. Un conjunto integrado de módulos se puede tratar como una unidad y una unidad probada en gran parte del de la misma manera que los módulos constituyentes, pero generalmente trabajando a un nivel “superior” de funcionalidad. Las pruebas de integración son la etapa intermedia entre las pruebas unitarias y prueba completa del sistema. Por lo tanto, las pruebas de integración se centran en la interacción entre dos o más unidades para asegurarse de que esas unidades trabajen juntas con éxito y apropiadamente. Estas pruebas generalmente se realizan de abajo hacia arriba, pero también se pueden realizar de arriba hacia abajo usando simulacros o stubs para representar llamadas o funciones de llamada. Un un punto importante a tener en cuenta es que no debe intentar probar todo junto a la vez (las llamadas pruebas Big Bang), ya que es más difícil aislar los errores para que se puede rectificar. Esta es la razón por la que es más común encontrar que las pruebas de integración tienen se ha realizado en un estilo de abajo hacia arriba. 14.4.3 Pruebas del sistema Las pruebas del sistema tienen como objetivo validar que la combinación de todos los módulos, unidades, datos, instalación, configuración, etc. funciona correctamente y cumple los requisitos especificado para todo el sistema. La prueba de que el sistema tiene un todo generalmente implica probar la mayor parte de la funcionalidad o los comportamientos del sistema. tal comportamiento Las pruebas basadas a menudo involucran a los usuarios finales y otras partes interesadas que son menos tecnológicas. nical Para respaldar tales pruebas, ha evolucionado una variedad de tecnologías que permiten una Estilo inglés para las descripciones de las pruebas. Este estilo de prueba se puede utilizar como parte del proceso de recopilación de requisitos y puede conducir a un desarrollo impulsado por el comportamiento (BDD) proceso. El módulo Python pytest-bdd proporciona una extensión de estilo BDD al marco central de pytest. 14.4 Pruebas de sistemas de software 169
14.4.4 Pruebas de instalación/actualización La prueba de instalación es la prueba de procesos de instalación completos, parciales o de actualización. También valida que el software de instalación y transición necesario para pasar a la nueva liberación para el producto está funcionando correctamente. Típicamente, • verifica que el software se puede desinstalar por completo a través de su restitución proceso. • determina qué archivos se agregan, modifican o eliminan en el hardware en el que se instaló el programa. • determina si algún otro programa en el hardware se ve afectado por el nuevo software que se ha instalado. • determina si el software se instala y funciona correctamente en todo el hardware plataformas y sistemas operativos en los que se supone que funciona. 14.4.5 Pruebas de humo Una prueba de humo es una prueba o conjunto de pruebas diseñadas para verificar que los fundamentos de la trabajo del sistema. Las pruebas de humo se pueden ejecutar contra una nueva implementación o una parcheada implementación para verificar que la instalación funciona lo suficientemente bien como para justificar pruebas adicionales. El no pasar una prueba de humo detendría cualquier otra prueba hasta que el pasan las pruebas de humo. El nombre deriva de los primeros días de la electrónica: si un dispositivo comenzó a humear después de que se encendió, los probadores sabían que no tenía sentido probándolo más. Para las tecnologías de software, las ventajas de realizar humo las pruebas incluyen: • Las pruebas de humo a menudo se automatizan y estandarizan de una construcción a otra. • Porque las pruebas de humo validan cosas que se espera que funcionen, cuando fallan suele ser una indicación de que algo fundamental ha ido mal (el mal se ha utilizado una versión de una biblioteca) o que una nueva compilación ha introducido un error en aspectos fundamentales del sistema. • Si un sistema se construye diariamente, debe probarse el humo diariamente. • Será necesario agregar periódicamente a las pruebas de humo a medida que se presenten nuevas funcionalidades. añadido al sistema. 14.5 Automatización de pruebas La forma real en que se escriben y ejecutan las pruebas necesita una cuidadosa consideración. En general, deseamos automatizar la mayor parte posible del proceso de prueba, ya que esto facilita la ejecución de las pruebas y también garantiza no solo que se ejecuten todas las pruebas, sino que 170 14 Introducción a las pruebas
se ejecutan de la misma manera cada vez. Además, una vez que se configura una prueba automatizada por lo general, será más rápido volver a ejecutar esa prueba automatizada que repetir manualmente una serie de pruebas. Sin embargo, no todas las características de un sistema se pueden probar fácilmente a través de una herramienta de prueba automatizada y, en algunos casos, el entorno físico puede hacer que pruebas difíciles de automatizar. Por lo general, la mayoría de las pruebas unitarias están automatizadas y la mayoría de las pruebas de aceptación son manuales. También tendrá que decidir qué formas de prueba deben llevarse a cabo. La mayoría del software los proyectos deben tener pruebas unitarias, pruebas de integración, pruebas de sistema y aceptación la prueba como requisito necesario. No todos los proyectos implementarán desempeño o prueba de estabilidad, pero debe tener cuidado de no omitir ninguna etapa de prueba y asegúrese de que no es aplicable. 14.6 Desarrollo basado en pruebas Test Driven Development (o TDD) es una técnica de desarrollo mediante la cual el desarrollo Los operadores escriben casos de prueba antes de escribir cualquier código de implementación. Las pruebas así impulsar o dictar el código que se desarrolla. La implementación solo proporciona como tanta funcionalidad como se requiere para pasar la prueba y, por lo tanto, las pruebas actúan como una especificación ficación de lo que hace el código (y algunos argumentan que las pruebas son, por lo tanto, parte de ese especificación y proporcionar documentación de lo que el sistema es capaz de hacer). TDD tiene el beneficio de que como las pruebas deben escribirse primero, siempre hay un conjunto de pruebas disponibles para realizar pruebas unitarias, de integración, de regresión, etc. Esto es bueno como los desarrolladores pueden encontrar que escribir pruebas y mantenerlas es aburrido y de menos interés que el código real en sí mismo y, por lo tanto, poner menos énfasis en el régimen de prueba de lo que podría ser deseable. TDD alienta, y de hecho requiere, que los desarrolladores mantener un conjunto exhaustivo de pruebas repetibles y que esas pruebas se desarrollen para la misma calidad y estándares que el cuerpo principal del código. Hay tres reglas de TDD según lo definido por Robert Martin, estas son:
- No se le permite escribir ningún código de producción a menos que sea para hacer una falla pase de prueba unitaria
- No se le permite escribir más de una prueba unitaria de lo que es suficiente para fallar; y fallas de compilación son fallas
- No está permitido escribir más código de producción del suficiente para aprobar la única prueba unitaria que falla. Esto conduce al ciclo TDD descrito en la siguiente sección. 14.5 Automatización de pruebas 171
14.6.1 El ciclo TDD Hay un ciclo de desarrollo cuando se trabaja de manera TDD. La forma más corta de este ciclo es el mantra TDD: Rojo / Verde / Refactorizar Que se relaciona con el conjunto de herramientas de prueba unitaria donde es posible escribir una unidad prueba. Dentro de herramientas como PyCharm, cuando ejecuta una prueba pyunit o pytest, una Prueba La vista se muestra con rojo que indica que una prueba falló o verde que indica que la prueba aprobado. Por lo tanto, Rojo/Verde, en otras palabras, escriba la prueba y déjela fallar, luego implementar el código para asegurarse de que pasa. La última parte de este mantra es Refactor lo que indica que una vez que lo tenga funcionando, haga que el código sea más limpio, mejor y más ajustado Refactorizándolo. La refactorización es el proceso mediante el cual se modifica el comportamiento del sistema. no se modifica pero se modifica la implementación para mejorarla. El ciclo TDD completo se muestra en el siguiente diagrama que destaca la prueba primer enfoque de TDD: El mantra TDD se puede ver en el ciclo TDD que se muestra arriba y se describe con más detalle a continuación:
- Escriba una sola prueba.
- Ejecute la prueba y vea cómo falla.
- Implemente solo el código suficiente para que la prueba pase.
- Ejecute la prueba y vea cómo pasa.
- Refactorizar para mayor claridad y tratar cualquier problema de reutilización, etc.
- Repita para la siguiente prueba. 172 14 Introducción a las pruebas
14.6.2 Complejidad de la prueba El objetivo es luchar por la simplicidad en todo lo que hace dentro de TDD. Por lo tanto, escribes un prueba que falla, luego haga lo suficiente para que la prueba pase (pero no más). Entonces tú refactorizar el código de implementación (es decir, cambiar las partes internas de la unidad bajo prueba) para mejorar la base de código. Continúe haciendo esto hasta que toda la funcionalidad para un unidad ha sido completada. En términos de cada prueba, debe esforzarse nuevamente por la simplicidad. con cada prueba solo probando una cosa con una sola afirmación por prueba (aunque este es el tema de mucho debate dentro del mundo TDD). 14.6.3 refactorización El énfasis en la refactorización dentro de TDD hace que sea más que solo probar o probar Primer Desarrollo. Este enfoque en la refactorización es realmente un enfoque en el (re)diseño y mejora incremental. Las pruebas proporcionan la especificación de lo que se necesita como así como la verificación de que se mantiene el comportamiento existente, pero la refactorización conduce a un mejor diseño de software. Por lo tanto, sin refactorizar, ¡TDD no es TDD! 14.7 Diseño para la capacidad de prueba La capacidad de prueba tiene varias facetas. • Configurabilidad. Configure el objeto bajo prueba a una configuración apropiada para la prueba • Controlabilidad. Controlar la entrada (y el estado interno) • Observabilidad. Observa su salida • Verificabilidad. Que podamos verificar esa salida de manera adecuada. 14.7.1 Reglas generales de comprobabilidad Si no puede probar el código, cámbielo para que pueda hacerlo. Si su código es difícil de validar, ¡cámbielo para que no lo sea! ¡Solo se debe probar una clase concreta por prueba de unidad y luego simular el resto! Si su código es difícil de reconfigurar para que funcione con Mocks, hágalo para que pueda codificar puede usar simulacros! ¡Diseñe su código para que sea comprobable! 14.6 Desarrollo basado en pruebas 173
14.8 Recursos en línea Consulte los siguientes recursos en línea para obtener más información sobre las pruebas y Test Driven Desarrollo (TDD). • https://www.test-institute.org/Introduction_To_Software_Testing.php Introducción a las Pruebas de Software. • https://en.wikibooks.org/wiki/Introduction_to_Software_Engineering/Testing Introducción al libro wiki de pruebas de software. • https://en.wikipedia.org/wiki/Test-driven_development Test Driven Development- Mención página de wikipedia. • http://agiledata.org/essays/tdd.html una introducción al desarrollo basado en pruebas. • https://medium.freecodecamp.org/learning-to-test-with-python-997ace2d8abe Introducción sencilla a TDD con Python. • http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd Robert Mart- Hay tres reglas para TDD. • http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata La bolera Game Kata que presenta un ejemplo práctico de cómo se puede usar TDD para crear un Aplicación de mantenimiento de puntuación de Ten Pin Bowls. 14.9 Recursos del libro • El arte de las pruebas de software, G. J. Myers, C. Sandler y T. Badgett, John Wiley & Sons, 3.ª edición (diciembre de 2011), 1118031962. 174 14 Introducción a las pruebas
Capítulo 15 Marco de prueba de PyTest 15.1 Introducción Hay varios marcos de prueba disponibles para Python, aunque solo uno, unittest viene como parte de la instalación típica de Python. Las bibliotecas típicas incluyen Prueba unitaria (que está disponible dentro de la distribución de Python de forma predeterminada) y PyTest. En este capítulo veremos PyTest y cómo se puede usar para escribir pruebas unitarias en Python para funciones y clases. 15.2 ¿Qué es PyTest? PyTest es una biblioteca de prueba para Python; actualmente es uno de los Python más populares bibliotecas de prueba (otras incluyen unittest y doctest). PyTest se puede utilizar para varios niveles de prueba, aunque su aplicación más común es como prueba unitaria estructura. También se usa a menudo como un marco de prueba dentro de un TDD basado projecto de desarrollo. De hecho, es utilizado por Mozilla y Dropbox como su Python marco de prueba. PyTest ofrece una gran cantidad de funciones y una gran flexibilidad en la forma en que se realizan las pruebas. escrito y en cómo se define el comportamiento establecido. Encuentra automáticamente la prueba basada en convenciones de nomenclatura y se puede integrar fácilmente en una variedad de editores e IDE incluyendo PyCharm. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_15 175
15.3 Configuración de PyTest Probablemente necesitará configurar PyTest para poder usarlo desde dentro de su ambiente. Si está utilizando el editor PyCharm, deberá agregar el Módulo PyTest al proyecto PyCharm actual y dígale a PyCharm que desea use PyTest para ejecutar todas las pruebas por usted. 15.4 Un ejemplo simple de PyTest Algo para probar Para poder explorar PyTest, primero necesitamos probar algo; nosotros por lo tanto defina una clase Calculadora simple. La calculadora mantiene un total acumulado de los operaciones realizadas; permite establecer un nuevo valor y luego este valor puede ser sumado a, o restado de, ese total acumulado. def init(uno mismo): auto.actual = 0 propio.total = 0 def set(auto, valor): self.actual = valor def añadir (auto): propio.total += propio.actual def sub(uno mismo): self.total -= self.actual def total(auto): devolver self.total Calculadora de clase: Guarde esta clase en un archivo llamado calculadora.py. escribir una prueba Ahora crearemos una prueba de unidad PyTest muy simple para nuestra clase Calculadora. Este test se definirá en una clase llamada test_calculator.py. Deberá importar la clase de calculadora que escribimos anteriormente en su archivo test_calculator.py (recuerde que cada archivo es un módulo en Python). 176 15 Marco de prueba de PyTest
La declaración de importación exacta dependerá de dónde colocó el archivo de la calculadora en relación con la clase de prueba. En este caso, los dos archivos están en el mismo directorio y por lo que podemos escribir: desde calculadora importar Calculadora Ahora definiremos una prueba, la prueba debe tener como prefijo test_ para que PyTest encontrarlos. De hecho, PyTest usa varias convenciones para encontrar pruebas, que son: • Busque archivos test_*.py o *test.py. • De esos archivos, recopile los elementos de prueba: – funciones de prueba test_prefixed, – métodos de prueba test_prefixed dentro de Test clases de prueba prefijadas (sin un__init__método). Tenga en cuenta que mantenemos separados los archivos de prueba y los archivos que contienen el código que se va a probar; de hecho, en muchos casos se mantienen en diferentes estructuras de directorios. Esto significa que no hay posibilidad de que los desarrolladores usen accidentalmente pruebas en el código de producción, etc. Ahora agregaremos al archivo una función que defina una prueba. llamaremos al función test_add_one; debe comenzar con test debido a la con- vención Sin embargo, hemos intentado que el resto del nombre de la función sea descriptivo, para que quede claro lo que está probando. La definición de la función se da a continuación: desde calculadora importar Calculadora def test_add_one(): calc = Calculadora() calc.set(1) calc.add() afirmar calc.total == 1 La función de prueba crea una nueva instancia de la clase Calculadora y luego llama a varios métodos en él; para configurar el valor a agregar, luego la llamada a add() método en sí, etc. La parte final de la prueba es la afirmación. La afirmación verifica que el comportamiento de la calculadora es como se esperaba. La declaración de afirmación de PyTest resuelve lo que es que se está probando y qué debe hacer con el resultado, incluida la adición de información a agregarse a un informe de ejecución de prueba. Evita la necesidad de tener que aprender un montón de afirmar métodos de tipo de algo (a diferencia de otros marcos de prueba). Tenga en cuenta que una prueba sin una afirmación no es una prueba; es decir, no prueba nada. Muchos IDE brindan soporte directo para marcos de prueba, incluido PyCharm. Por ejemplo, PyCharm ahora detectará que ha escrito una función con un afirmar declaración en él y agregar un icono de ejecución de prueba en el área gris a la izquierda de la 15.4 Un ejemplo simple de PyTest 177
editor. Esto se puede ver en la siguiente imagen donde se ha marcado una flecha verde. añadido en la línea 4; este es el botón ‘Ejecutar prueba’: El desarrollador puede hacer clic en la flecha verde para ejecutar la prueba. entonces serán presentado con el menú Ejecutar que está preconfigurado para usar PyTest para usted: Si el desarrollador ahora selecciona la opción Ejecutar; esto usará el corredor PyTest para ejecutar la prueba y recopilar información sobre lo sucedido y presentarla en un Vista de salida de PyTest en la parte inferior del IDE: Aquí puede ver un árbol en el panel de la izquierda que actualmente contiene la prueba definido en el archivo test_calculator.py. Este árbol muestra si las pruebas tienen pasó o falló. En este caso, tenemos una marca verde que muestra que la prueba pasó. A la derecha de este árbol se encuentra el panel de salida principal que muestra los resultados de ejecutando las pruebas. En este caso, muestra que PyTest ejecutó solo una prueba y que esta fue la prueba test_add_one que se definió en test_calculator.py y que 1 prueba superada. Si ahora cambia la aserción en la prueba para comprobar que el resultado es 0, el la prueba fallará. Cuando se ejecuta, la pantalla IDE se actualizará en consecuencia. El árbol en el panel de la izquierda ahora muestra la prueba como fallida mientras que el de la derecha El panel proporciona información detallada sobre la prueba que falló, incluido en qué parte del prueba se definió la aserción fallida. Esto es muy útil cuando se intenta depurar la prueba fallas 178 15 Marco de prueba de PyTest
15.5 Trabajando con PyTest Funciones de prueba Podemos probar funciones independientes, así como clases usando PyTest. incremento de la función a continuación (que simplemente agrega uno a cualquier número que se le pase): incremento def(x): volver x + 1 Podemos escribir una prueba PyTest para esto de la siguiente manera: def test_increment_integer_3(): afirmar incremento(3) == 4 La única diferencia real es que no hemos tenido que crear una instancia de una clase: Organización de pruebas Las pruebas se pueden agrupar en uno o más archivos; PyTest buscará todos los archivos siguiendo la convención de nomenclatura (nombres de archivo que comienzan o terminan con ‘prueba’) en lugares especificados: • Si no se especifican argumentos cuando se ejecuta PyTest, la búsqueda de Los archivos de prueba con nombre comienzan desde la variable de entorno testpaths (si está configurada). ured) o el directorio actual. Alternativamente, los argumentos de la línea de comando pueden ser utilizado en cualquier combinación de directorios o nombres de archivos, etc. 15.5 Trabajando con PyTest 179
• PyTest buscará recursivamente en subdirectorios, a menos que coincide con la variable de entorno norecursedirs. • En esos directorios, buscará archivos que coincidan con la convención de nomenclatura. ciones test_*.py o *_test.py archivos. Las pruebas también se pueden organizar dentro de los archivos de prueba en clases de prueba. Uso de clases de prueba puede ser útil para agrupar pruebas y administrar la configuración y el desmontaje comportamientos de grupos separados de pruebas. Sin embargo, el mismo efecto se puede lograr con separar las pruebas relacionadas con diferentes funciones o clases en diferentes archivos. Accesorios de prueba No es raro necesitar ejecutar algún comportamiento antes o después de cada prueba o de hecho antes o después de un grupo de pruebas. Tales comportamientos se definen dentro de lo que es comúnmente conocidos como accesorios de prueba. Podemos agregar código específico para ejecutar: • al principio y al final de un módulo de clase de prueba de código de prueba (setup_module/ desmontaje_módulo) • al principio y al final de una clase de prueba (setup_class/teardown_class) o usando el estilo alternativo de los accesorios de nivel de clase (montaje/desmontaje) • antes y después de una llamada de función de prueba (setup_function/teardown_function) • antes y después de una llamada de método de prueba (setup_method/teardown_method) Para ilustrar por qué podríamos usar un accesorio, ampliemos nuestra prueba de la Calculadora: def prueba_valor_inicial(): calc = Calculadora() afirmar calc.total == 0 def test_add_one(): calc = Calculadora() calc.set(1) calc.add() afirmar calc.total == 1 def test_subtract_one(): calc = Calculadora() calc.set(1) calc.sub() afirmar calc.total == -1 def test_add_one_and_one(): calc = Calculadora() calc.set(1) calc.add() calc.set(1) calc.add() afirmar calc.total == 2 180 15 Marco de prueba de PyTest
Ahora tenemos cuatro pruebas para ejecutar (podríamos ir más lejos, pero esto es suficiente por ahora). Uno de los problemas con este conjunto de pruebas es que hemos repetido la creación del Objeto calculadora al inicio de cada prueba. Si bien esto no es un problema en sí mismo, da como resultado un código duplicado y la posibilidad de futuros problemas en términos de mantenimiento si queremos cambiar la forma en que se crea una calculadora. También puede no ser tan eficiente como reutilizar el objeto Calculadora para cada prueba. Sin embargo, podemos definir un accesorio que se puede ejecutar antes de cada prueba individual se ejecuta la función. Para hacer esto escribiremos una nueva función y usaremos el pytest.fixture decorador en esa función. Esto marca la función como siendo especial y que se puede utilizar como accesorio en una función individual. Las funciones que requieren el accesorio deben aceptar una referencia al accesorio como un argumento a la función de prueba individual. Por ejemplo, para que una prueba acepte un accesorio llamada calculadora; debe tener un argumento con el nombre del accesorio, es decir calculadora. Este nombre se puede usar para acceder al objeto devuelto. Esto es ilustrado a continuación: importar pytest desde calculadora importar Calculadora @pytest.fixture calculadora de definición(): “““Devuelve una instancia de Calculadora””” devolver Calculadora() def prueba_valor_inicial(calculadora): afirmar calculadora.total == 0 def test_add_one(calculadora): calculadora.set(1) calculadora.add() afirmar calculadora.total == 1 def test_subtract_one(calculadora): calculadora.set(1) calculadora.sub() afirmar calculadora.total == -1 def test_add_one_and_one(calculadora): calculadora.set(1) calculadora.add() calculadora.set(1) calculadora.add() afirmar calculadora.total == 2 15.5 Trabajando con PyTest 181
En el código anterior, cada una de las funciones de prueba acepta el accesorio de la calculadora que se utiliza para instanciar el objeto Calculadora. Por lo tanto, hemos desduplicado cated nuestro código; ahora solo hay una pieza de código que define cómo una calculadora El objeto debe ser creado para nuestras pruebas. Tenga en cuenta que cada prueba se suministra con un nueva instancia del objeto Calculadora; por lo tanto, no hay posibilidad de una prueba repercutiendo en otra prueba. También se considera una buena práctica agregar una cadena de documentos a sus accesorios, ya que hemos hecho arriba. Esto se debe a que PyTest puede producir una lista de todos los accesorios disponibles a lo largo con sus cadenas de documentación. Desde la línea de comando esto se hace usando:
accesorios pytest Los accesorios PyTest se pueden aplicar a funciones (como arriba), clases, módulos, paquetes o sesiones. El alcance de un accesorio se puede indicar a través de la (opcional) parámetro de alcance al decorador de accesorios. El valor predeterminado es “función”, por lo que no necesitamos especificar nada arriba. El alcance determina en qué punto un se debe ejecutar el dispositivo. Por ejemplo, un accesorio con alcance de ‘sesión’ se ejecutará una vez para la sesión de prueba, se ejecutará un accesorio con alcance de módulo una vez para el módulo (ese es el accesorio y cualquier cosa que genere se compartirá en todas las pruebas en el módulo actual), un accesorio con ámbito de clase indica un accesorio que se ejecuta para cada nueva instancia de una clase de prueba creada, etc. Otro parámetro para el decorador de accesorios es el uso automático, que si se establece en Verdadero active el dispositivo para todas las pruebas que puedan verlo. se requiere una referencia explícita en una función de prueba (o método, etc.) para activar el dispositivo. Si agregamos algunos accesorios adicionales a nuestras pruebas, podemos ver cuándo se ejecutan: @pytest.fixture calculadora de definición(): “““Devuelve una instancia de Calculadora””” print(‘accesorio de calculadora’) devolver Calculadora() importar pytest desde calculadora importar Calculadora es) @pytest.fixture(alcance=‘sesión’, uso automático=Verdadero) def session_scope_fixture(): imprimir(‘session_scope_fixture’) @pytest.fixture(scope=‘módulo’, autouse=True) @pytest.fixture(scope=‘clase’, autouse=Tr def class_scope_fixture(): imprimir (‘class_scope_fixture’) def module_scope_fixture(): imprimir (‘module_scope_fixture’) 182 15 Marco de prueba de PyTest
Si ejecutamos esta versión de las pruebas, entonces la salida muestra cuando los diversos se ejecutan los accesorios: session_scope_fixture module_scope_fixture class_scope_fixture accesorio de calculadora .class_scope_fixture accesorio de calculadora .class_scope_fixture accesorio de calculadora .class_scope_fixture accesorio de calculadora Tenga en cuenta que los accesorios de mayor alcance se instancian primero. 15.6 Pruebas parametrizadas Un requisito común de una prueba para ejecutar las mismas pruebas varias veces con varios diferentes valores de entrada. Esto puede reducir en gran medida el número de pruebas que deben realizarse. definido. Estas pruebas se denominan pruebas parametrizadas; con los valores de los parámetros para la prueba especificada usando el decorador @pytest.mark.parametrize. def prueba_valor_inicial(calculadora): afirmar calculadora.total == 0 def test_add_one(calculadora): calculadora.set(1) calculadora.add() afirmar calculadora.total == 1 def test_subtract_one(calculadora): calculadora.set(1) calculadora.sub() afirmar calculadora.total == -1 def test_add_one_and_one(calculadora): calculadora.set(1) calculadora.add() calculadora.set(1) calculadora.add() afirmar calculadora.total == 2 15.5 Trabajando con PyTest 183
Esto ilustra la configuración de una prueba parametrizada para la Calculadora en la que dos los valores de entrada se suman y se comparan con el resultado esperado. Nota que los parámetros se nombran en el decorador y luego se usa una lista de tuplas para defina los valores que se utilizarán para los parámetros. En este caso la prueba_ calculadora_añadir_operación se ejecutará dos pasando en 3, 1 y 4 y luego pasando 3, 2 y 5 para los parámetros entrada1, entrada2 y esperado respectivamente. Prueba de excepciones Puede escribir pruebas que verifiquen que se generó una excepción. Esto es útil como prueba el comportamiento negativo es tan importante como probar el comportamiento positivo. por ejemplo, nosotros Es posible que desee verificar que se genera una excepción particular cuando intentamos retirar dinero de una cuenta bancaria que nos llevará por encima de nuestro límite de sobregiro. Para verificar la presencia de una excepción en PyTest, use la instrucción with y pytest.raises. Este es un administrador de contexto que verificará al salir que el se generó la excepción especificada. Se utiliza de la siguiente manera: con pytest.raises(cuentas.BalanceError): cuenta_actual.retirar(200.0) Ignorando las pruebas En algunos casos, es útil escribir una prueba de funcionalidad que aún no se ha implementado; esto puede ser para asegurarse de que la prueba no se olvide o porque ayuda para documentar lo que debe hacer el elemento bajo prueba. Sin embargo, si se ejecuta la prueba, el conjunto de pruebas fallará porque la prueba se está ejecutando contra un comportamiento que tiene aún por escribir. @pytest.mark.parametrize decorador. @pytest.mark.parametrize(’entrada1,entrada2,esperado’, [ (3, 1, 4), (3, 2, 5), ]) def test_calculator_add_operation(calculadora, entrada1, entrada2, esperada): calculadora.set(entrada1) calculadora.add() calculadora.set(entrada2) calculadora.add() afirmar calculadora.total == esperado 184 15 Marco de prueba de PyTest
Una forma de abordar este problema es decorar una prueba con @pytest.- mark.skip decorador: @pytest.mark.skip(motivo=‘aún no implementado’) def test_calculator_multiply(calculadora): calculadora.multiplicar(2, 3) afirmar calculadora.total == 6 Esto indica que PyTest debe registrar la presencia de la prueba pero no debe intente ejecutarlo. PyTest notará que la prueba se omitió, por ejemplo en PyCharm esto se muestra usando un círculo con una línea que lo atraviesa. En general, se considera una buena práctica proporcionar una razón por la que se ha realizado la prueba. omitido para que sea más fácil de rastrear. Esta información también está disponible cuando PyTest se salta la prueba: 15.7 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre PyTest: • http://pythontesting.net/framework/PyTest/PyTest-introduction/PyTest introducción. • https://github.com/pluralsight/intro-to-PyTest Una introducción basada en ejemplos a PyTest. • https://docs.pytest.org/en/latest/Página de inicio de PyTest. • https://docs.pytest.org/en/latest/#documentation Documentación de PyTest. 15.8 Ejercicios Cree una clase Calculadora simple que se pueda usar con fines de prueba. Este calculadora simple se puede utilizar para sumar, restar, multiplicar y dividir números. 15.6 Pruebas parametrizadas 185
Esta será una aplicación puramente impulsada por comandos que permitirá al usuario especificar • la operación a realizar y • los dos números a utilizar con esa operación. El objeto Calculadora luego devolverá un resultado. Se puede usar el mismo objeto para repetir esta secuencia de pasos. Este comportamiento general de la Calculadora es se ilustra a continuación en forma de diagrama de flujo: También debe proporcionar una función de memoria que permita que el resultado actual sea agregado o restado del total actual de la memoria. También debería ser posible recuperar el valor en la memoria y borrar la memoria. A continuación, escriba un conjunto de pruebas PyTest para la clase Calculadora. Piense en qué pruebas necesita escribir; recuerda que no puedes escribir pruebas para cada valor que podría usarse para una operación; pero considere los límites, 0, −1, 1, −10, +10, etc Por supuesto, también debe considerar el efecto acumulativo del comportamiento del función de memoria de la calculadora; es decir, múltiples adiciones de memoria o sub-memoria tracciones y combinaciones de estas. A medida que identifique las pruebas, es posible que tenga que actualizar su implementación. de la clase Calculadora. ¿Ha tenido en cuenta todas las opciones de entrada, por ejemplo dividir por cero: lo que debería suceder en estas situaciones. 186 15 Marco de prueba de PyTest
capitulo 16 Burlarse para probar 16.1 Introducción Probar sistemas de software no es algo fácil de hacer; las funciones, objetos, métodos, etc. Los que están involucrados en cualquier programa pueden ser cosas complejas por derecho propio. En muchos casos de los que dependen e interactúan con otras funciones, métodos y objetos; muy pocos funciones y métodos operan de forma aislada. Así, el éxito o el fracaso de una función o método o el estado general de un objeto depende de otros elementos del programa. Sin embargo, en general es mucho más fácil probar una sola unidad de forma aislada que probarlo como parte de un sistema más grande y complejo. Por ejemplo, tomemos un Python clase como una sola unidad para ser probada. Si podemos probar esta clase por sí sola, solo tenemos que tener en cuenta el estado del objeto de clases y el comportamiento definido para el clase al escribir nuestra prueba y determinar los resultados apropiados. Sin embargo, si esa clase interactúa con sistemas externos como servicios externos, bases de datos, software de terceros, fuentes de datos, etc. Entonces el proceso de prueba se vuelve mas complejo: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_16 187
Ahora puede ser necesario verificar las actualizaciones de datos realizadas en la base de datos, o la información enviado a un servicio remoto, etc. para confirmar que la operación del objeto de una clase es correcto. Esto hace que no solo el software que se está probando sea más complejo, sino que también hace que las pruebas en sí sean más complejas. Esto significa que hay una mayor probabilidad que la prueba fallará, que las pruebas contendrán errores o problemas y que la prueba será más difícil de entender y mantener para alguien. Así un común El objetivo al escribir pruebas unitarias o pruebas de subsistemas es poder probar elementos/ unidades en aislamiento. La pregunta es cómo hacer esto cuando una función o método se basa en otros ¿elementos? La clave para desacoplar funciones, métodos y objetos de otro programa o elementos del sistema es usar simulacros. Estos simulacros se pueden usar para desacoplar un objeto de otra, una función de otra y un sistema de otro; de este modo simplificar el entorno de prueba. Estos simulacros solo están destinados a ser utilizados para propósitos de prueba, por ejemplo, el escenario anterior podría simplificarse burlándose cada uno de los sistemas externos como se muestra a continuación: La simulación no es un concepto específico de Python y hay muchas bibliotecas de simulación disponible para muchos idiomas diferentes. Sin embargo, en este capítulo nos centraremos en la biblioteca unes.mock que ha sido parte de la distribución estándar de Python desde Python 3.3. 16.2 ¿Por qué burlarse? Una primera pregunta útil a considerar con respecto a la burla, en las pruebas de software, es ‘¿Por qué burlarse?’. Es decir, ¿por qué molestarse con el concepto de un simulacro en primer lugar; ¿Por qué no probar con la cosa real? Hay varias respuestas a esto, algunas de las cuales se discuten a continuación: 188 dieciséis Burlarse para probar
La prueba aislada es más fácil. Como se mencionó en la introducción, probar una unidad (ya sea una clase, una función, un módulo, etc.) es más fácil de forma aislada que cuando depende de clases externas, funciones, módulos, etc. La cosa real no está disponible. En muchos casos es necesario simular parte de un sistema o una interfaz a otro sistema porque lo real simplemente no es disponible. Esto podría deberse a varias razones, entre ellas, que no se ha desarrollado todavía. En el curso natural del desarrollo de software, es probable que algunas partes de un sistema para ser desarrollado y listo para probar antes que otras partes. Si una parte depende de otra parte para algún elemento de su operación, entonces el sistema que aún no está disponible puede ser burlado. En otras situaciones, el equipo de desarrollo o el equipo de prueba pueden no tener acceso a lo real. Esto puede deberse a que solo está disponible dentro de una producción. contexto. Por ejemplo, si una casa de desarrollo de software está desarrollando un subsistema puede que no tenga acceso a otro subsistema ya que es propietario y solo accesible una vez que el software se ha implementado dentro de la organización del cliente. Los elementos reales pueden llevar mucho tiempo. Queremos que nuestras pruebas se ejecuten tan rápido como posible y ciertamente dentro de un entorno de integración continua (CI) queremos para que se ejecuten lo suficientemente rápido como para que podamos probar repetidamente un sistema a lo largo del día. En algunas situaciones, la cosa real puede tomar una cantidad significativa de tiempo para procesar la escenario de prueba. Como queremos probar nuestro propio código, es posible que no nos preocupe si un sistema fuera de nuestro control funciona correctamente o no (al menos en este nivel de prueba; todavía puede ser una preocupación para la integración y las pruebas del sistema). Podemos por lo tanto mejorar los tiempos de respuesta de nuestras pruebas si nos burlamos del sistema real y reemplácelo con un simulacro que proporciona tiempos de respuesta mucho más rápidos (posiblemente porque utiliza respuestas enlatadas). El real cosa acepta tiempo a colocar arriba. En a Continuo Integración (CI), las nuevas construcciones de un sistema se prueban regular y repetidamente (por ejemplo cada vez que se realiza un cambio en su base de código). En tales situaciones, puede ser necesario para configurar e implementar el sistema final en un entorno adecuado para realizar las pruebas correspondientes. Si la configuración de un sistema externo requiere mucho tiempo, implementarlo e inicializarlo puede ser más efectivo para simular ese sistema. Difícil emular ciertas situaciones. Puede ser difícil dentro de un escenario de prueba para emular situaciones específicas. Estas situaciones suelen estar relacionadas con errores o excepciones. circunstancias cionales que nunca deberían ocurrir dentro de un funcionamiento correcto ambiente. Sin embargo, bien puede ser necesario validar que si tal situación ocurre, entonces el software puede manejar ese escenario. Si estos escáneres son relacionado con cómo el sistema externo (la unidad bajo prueba) falla o funciona incorrectamente, entonces puede ser necesario simular estos sistemas para poder generar los escenarios. Queremos pruebas repetibles. Por su propia naturaleza, cuando ejecuta una prueba, usted desea que pase o falle cada vez que se ejecute con las mismas entradas. ciertamente no quieren pruebas que pasen a veces y fallen otras veces. Esto significa que no hay confianza en las pruebas y las personas a menudo comienzan a ignorar las pruebas fallidas. Esta situación puede 16.2 ¿Por qué burlarse? 189
ocurrir si los datos proporcionados por los sistemas de los que depende una prueba no proporcionan datos repetibles. Esto puede suceder por varias razones diferentes, pero una causa común es porque devuelven datos reales. Dichos datos reales pueden estar sujetos a cambios, por ejemplo Considere un sistema que utilice una fuente de datos para el tipo de cambio actual entre fondos. y dólares Si la prueba asociada confirma que una operación cotizada en dólares es convertido correctamente a fondos utilizando el tipo de cambio actual, entonces esa prueba es probable para generar un resultado diferente cada vez que se ejecuta. En esta situación mentiría mejor para simular el servicio de tipo de cambio actual para que un tipo de cambio fijo/conocido sea usado. El Sistema Real no es lo suficientemente confiable. En algunos casos, el sistema real puede no ser lo suficientemente confiable como para permitir pruebas repetibles. El Sistema Real puede no permitir que se repitan las pruebas. Finalmente, el sistema real puede no permitir que las pruebas se repitan fácilmente. Por ejemplo, una prueba que involucre presentación de una transacción por un cierto número de acciones de IBM con una orden de negociación man- el sistema de gestión puede no permitir que ese comercio, con esas acciones, para ese cliente sea ejecutar varias veces (ya que entonces parecería ser múltiples intercambios). Sin embargo, para el propósitos de prueba, es posible que deseemos probar la presentación de tal comercio en multipel diferentes escenarios, varias veces. Por lo tanto, puede ser necesario burlarse de la Orden real. Sistema de Gestión para que dichas pruebas puedan ser escritas. 16.3 ¿Qué es burlarse? La sección anterior dio varias razones para usar simulacros; lo siguiente a considerar entonces es lo que es un simulacro? Los simulacros, incluidas las funciones simuladas, los métodos y los objetos simulados, son cosas que: • Poseer la misma interfaz que la real, ya sean funciones simuladas, métodos u objetos completos. Por lo tanto, toman la misma gama y tipos de parámetros y devolver información similar usando tipos similares. • Definir un comportamiento que de alguna manera represente/imite un comportamiento ejemplar real pero típicamente de maneras muy controladas. Este comportamiento puede ser coed duro, puede realmente en un conjunto de reglas o comportamiento simplificado; puede ser muy simplista o callado sofisticado por derecho propio. Por lo tanto, emulan el sistema real y, desde fuera del simulacro, en realidad pueden parece ser el sistema real. En muchos casos, el término simulacro se usa para cubrir una variedad de formas diferentes en las que se puede emular lo real; cada tipo de simulacro tiene sus propias características. Es por lo tanto, es útil distinguir los diferentes tipos de simulacros, ya que esto puede ayudar a determinar minar el estilo de simulación que se adoptará en una situación de prueba particular. 190 dieciséis Burlarse para probar
Hay diferentes tipos de simulacros que incluyen: • Talones de prueba. Un talón de prueba suele ser una función, método u objeto codificado a mano. utilizado para propósitos de prueba. El comportamiento implementado por un trozo de prueba puede representar resentir un subconjunto limitado de la funcionalidad de la cosa real. • Falsificaciones. Las falsificaciones suelen proporcionar funcionalidad adicional en comparación con una prueba Talón. Las falsificaciones se pueden considerar como una versión específica de prueba de la cosa real, como una base de datos en memoria utilizada para pruebas en lugar de la base de datos real. Estas falsificaciones suelen tener algunas limitaciones en su funcionalidad, por ejemplo ejemplo, cuando finalizan las pruebas, todos los datos se eliminan de la memoria base de datos en lugar de almacenarse permanentemente en el disco. • Simulacros de prueba generados automáticamente. Por lo general, se generan automáticamente usando un marco de apoyo. Como parte del montaje de la prueba, las expectativas asociado con el simulacro de prueba. Estas expectativas pueden especificar los resultados a retorno para entradas específicas, así como si se llamó al simulacro de prueba, etc. • Prueba Simulacro de Espía. Si estamos probando una unidad en particular y devuelve el resultado correcto podríamos decidir que no necesitamos considerar el comportamiento interno del unidad. Sin embargo, es común querer confirmar que se invocó el simulacro de prueba en el ay que esperábamos. Esto ayuda a verificar el comportamiento interno de la unidad bajo prueba. Esto se puede hacer usando un espía simulado de prueba. Tal simulacro de prueba registra cómo Cuántas veces se llamó y qué parámetros usó dónde (así como otros información). Luego, la prueba puede interrogar al simulacro de prueba para validar que fue invocado como se esperaba/tantas veces como se esperaba/con los parámetros correctos, etc. 16.4 Conceptos comunes de marco de simulación Como se ha mencionado, existen varios marcos de simulación no solo para Python pero otros lenguajes como Java, C# y Scala, etc. Todos estos marcos tienen un comportamiento central común. Este comportamiento permite que una función, método o objeto que se creará en función de la interfaz presentada por la cosa real. Por supuesto a diferencia de lenguajes como C# y Java Python no tiene una interfaz formal concepto; sin embargo, esto no impide que el marco de burla siga usando el la misma idea. En general, una vez que se ha creado un simulacro, es posible definir cómo ese simulacro debe parecer comportarse; en general, esto implica especificar el resultado de retorno a utilizar para una función o método. También es posible verificar que el simulacro ha sido invocado como se esperaba con los parámetros esperados. El simulacro real se puede agregar a una prueba o a un conjunto de pruebas mediante programación oa través de algún tipo de decorador. En cualquier caso, durante la duración de la prueba, el simulacro se utilizará en lugar de la cosa real. 16.3 ¿Qué es burlarse? 191
Las aserciones se pueden usar para verificar los resultados devueltos por la unidad bajo prueba. mientras que los métodos específicos simulados se utilizan típicamente para verificar (espiar) los métodos definido en el simulacro. 16.5 Simulación de marcos para Python Debido a la naturaleza dinámica de Python, se adapta bien a la construcción de simulacros. funciones, métodos y objetos. De hecho, hay varias burlas ampliamente utilizadas. Marcos disponibles para Python que incluyen: • unittest.mock El unittest.mock (incluido en la distribución de Python desde Python 3.3 en adelante). Esta es la biblioteca de simulación predeterminada provista con Python para crear objetos simulados en las pruebas de Python. • pymox Este es un marco de creación ampliamente utilizado. Es un marco de código abierto. trabajo y tiene un conjunto más completo de instalaciones para hacer cumplir la interfaz de un clase burlada. • Mocktest Este es otro marco de simulación popular. Tiene su propio DSL (Lenguaje Específico del Dominio) para soportar burlas y un amplio conjunto de expectativas Comportamiento coincidente para objetos simulados. En el resto de este capítulo nos centraremos en la biblioteca unittest.mock, ya que se proporciona como parte de la distribución estándar de Python. 16.6 La biblioteca unittest.mock La biblioteca de simulación estándar de Python es la biblioteca unittest.mock. Ha sido incluido en la distribución estándar de Python desde Python 3.3 y proporciona un forma de definir simulacros para pruebas unitarias. La clave de la biblioteca unittest.mock es la clase Mock y su subclase MágicoMock. Los objetos Mock y MagicMock se pueden usar para simular funciones, métodos e incluso clases enteras. Estos objetos simulados pueden tener respuestas enlatadas definidos de modo que cuando estén involucrados por la unidad bajo prueba responderán adecuadamente. Los objetos existentes también pueden tener atributos o métodos individuales. burlado que permite probar un objeto con un estado conocido y un comportamiento específico. Para facilitar el trabajo con objetos simulados, la biblioteca proporciona la @unittest.mock.patch() decorador. Este decorador se puede utilizar para reemplazar funciones y objetos reales con instancias simuladas. La función detrás del decorador también se puede usar como un administrador de contexto, lo que permite su uso con-como estado- mentos que proporcionan un control detallado sobre el alcance del simulacro si es necesario. 192 dieciséis Burlarse para probar
16.6.1 Clases de Mock y Magic Mock La biblioteca unittest.mock proporciona la clase Mock y MagicMock clase. La clase Mock es la clase base para los objetos simulados. La clase MagicMock es una subclase de la clase Mock. Se llama la clase MagicMock ya que proporciona implementaciones predeterminadas para varios métodos mágicos como .len(), . str() y .iter(). Como un ejemplo simple, considere la siguiente clase a probar: clase AlgunaClase(): def _método_oculto(uno mismo): volver 0 def public_method(self, x): devuelve self.método_oculto() + x Esta clase define dos métodos; uno está pensado como parte de la interfaz pública del clase (el método_público()) y otra destinada solo para uso interno o privado (el método_oculto()). Observe que el método oculto utiliza la convención de precediendo su nombre por un guión bajo (’’). Supongamos que deseamos probar el comportamiento de public_method() y quiero simular el _hidden_method(). Podemos hacer esto escribiendo una prueba que creará un objeto simulado y lo usará en lugar del verdadero _hidden_method(). Probablemente podríamos usar el Mock class o la clase MagicMock para esto; sin embargo, debido a la funcionalidad adicional proporcionado por la clase MagicMock, es una práctica común usar esa clase. Lo haremos pues haz lo mismo. La prueba a crear se definirá dentro de un método dentro de una clase de prueba. El Los nombres del método de prueba y la clase de prueba son por convención descriptivos y por lo tanto describirá lo que se está probando, por ejemplo: de unittest.importación simulada * de unittest importar TestCase de unittest importación principal clase test_SomeClass_public_interface(TestCase): def test_public_method(self): objeto_prueba = AlgunaClase()
Configure la respuesta enlatada en el método simulado
test_object._hidden_method = MagicMock(nombre = ‘método_oculto’) test_object._hidden_method.return_value = 10
Probar el objeto
resultado = test_object.public_method(5) self.assertEqual(15, resultado, ‘valor de retorno de public_method incorrecto’) 16.6 La biblioteca unittest.mock 193
En este caso, tenga en cuenta que primero se crea una instancia de la clase que se está probando. El MagicMock es luego instanciado y asignado al nombre del método para ser simulado. Esta en El efecto reemplaza ese método por test_object. El MagicMock. El El objeto MagicMock recibe un nombre, ya que esto ayuda a tratar cualquier problema en el informe generado por el marco une. Después de esto, la respuesta enlatada de se define la versión simulada de _hidden_method(); siempre devolverá el valor 10. En este punto, hemos configurado el simulacro que se usará para la prueba y ahora estamos listos para ejecutar la prueba. Esto se hace en la siguiente línea donde se llama a public_method() en test_object con el parámetro 5. Luego se almacena el resultado. Luego, la prueba valida el resultado para garantizar que sea correcto; es decir, que el devuelto el valor es 15 Aunque este es un ejemplo muy simple, ilustra cómo un método puede ser burlado usando la clase MagicMock. 16.6.2 los parcheadores unittest.mock.patch(), unittest.mock.patch.object() y Los decoradores unittest.patch.dict() se pueden usar para simplificar la creación de objetos simulados. • El decorador de parches toma un objetivo para el parche y devuelve un MagicMock objeto en su lugar. Se puede utilizar como método TastCase o como decorador de clases. Como un decorador de clase, decora automáticamente cada método de prueba en la clase. Puede también se puede usar como administrador de contexto a través de las declaraciones with y with-as. • El decorador patch.object se puede proporcionar con dos o tres argumentos Cuando se le den tres argumentos, reemplazará el objeto a parchear, con un simulacro para el atributo/nombre de método dado. Cuando se dan dos argumentos el objeto a parchear recibe un objeto MagicMock predeterminado para el objeto especificado atributo/función. • El decorador patch.dict parchea un diccionario o un objeto similar a un diccionario. Por ejemplo, podemos reescribir el ejemplo presentado en la sección anterior usando el decorador @patch.object para proporcionar el objeto simulado para el _hid- den_method() (devuelve un MagicMock vinculado a SomeClass): 194 dieciséis Burlarse para probar
En el código anterior, _hidden_method() se reemplaza con una versión simulada para SomeClass dentro del método test_public_method(). Tenga en cuenta que el simulacro versión del método se pasa como un parámetro al método de prueba para que el se puede especificar una respuesta enlatada. También puede usar el decorador @patch() para simular una función de un módulo. Por ejemplo, dado algún módulo externo con una función api_call, podemos simule esa función usando el decorador @patch(): @patch(’external_module.api_call’) def test_some_func(self, mock_api_call): Esto usa patch() como decorador y pasa la ruta del objeto de destino. El objetivo la ruta era ’external_module.api_call’, que consiste en el nombre del módulo y el Función para burlarse. 16.6.3 Burlarse de objetos devueltos En los ejemplos vistos hasta ahora, los resultados devueltos por las funciones simuladas o Los métodos han sido enteros simples. Sin embargo, en algunos casos los valores devueltos deben burlarse de sí mismos ya que el sistema real devolvería un objeto complejo con múltiples atributos y métodos. El siguiente ejemplo usa un objeto MagicMock para representar un objeto devuelto de una función simulada. Este objeto tiene dos atributos, uno es una respuesta código y el otro es una cadena JSON. JSON significa la notación de objetos de JavaScript y es un formato de uso común en los servicios web. clase test_SomeClass_public_interface(TestCase): @parche.objeto(AlgunaClase, ‘_método_oculto’) def test_public_method(self, mock_method):
Configurar respuesta enlatada
método_simulado.return_value = 10
Crear objeto para ser probado
objeto_prueba = AlgunaClase() resultado = test_object.public_method(5) self.assertEqual(15, resultado, ‘valor de retorno de public_method incorrecto’) 16.6 La biblioteca unittest.mock 195
En este ejemplo, la función que se está probando es some_func() pero some_func() llamadas afuera a el burlado función módulo_externo.api_call(). Esta función simulada devuelve un objeto MagicMock con un objeto preespecificado código_estado y respuesta. Las aserciones entonces validan que el objeto devuelto por some_func() contiene el código de estado y la respuesta correctos. 16.6.4 Se han llamado simulacros de validación Usando unittest.mock es posible validar que una función simulada o método era llamado adecuadamente usando afirmar_llamada(), afirmar_- llamado_con() o afirmar_llamado_una vez_con() dependiendo de si la función toma parámetros o no.
Llama a la API externa, que queremos simular
respuesta = external_module.api_call() devolver clase de respuesta test_some_func_calling_api(TestCase): clase test_some_func_calling_api(TestCase): @patch(’external_module.api_call’) def test_some_func(self, mock_api_call):
Configura una versión simulada de api_call
mock_api_call.return_value = MagicMock(status_code=200, respuesta=json.dumps({‘clave’:‘valor’}))
Llama a some_func() que llama al (simulacro) api_call()
función resultado = alguna_función()
Comprobar que el resultado devuelto por some_func() sea
lo que se esperaba self.assertEqual(result.status_code, 200, “devuelto el código de estado no es 200”) self.assertEqual(resultado.respuesta, ‘{“clave”: “valor”}’, “respuesta JSON incorrecta”) importar módulo_externo de unittest.importación simulada * de unittest importar TestCase de unittest importación principal importar json def alguna_func(): 196 dieciséis Burlarse para probar
La siguiente versión de la prueba test_some_func_with_params() El método verifica que la función simulada api_call() fue llamada con el correcto parámetro. @patch(’external_module.api_call_with_param’) def test_some_func_with_param(self, mock_api_call):
Configura una versión simulada de api_call
mock_api_call.return_value = MagicMock(status_code=200, respuesta=json.dumps({’edad’: ‘23’})) resultado = alguna_función_con_parámetro(‘Phoebe’)
Verificar resultado devuelto por some_func() es lo que fue
esperado self.assertEqual(resultado.respuesta, ‘{edad": “23”}’, ‘JSON resultado incorrecto’)
Verifique que se haya llamado a mock_api_call con el correcto
parámetros mock_api_call.api_call_with_param.assert_ named_with(‘Phoebe’) Si quisiéramos validar que sólo se ha llamado una vez podríamos usar el método de afirmación_llamado_una vez_con(). 16.7 Uso de Mock y MagicMock 16.7.1 Nombrando tus simulacros Puede ser útil darle un nombre a tus simulacros. El nombre se usa cuando el simulacro aparece en los mensajes de error de prueba. El nombre también se propaga a los atributos o métodos del simulacro: simulacro = MagicMock(nombre=‘foo’) 16.7.2 Clases simuladas Además de burlarse de un método individual en una clase, es posible burlarse de un método completo clase. Esto se hace proporcionando al decorador patch() el nombre de la clase para parchear (sin atributo/método con nombre). En este caso se reemplaza la clase while por un objeto MagicMock. A continuación, debe especificar cómo debe comportarse esa clase. 16.6 La biblioteca unittest.mock 197
En este ejemplo, la clase people.Person se ha burlado. Esta clase tiene un método de cálculo de pago () que se burla aquí. La clase de nómina tiene un método generate_payslip() que espera recibir un objeto Person. A continuación, utiliza la información proporcionada por los objetos de persona para calcular_pagar() para generar la cadena devuelta por el método generate_payslip(). 16.7.3 Atributos en clases simuladas Los atributos en un objeto simulado se pueden definir fácilmente, por ejemplo, si queremos establecer un atributo en un objeto simulado, entonces podemos simplemente asignar un valor al atributo: importar personas de unittest.importación simulada * de unittest importar TestCase clase MiPrueba(CasoPrueba): @patch(‘personas.Persona’) def test_one(self, MockPerson): self.assertIs(gente.Persona, MockPerson) instancia = MockPerson.return_value instancia.edad = 24 instancia.nombre = ‘Adán’ self.assertEqual(24, instancia.edad, ’edad incorrecta’) self.assertEqual(‘Adam’, instancia.nombre, ’nombre incorrecto’) En este caso, el atributo edad y nombre se han agregado a la instancia simulada de la clase people.Person. importar personas de unittest.importación simulada * de unittest importar TestCase de unittest importación principal clase MiPrueba(CasoPrueba): @patch(‘personas.Persona’) def test_one(self, MockPerson): self.assertIs(gente.Persona, MockPerson) instancia = MockPerson.return_value instancia.calculate_pay.return_value = 250.0 nómina = personas.Nómina() resultado = payroll.generate_payslip(instancia) self.assertEqual(‘Ganaste 250.0’, result, ’nómina incorrecto’) 198 dieciséis Burlarse para probar
Si el atributo en sí necesita ser un objeto simulado, todo lo que se requiere es asigne un objeto MagicMock (o Mock) a ese atributo: instancia.dirección = MagicMock(nombre=‘Dirección’) 16.7.4 constantes burlonas Es muy fácil burlarse de una constante; esto se puede hacer usando @patch() decorador y probando el nombre de la constante y el nuevo valor a utilizar. Este valor puede ser un valor literal como 42 o ‘Hola’ o puede ser un objeto simulado en sí mismo (como un objeto MagicMock). Por ejemplo: @patch(‘mimodulo.MAX_COUNT’, 10) def test_something(self):
La prueba ahora puede usar mymodule.MAX_COUNT
16.7.5 Propiedades simuladas También es posible burlarse de las propiedades de Python. Esto se hace de nuevo usando el @patch decorador pero usando la clase unittest.mock.PropertyMock y el parámetro new_callable. Por ejemplo: @patch(‘mymoule.Car.wheels’, new_callable=mock.PropertyMock) def test_some_property(self, mock_wheels): mock_wheels.return_value = 6
Resto del método de prueba
16.7.6 Generar excepciones con simulacros Un atributo muy útil que se puede especificar cuando se crea un objeto ficticio es el efecto secundario. Si establece esto en una clase o instancia de excepción, entonces la excepción se generará cuando se llame al simulacro, por ejemplo: simulacro = Mock(side_effect=Exception(’¡Boom!’)) imitar() Esto dará como resultado que se genere la Excepción cuando se invoque el simulacro (). 16.7 Uso de Mock y MagicMock 199
16.7.7 Aplicación de parches a cada método de prueba Si desea simular algo para cada prueba en una clase de prueba, puede decorar toda la clase en lugar de cada método individual. El efecto de la decoración calificar la clase es que el parche se aplicará automáticamente a todos los métodos de prueba en la clase (es decir, a todos los métodos que comienzan con la palabra ‘prueba’). Por ejemplo: de unittest importar TestCase de unittest importación principal @patch(‘personas.Persona’) clase MiPrueba(CasoPrueba): def test_one(self, MockPerson): self.assertIs(gente.Persona, MockPerson) def test_two(self, MockSomeClass): self.assertIs(gente.Persona, MockSomeClass) def hacer_algo(uno mismo): devolver algo’ importar personas de unittest.importación simulada * En la clase de prueba anterior, las pruebas test_one y test_two se suministran con el versión simulada de la clase Person. Sin embargo, el método do_something() es no afectado. 16.7.8 Uso de Patch como administrador de contexto La función de parche se puede utilizar como administrador de contexto. Esto da un grano fino control sobre el alcance del objeto simulado. En el siguiente ejemplo, el método test_one() contiene un with-as declaración que usamos para parchear (simular) la clase de persona como MockPerson. Este La clase simulada solo está disponible dentro de la instrucción with-as. 200 dieciséis Burlarse para probar
16.8 Simula donde lo usas El error más común que cometen las personas que usan la biblioteca unittest.mock es burlarse en el lugar equivocado. La regla es que debes burlarte de dónde estás. va a usarlo; o para decirlo de otra manera, siempre debes burlarte de la cosa real donde se importa, no de donde se importa. 16.9 Problemas con el pedido de parches Es posible tener varios decoradores de parches en un método de prueba. sin embargo, el El orden en el que defina los decoradores de parches es importante. La clave para entender en pie lo que debe ser el orden es trabajar al revés para que cuando los simulacros sean pasan al método de prueba, se presentan con los parámetros correctos. Por ejemplo: @patch(‘mimodulo.sys’) @patch(‘mimodulo.os’) @patch(‘mimodulo.os.ruta’) def prueba_algo(yo, mock_os_path, maquetas, simulacro_sys):
El resto del método de prueba
importar personas de unittest.importación simulada * de unittest importar TestCase de unittest importación principal clase MiPrueba(CasoPrueba): def prueba_uno(uno mismo): con patch(‘personas.Persona’) como MockPerson: self.assertIs(gente.Persona, MockPerson) instancia = MockPerson.return_value instancia.calculate_pay.return_value = 250.0 nómina = personas.Nómina() resultado = payroll.generate_payslip(instancia) self.assertEqual(‘Ganaste 250.0’, resultado, ’nómina incorrecta’) 16.7 Uso de Mock y MagicMock 201
Observe que el simulacro del último parche se pasa al segundo parámetro pasado al método test_something() (self es el primer parámetro de todos los métodos). Sucesivamente el simulacro del primer parche se pasa al último parámetro. Así se pasan las burlas en el método de prueba en el orden inverso al que están definidos. 16.10 ¿Cuántas burlas? Una pregunta interesante a considerar es ¿cuántos simulacros debe usar por prueba? Este es el tema de mucho debate dentro de la comunidad de pruebas de software. El Las reglas generales sobre este tema se dan a continuación, sin embargo, debe ser tenga en cuenta que se trata de directrices y no de reglas estrictas y rápidas. • Evite más de 2 o 3 simulacros por prueba. Debe evitar más de 2-3 las burlas a medida que se burlan de sí mismas se vuelven más difíciles de manejar. Muchos también consideran que si necesita más de 2 o 3 simulacros por prueba, probablemente haya algunos cuestiones de diseño subyacentes que deben tenerse en cuenta. Por ejemplo, si eres probando una clase de Python, entonces esa clase puede tener demasiadas dependencias. Alternativamente, la clase puede tener demasiadas responsabilidades y debe dividirse en varias clases independientes; cada uno con una responsabilidad distinta. Otra causa podría ser que el comportamiento de la clase no esté encapsulado suficiente y que está permitiendo que otros elementos interactúen con la clase en maneras más informales (es decir, la interfaz entre la clase y otros elementos es no limpia/explota lo suficiente). El resultado es que puede ser necesario refactorizar su clase antes de avanzar con el desarrollo y las pruebas. • Solo búrlate de tu vecino más cercano. Solo debes burlarte de tu más cercano vecino ya sea una función, método u objeto. Debes tratar de evitar burlándose de dependencias de dependencias. Si te encuentras haciendo esto, entonces será más difícil de configurar, mantener, comprender y desarrollar. Tambien es cada vez más probable que esté probando los simulacros en lugar de su propia función, método o clase. 16.11 Consideraciones burlonas A continuación, se proporcionan algunas reglas generales que se deben tener en cuenta al usar simulacros con tus pruebas: • No se exceda en las simulaciones: si lo hace, puede terminar simplemente probando las simulaciones. ellos mismos. 202 dieciséis Burlarse para probar
• Decida qué burlarse, los ejemplos típicos de qué burlarse incluyen esos elementos que aún no están disponibles, aquellos elementos que no son por defecto repetibles (como como fuentes de datos en vivo) o aquellos elementos del sistema que consumen mucho tiempo o complejo. • Decida dónde simular, como las interfaces para la unidad bajo prueba. Quieres probar la unidad para que cualquier interfaz que tenga con otro sistema, función o clase pueda ser candidato para un simulacro. • Decida cuándo simular para que pueda determinar los límites de la prueba. • Decida cómo implementará sus simulacros. Por ejemplo, necesita con- Considere qué marco (s) de simulación usará o cómo simular com- componentes como una base de datos. 16.12 Recursos en línea Hay una gran cantidad de información disponible sobre cómo burlarse, cuándo burlarse y qué bibliotecas simuladas usar, sin embargo, lo siguiente proporciona puntos de partida útiles para burlarse de Python: • https://docs.python.org/3/library/unittest.mock.html El Pitón Estándar Documentación de la biblioteca en la biblioteca unitest.mock. • https://docs.python.org/3/library/unittest.mock-examples.html Un conjunto de exámenes ples que puede usar para explorar la burla usando unites.mock. • https://pymox.readthedocs.io/en/latest/index.html Pymox es una alternativa abierta Marco de objeto simulado fuente para Python. • http://gfxmonk.net/dist/doc/mocktest/doc mocktest es otra burla biblioteca para Python. 16.13 Ejercicios Una de las razones para burlarse es garantizar que las pruebas sean repetibles. en este ejercicio Simularemos el uso de un número aleatorio generado para garantizar que nuestras pruebas puedan repetirse fácilmente. El siguiente programa genera una baraja de cartas y elige una carta al azar de la cubierta: 16.11 Consideraciones burlonas 203
Cada vez que se ejecuta el programa, se toma una carta diferente, por ejemplo, en dos con- ejecuciones consecutivas se obtiene el siguiente resultado: Elegiste 13 de tréboles Elegiste 1 de corazones Ahora queremos escribir una prueba para la función pick_a_card(). deberías burlarte ejecute la función random.randint() para hacer esto. importar al azar def create_suite(suite): return [(i, suite) for i in range(1, 14)] def pick_a_card(mazo): print(‘Tú elegiste’) posición = random.randint(0, 52) print(cubierta[posición][0], “de”, cubierta[posición][1]) volver (plataforma[posición])
Configurar los datos
corazones = create_suite(‘corazones’) picas = create_suite(‘picas’) diamantes = create_suite(‘diamantes’) clubes = create_suite(‘clubes’)
Hacer la baraja de cartas
mazo = corazones + picas + diamantes + tréboles
Elija al azar de la baraja de cartas
carta = escoger_una_carta(mazo) 204 dieciséis Burlarse para probar
Parte IV Entrada/salida de archivos
capitulo 17 Introducción a Archivos, Rutas y IO 17.1 Introducción El sistema operativo es una parte fundamental de cualquier sistema informático. se compone de elementos que gestionan los procesos que se ejecutan en la CPU, cómo se utiliza la memoria y administrado, cómo se usan los dispositivos periféricos (como impresoras y escáneres), permite que el sistema informático se comunique con otros sistemas y también proporciona compatibilidad con el sistema de archivos utilizado. El sistema de archivos permite que los programas almacenen datos de forma permanente. Estos datos pueden luego ser recuperado por aplicaciones en una fecha posterior; potencialmente después de que toda la computadora haya sido apagado y reiniciado. El Sistema de Gestión de Ficheros es el encargado de gestionar la creación, acceso y modificación del almacenamiento a largo plazo de datos en archivos. Estos datos pueden almacenarse local o remotamente en discos, cintas, unidades de DVD, USB unidades, etc Aunque esto no siempre fue así; la mayoría de los sistemas operativos modernos organizan archivos en una estructura jerárquica, generalmente en forma de árbol invertido. Para Por ejemplo, en el siguiente diagrama, la raíz de la estructura del directorio se muestra como ‘/’. Este directorio raíz contiene seis subdirectorios. A su vez, el subdirectorio Usuarios contiene 3 directorios más y así sucesivamente: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_17 207
Cada archivo se encuentra dentro de un directorio (también conocido como carpeta en algunos sistemas operativos). sistemas como Windows). Un directorio puede contener cero o más archivos y cero o más directorios. Para cualquier directorio dado, existen relaciones con otros directorios como se muestra a continuación para el directorio jhunt: El directorio raíz es el punto de partida de la estructura jerárquica del árbol de directorios. Un directorio secundario de un directorio determinado se conoce como subdirectorio. el directorio que contiene el directorio dado se conoce como el directorio principal. En cualquier momento, el directorio dentro del cual el programa o el usuario está trabajando actualmente, se conoce como el directorio de trabajo actual. Un usuario o un programa puede moverse por esta estructura de directorios según sea necesario. Hacer esto, el usuario normalmente puede emitir una serie de comandos en una terminal o ventana de comando Como cd para cambiar de directorio o pwd para imprimir el trabajo directorio. Alternativamente, interfaces gráficas de usuario (GUI) para sistemas operativos suelen incluir algún tipo de aplicación de administrador de archivos que permite al usuario ver el estructura de archivos en términos de un árbol. El programa Finder para Mac se muestra a continuación con una estructura de árbol mostrada para un directorio pycharmprojects. Una vista similar también se presenta para el programa Windows Explorer. 208 17 Introducción a Archivos, Rutas y IO
17.2 Atributos de archivo Un archivo tendrá un conjunto de atributos asociados, como la fecha en que se creado, la fecha en que se actualizó/modificó por última vez, el tamaño del archivo, etc. También suelen tener un atributo que indica quién es el propietario del archivo. Este puede ser el creador del archivo; sin embargo, la propiedad de un archivo se puede cambiar desde el línea de comando o a través de la interfaz GUI. Por ejemplo, en Linux y Mac OS X el comando chown se puede usar para cambiar la propiedad del archivo. También puede tener otros atributos que indican quién puede leer, escribir o ejecutar el archivo En los sistemas de estilo Unix (como Linux y Mac OS X), estos derechos de acceso se puede especificar para el propietario del archivo, para el grupo al que está asociado el archivo y para todos los demás usuarios. El propietario del archivo puede tener derechos específicos para leer, escribir y ejecutar un archivo. Estos suelen estar representados por los símbolos ‘r’, ‘w’ y ‘x’ respectivamente. Para ejemplo, el siguiente usa la notación simbólica asociada con archivos Unix y indica que el propietario del archivo puede leer, escribir y ejecutar un archivo: Aquí el primer guión se deja en blanco como se hace con archivos (o directorios) especiales, luego el siguiente conjunto de tres caracteres representa los permisos para el propietario, los siguientes conjunto de tres permisos para todos los demás usuarios. Como este ejemplo tiene rwx en 17.2 Atributos de archivo 209
el primer grupo de tres caracteres indica que el usuario puede leer ‘r’, escribir ‘w’ y ejecute ‘x’ el archivo. Sin embargo, los siguientes seis caracteres son todos guiones que indican que el grupo y todos los demás usuarios no pueden acceder al archivo en absoluto. El grupo al que pertenece un archivo es un grupo que puede tener cualquier número de usuarios como miembros Un miembro del grupo tendrá los derechos de acceso indicados por el ajustes de grupo en el archivo. En cuanto al propietario de un archivo estos pueden ser de lectura, escritura o ejecutar el archivo. Por ejemplo, si los miembros del grupo pueden leer y ejecutar un entonces esto se mostraría usando la notación simbólica como: Ahora este ejemplo indica que solo los miembros del grupo pueden leer y ejecutar el archivo; tenga en cuenta que los miembros del grupo no pueden escribir el archivo (por lo tanto, no pueden modificar el archivo). Si un usuario no es el propietario de un archivo, ni un miembro del grupo del que forma parte el archivo de, entonces sus derechos de acceso están en la categoría “todos los demás”. De nuevo esta categoría puede tener permisos de lectura, escritura o ejecución. Por ejemplo, usando el símbolo notación, si todos los usuarios pueden leer el archivo pero no pueden hacer nada más, entonces esto se mostraría como: Por supuesto, un archivo puede combinar los permisos anteriores, por lo que un propietario puede ser permitido leer, escribir y ejecutar un archivo, el grupo puede ser capaz de leer y ejecutar el archivo, pero todos los demás usuarios solo pueden leer el archivo. Esto se mostraría como: Además de la notación simbólica también existe una notación numérica que se utiliza con sistemas estilo Unix. La notación numérica utiliza tres dígitos para representar el permisos Cada uno de los tres dígitos más a la derecha representa un componente diferente de los permisos: propietario, grupo y otros. Cada uno de estos dígitos es la suma de sus bits componentes en el número binario sistema. Como resultado, los bits específicos se suman a la suma representada por un número: • El bit de lectura suma 4 a su total (en binario 100), • El bit de escritura suma 2 a su total (en binario 010), y • El bit de ejecución suma 1 a su total (en binario 001). • Esta las siguientes notaciones simbólicas pueden ser representadas por un equivalente notación numérica: Simbólico notación Numérico notación Significado rwx—– 0700 Leer, escribir y ejecutar solo para el propietario -rwxrwx— 0770 Leer, escribir y ejecutar para el propietario y el grupo -rwxrwxrwx 0777 Leer, escribir y ejecutar para propietario, grupo y otros 210 17 Introducción a Archivos, Rutas y IO
Los directorios tienen atributos similares y derechos de acceso a los archivos. por ejemplo, el siguiente notación simbólica indica que un directorio (indicado por la ’d’) ha leído y ejecutar permisos para el propietario del directorio y para el grupo. Otros usuarios no se puede acceder a este directorio: Los permisos asociados con un archivo o directorio se pueden cambiar usando un comando desde una terminal o ventana de comandos (como chmod que se usa para modificar los permisos asociados a un archivo o directorio) o de forma interactiva utilizando el herramienta de estilo del explorador de archivos. 17.3 Caminos Una ruta es una combinación particular de directorios que puede conducir a un subespecífico directorio o archivo. Este concepto es importante como sistemas de archivos Unix/Linux/Max OS X y Windows. representan un árbol invertido de directorios y archivos. Por lo tanto, es importante poder ubicaciones de referencia únicas con el árbol. Por ejemplo, en el siguiente diagrama, la ruta /Users/jhunt/work- espacios/pycharmprojects/furtherpython/chapter2 está resaltado: Una ruta puede ser absoluta o relativa. Una ruta absoluta es aquella que proporciona una secuencia completa de directorios desde la raíz del sistema de archivos a un subespecífico directorio o archivo. Una ruta relativa proporciona una secuencia desde el directorio de trabajo actual hasta un subdirectorio o archivo en particular. La ruta absoluta funcionará dondequiera que se encuentre actualmente un programa o usuario dentro del árbol de directorios. Sin embargo, una ruta relativa solo puede ser relevante en un caso específico. ubicación. 17.2 Atributos de archivo 211
Por ejemplo, en el siguiente diagrama, la ruta relativa pycharmprojects/ laterpython/chapter2 solo es significativo en relación con los espacios de trabajo del directorio: Tenga en cuenta que una ruta absoluta comienza desde el directorio raíz (representado por ‘/’) donde como a relativo camino empieza de a particular subdirectorio (semejante como proyectos Pycham). 17.4 Entrada/salida de archivos La entrada/salida de archivos (a menudo denominada E/S de archivos) implica leer y escribir datos hacia y desde archivos. Los datos que se escriben pueden estar en diferentes formatos. Por ejemplo, un formato común utilizado en los sistemas Unix/Linux y Windows es el Formato de texto ASCII. El formato ASCII (o Código Estándar Estadounidense para Información Intercambio) es un conjunto de códigos que representan varios caracteres que es ampliamente utilizado por sistemas operativos. La siguiente tabla ilustra algunos de los caracteres ASCII. códigos y lo que representan: codigo decimal Personaje Significado 42 * Asterisco 43 + Más 48 0 Cero 49 1 Uno 50 2 Dos 51 3 Tres sesenta y cinco A A mayúscula 66 B B mayúscula 67 C C mayúscula 68 D D mayúscula (continuado) 212 17 Introducción a Archivos, Rutas y IO
(continuado) codigo decimal Personaje Significado 97 a minúsculas 98 b b minúscula 99 C c minúscula 100 d minúscula ASCII es un formato muy útil para usar con archivos de texto, ya que pueden ser leídos por una amplia variedad de editores y navegadores. Estos editores y navegadores hacen que sea muy fácil crear archivos legibles por humanos. Sin embargo, lenguajes de programación como Python a menudo usan un conjunto diferente de codificaciones de caracteres, como un carácter Unicode codificación (como UTF-8). Unicode es otro estándar para representar caracteres usando varios códigos. Los sistemas de codificación Unicode ofrecen una gama más amplia de posibles codificaciones de caracteres que ASCII, por ejemplo, la última versión de Unicode en mayo 2019, Unicode 12.1, contiene un repertorio de 137.994 caracteres que cubren 150 guiones modernos e históricos, así como múltiples conjuntos de símbolos y emojis. Sin embargo, esto significa que puede ser necesario traducir ASCII a Unicode (por ejemplo, UTF-8) y viceversa al leer y escribir archivos ASCII en Python. Otra opción es utilizar un formato binario para los datos de un archivo. La ventaja de usar datos binarios es que se requiere poca o ninguna traducción de la representación interna sentación de los datos utilizados en el programa Python en el formato almacenado en el archivo. Él también suele ser más conciso que un formato ASCII equivalente y es más rápido para un programa para leer y escribir y ocupa menos espacio en disco, etc. Sin embargo, la desventaja de un formato binario es que no está en un formato fácilmente legible por humanos. también puede ser difícil para otros programas, particularmente aquellos escritos en otro lenguaje de programación. lenguajes como Java o C#, para leer los datos en los archivos. 17.5 Acceso secuencial versus acceso aleatorio Los datos se pueden leer (o incluso escribir en) un archivo de forma secuencial o a través de un enfoque de acceso aleatorio. El acceso secuencial a los datos en un archivo significa que el programa lee (o escribe) datos en un archivo secuencialmente, comenzando al principio de un archivo y procesando los datos un elemento a la vez hasta llegar al final del archivo. El proceso de lectura solo se mueve adelante y solo al siguiente elemento de datos para leer. El acceso aleatorio a un archivo de datos significa que el programa puede leer (o escribir) datos en cualquier lugar del archivo en cualquier momento. Es decir, el programa puede posicionarse en un punto particular en el archivo (o más bien se puede colocar un puntero dentro del archivo) y entonces puede comenzar a leer (o escribir) en ese punto. Si está leyendo, leerá el siguiente. elemento de datos relativo al puntero en lugar del inicio del archivo. Si está escribiendo datos entonces escribirá datos desde ese punto en lugar de al final del archivo. Si hay ya hay datos en ese punto del archivo, entonces se sobrescribirá. Este tipo de acceso es 17.4 Entrada/salida de archivos 213
también conocido como acceso directo ya que el programa de computadora necesita saber dónde está el los datos se almacenan dentro del archivo y, por lo tanto, van directamente a esa ubicación para los datos. En algunos casos, la ubicación de los datos se registra en un índice y, por lo tanto, también se conoce como acceso indexado. El acceso secuencial a archivos tiene ventajas cuando un programa necesita acceder a información. mación en el mismo orden cada vez que se leen los datos. También es más rápido de leer o escribir todos los datos secuencialmente que a través del acceso directo ya que no hay necesidad de mover el puntero de archivo alrededor. Sin embargo, los archivos de acceso aleatorio son más flexibles ya que no es necesario almacenar los datos. escrita o leída en el orden en que se obtiene. También es posible saltar a sólo la ubicación de los datos requeridos y leer esos datos (en lugar de tener que leer secuencialmente todos los datos para encontrar los elementos de datos de interés). 17.6 Archivos y E/S en Python En el resto de esta sección del libro exploraremos las instalaciones básicas proporcionado para leer y escribir archivos en Python. También vamos a ver la base modelo de flujos para E/S de archivos. Después de esto, exploraremos el ampliamente utilizado CSV y Formatos de archivo de Excel y bibliotecas disponibles para admitirlos. Esta sección concluye por explorando las instalaciones de expresiones regulares en Python. Si bien este último tema no es estrictamente parte de la E/S de archivos, a menudo se usa para analizar los datos leídos de los archivos para filtrarlos información no deseada. 17.7 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://en.wikipedia.org/wiki/ASCII Página de Wikipedia sobre ASCII. • https://en.wikipedia.org/wiki/Unicode Página de Wikipedia en Unicode. • https://en.wikipedia.org/wiki/UTF-8 Página de Wikipedia en UTF-8. 214 17 Introducción a Archivos, Rutas y IO
capitulo 18 Leer y escribir archivos 18.1 Introducción La lectura y escritura de datos en un archivo es muy común en muchos programas. Python proporciona una gran cantidad de soporte para trabajar con archivos de varios tipos. Este capítulo le presenta la funcionalidad principal de E/S de archivos en Python. 18.2 Obtención de referencias a archivos Leer y escribir en archivos de texto en Python es relativamente sencillo. El La función integrada open() crea un objeto de archivo para usted que puede usar para leer y/o o escribir datos desde y/o en un archivo. La función requiere como mínimo el nombre del archivo con el que desea trabajar. Opcionalmente, puede especificar el modo de acceso (por ejemplo, leer, escribir, agregar, etc.). Si usted no especifica un modo, entonces el archivo se abre en modo de sólo lectura. También puede especificar si desea que las interacciones con el archivo se almacenen en búfer, lo que puede mejorar rendimiento agrupando las lecturas de datos. La sintaxis de la función open() es Dónde • file_name indica el archivo al que se accede. • access_mode El access_mode determina el modo en el que el archivo es para abrir, es decir, leer, escribir, agregar, etc. Se incluye una lista completa de valores posibles. dado a continuación en la tabla. Este es un parámetro opcional y el acceso al archivo predeterminado. el modo es lectura (r). file_object = open(file_name, access_mode, buffering) © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_18 215
• almacenamiento en búfer Si el valor de almacenamiento en búfer se establece en 0, no se realiza ningún almacenamiento en búfer. Si el el valor de almacenamiento en búfer es 1, el almacenamiento en búfer de línea se realiza al acceder a un archivo. Los valores de access_mode se dan en la siguiente tabla. El objeto de archivo en sí tiene varios atributos útiles, como • file.closed devuelve True si el archivo se ha cerrado (ya no se puede accedido porque se ha llamado al método close()). • file.mode devuelve el modo de acceso con el que se abrió el archivo. • file.name El nombre del archivo. El método file.close() se usa para cerrar el archivo una vez que haya terminado con él. Esto vaciará cualquier información no escrita en el archivo (esto puede ocurrir debido a almacenamiento en búfer) y cerrará la referencia del objeto de archivo al subyacente real archivo del sistema operativo. Esto es importante para hacer como dejar una referencia a un archivo abierto puede causar problemas en aplicaciones más grandes, ya que normalmente solo hay una cierta número de referencias de archivo posible a la vez y durante un largo período de tiempo estos Modo Descripción r Abre un archivo solo para lectura. El puntero de archivo se coloca al principio del archivo. Este es el modo por defecto rb Abre un archivo para lectura únicamente en formato binario. El puntero del archivo se coloca en el principio del archivo. Este es el modo por defecto r+ Abre un archivo para lectura y escritura. El puntero de archivo colocado al principio de el archivo rb+ Abre un archivo para lectura y escritura en formato binario. El puntero de archivo colocado en el comienzo del archivo w Abre un archivo sólo para escritura. Sobrescribe el archivo si existe. Si el archivo no existe, crea un nuevo archivo para escribir wb Abre un archivo para escribir solo en formato binario. Sobrescribe el archivo si existe. Si el archivo no existe, crea un nuevo archivo para escribir w+ Abre un archivo para escritura y lectura. Sobrescribe el archivo existente si el archivo existe. Si el archivo no existe, crea un nuevo archivo para lectura y escritura wb+ Abre un archivo para escritura y lectura en formato binario. Sobrescribe el archivo existente si el archivo existe. Si el archivo no existe, crea un nuevo archivo para lectura y escritura a Abre un archivo para agregar. El puntero de archivo está al final del archivo si el archivo existe. Es decir, el archivo está en el modo de adición. Si el archivo no existe, crea un nuevo archivo para escribiendo abdominales Abre un archivo para agregarlo en formato binario. El puntero de archivo está al final del archivo si el archivo existe. Es decir, el archivo está en el modo de adición. Si el archivo no existe, se crea un nuevo archivo para escribir un+ Abre un archivo para agregar y leer. El puntero de archivo está al final del archivo si el archivo existe. El archivo se abre en el modo de adición. Si el archivo no existe, crea un nuevo archivo para lectura y escritura ab+ Abre un archivo para agregar y leer en formato binario. El puntero del archivo está en el final del archivo si el archivo existe. El archivo se abre en el modo de adición. Si el archivo no existe, crea un nuevo archivo para lectura y escritura 216 18 Leer y escribir archivos
pueden agotarse, lo que da como resultado errores futuros, ya que los archivos ya no se pueden almacenar. abrió. El siguiente fragmento de código abreviado ilustra las ideas anteriores: La salida de esto es: 18.3 Lectura de archivos Por supuesto, habiendo configurado un objeto de archivo, queremos poder acceder al contenido del archivo o escribir datos en ese archivo (o hacer ambas cosas). Lectura de datos de un texto. El archivo es compatible con los métodos read(), readline() y readlines(): • El método read() Este método devolverá todo el contenido del archivo como un cuerda única • El método readline() lee la siguiente línea de texto de un archivo. devuelve todo el texto en una línea hasta el carácter de nueva línea inclusive. puede ser usado para leer un archivo una línea a la vez. • El método readlines() devuelve una lista de todas las líneas en un archivo, donde cada elemento de la lista representa una sola línea. Tenga en cuenta que una vez que haya leído algún texto de un archivo usando uno de los anteriores operaciones, entonces esa línea no se vuelve a leer. Por lo tanto, usar readlines() resultaría en otro readlines() que devuelve una lista vacía cualquiera que sea el contenido del archivo. Lo siguiente ilustra el uso del método readlines() para leer todo el texto en un archivo de texto en un programa y luego imprima cada línea a su vez: archivo = abrir(‘miarchivo.txt’, ‘r+’) print(‘archivo.nombre:’, archivo.nombre) print(‘archivo.cerrado:’, archivo.cerrado) print(‘archivo.modo:’, archivo.modo) archivo.cerrar() print(‘archivo.cerrado ahora:’, archivo.cerrado) archivo.nombre: miarchivo.txt archivo.cerrado: Falso archivo.modo: r+ archivo.cerrado ahora: Verdadero archivo = abrir(‘miarchivo.txt’, ‘r’) líneas = archivo.readlines() para línea en líneas: imprimir (línea, fin = ‘’) archivo.cerrar() 18.2 Obtención de referencias a archivos 217
Observe que dentro del ciclo for le hemos indicado a la función de impresión que queremos que el carácter final sea ’ ’ en lugar de una nueva línea; Esto se debe a que la línea cadena ya posee el carácter de nueva línea leído del archivo. 18.4 Iteración del contenido del archivo Como sugiere el ejemplo anterior; es muy común querer tramitar el contenido de un archivo una línea a la vez. De hecho, Python lo hace extremadamente fácil al hacer que el objeto de archivo soporte la iteración. La iteración del archivo accede a cada línea del archivo. y hace que esa línea esté disponible para el bucle for. Por lo tanto, podemos escribir: También es posible utilizar la lista de comprensión para proporcionar una forma muy concisa de cargar y procesar líneas en un archivo en una lista. Es similar al efecto de readlines() pero ahora podemos preprocesar los datos antes de crear la lista: 18.5 Escritura de datos en archivos El método write() admite la escritura de una cadena en un archivo. Por supuesto, el archivo El objeto que creamos debe tener un modo de acceso que permita escribir (como ‘w’). Nota que el método de escritura no agrega un carácter de nueva línea (representado como ‘\n’) al final de la cadena; debe hacerlo manualmente. A continuación se muestra un ejemplo de programa corto para escribir un archivo de texto: archivo = abrir(‘miarchivo.txt’, ‘r’) para la línea en el archivo: imprimir (línea, fin = ‘’) archivo.cerrar() archivo = abrir(‘miarchivo.txt’, ‘r’) líneas = [línea.superior() para línea en archivo] archivo.cerrar() imprimir (líneas) print(‘Escribiendo archivo’) f = open(‘mi-nuevo-archivo.txt’, ‘w’) f.write(’¡Hola desde Python!!\n’) f.write(‘Trabajar con archivos es fácil…\n’) f.write(‘Es genial…\n’) f.cerrar() 218 18 Leer y escribir archivos
Esto crea un nuevo archivo llamado my-new-file.txt. Luego escribe tres cadenas para el archivo cada uno con un carácter de nueva línea al final; luego cierra el archivo. El efecto de esto es crear un nuevo archivo llamado myfile.txt con tres líneas: 18.6 Uso de archivos y con extractos Como varios otros tipos en los que es importante cerrar los recursos; el objeto del archivo La clase implementa el Protocolo de administrador de contexto y, por lo tanto, se puede usar con el con declaración. Por lo tanto, es común escribir código que abrirá un archivo usando el con una estructura asegurando así que el archivo se cerrará cuando el bloque de código se termina con, por ejemplo: 18.7 El módulo de entrada de archivos En algunas situaciones, es posible que necesite leer la entrada de varios archivos a la vez. Tú podría hacer esto abriendo cada archivo de forma independiente y luego leyendo el contenido y agregar ese contenido a una lista, etc. Sin embargo, este es un requisito bastante común. ment que el módulo fileinput proporciona una función fileinput.input() que puede tomar una lista de archivos y tratar todos los archivos como una sola entrada simplificando significativamente este proceso, por ejemplo: con open(‘mi-nuevo-archivo.txt’, ‘r’) como f: líneas = archivo.readlines() para línea en líneas: imprimir (línea, fin = ‘’) con fileinput.input(files=(‘spam.txt’, ’eggs.txt’)) como f: para línea en f: proceso (línea) 18.5 Escritura de datos en archivos 219
Las características proporcionadas por el módulo de entrada de archivos incluyen • Devuelve el nombre del archivo que se está leyendo actualmente. • Devolver el entero “descriptor de archivo” para el archivo actual. • Devuelve el número de línea acumulado de la línea que se acaba de leer. • Devolver el número de línea en el archivo actual. Antes de que se haya leído la primera línea, este devuelve 0. • Una función booleana que indica si la línea actual que se acaba de leer es la primera línea de su archivo Algunos de estos se ilustran a continuación: 18.8 Cambio de nombre de archivos Se puede renombrar un archivo usando la función os.rename(). Esta función toma dos argumentos, el nombre de archivo actual y el nuevo nombre de archivo. Es parte del sistema operativo Python. módulo que proporciona métodos que se pueden utilizar para realizar una serie de operaciones de procesamiento de archivos (como cambiar el nombre de un archivo). Para usar el módulo, deberá primero necesita importarlo. A continuación se muestra un ejemplo del uso de la función de cambio de nombre: 18.9 Eliminación de archivos Un archivo se puede eliminar utilizando el método os.remove(). Este método elimina el archivo especificado por el nombre de archivo que se le pasa. Nuevamente, es parte del módulo os y por lo tanto, esto debe ser importado primero: con fileinput.input(files=(’textfile1.txt’, ’textfile2.txt’)) como f: línea = f.readline() print(‘f.nombredearchivo():’, f.nombredearchivo()) print(‘f.esprimeralinea():’, f.esprimeralinea()) print(‘f.lineno():’, f.lineno()) print(‘f.filelineno():’, f.filelineno()) para línea en f: imprimir (línea, fin = ‘’) importar sistema operativo os.rename(‘miarchivonombreoriginal.txt’,‘miarchivonuevonombre.txt’) importar sistema operativo os.remove(‘unnombredearchivo.txt’) 220 18 Leer y escribir archivos
18.10 Archivos de acceso aleatorio Todos los ejemplos presentados hasta ahora sugieren que los archivos se acceden secuencialmente, con la primera línea se lee antes de la segunda y así sucesivamente. Aunque esto es (probablemente) lo más enfoque común no es el único enfoque compatible con Python; tambien es Es posible utilizar un enfoque de acceso aleatorio a los contenidos dentro de un archivo. Para comprender la idea de acceso aleatorio a archivos, es útil comprender que puede mantener un puntero en un archivo para indicar dónde estamos en ese archivo en términos de lectura o escritura de datos. Antes de que se lea algo de un archivo, el puntero está antes del principio del archivo y leer la primera línea de texto, por ejemplo, avanzaría el punto al comienzo de la segunda línea en el archivo, etc. Esta idea se ilustra a continuación: Al acceder aleatoriamente al contenido de un archivo, el programador mueve manualmente el puntero a la ubicación requerida y lee o escribe texto relativo a ese puntero. Esto significa que pueden moverse por el archivo leyendo y escribiendo datos. El aspecto de acceso aleatorio de un archivo lo proporciona el método de búsqueda del archivo. objeto: • file.seek (desplazamiento, de dónde) este método determina dónde se leerá la siguiente o la operación de escritura (dependiendo del modo usado en la llamada open()) toma lugar. En lo anterior, el parámetro de desplazamiento indica la posición del puntero de lectura/escritura dentro del archivo. El movimiento también puede ser hacia adelante o hacia atrás (representado por un compensación negativa). El parámetro opcional wherece indica dónde se relaciona el desplazamiento. El valores utilizados para donde son: 18.10 Archivos de acceso aleatorio 221
• 0 indica que el desplazamiento es relativo al inicio del archivo (predeterminado). • 1 significa que el desplazamiento es relativo a la posición actual del puntero. • 2 indica que el desplazamiento es relativo al final del archivo. Así, podemos mover el puntero a una posición relativa al inicio del archivo, al final del archivo, o a la posición actual. Por ejemplo, en el siguiente código de muestra creamos un nuevo archivo de texto y escribimos un conjunto de caracteres en ese archivo. En este punto, el puntero se coloca después de la ‘z’ en el archivo Sin embargo, luego usamos seek() para mover el punto al décimo carácter en el archivo y ahora escribimos ‘Hola’, luego reposicionamos el puntero al sexto carácter en el archivo y escriba ‘BOO’. Luego cerramos el archivo. Finalmente, leemos todas las líneas. desde el archivo usando una declaración with as y la función open() y desde este veremos que el texto del archivo ahora es abcdefBOOjHELLOpqrstuvwxyz: 18.11 directorios Tanto los sistemas tipo Unix como los sistemas operativos Windows son estructuras jerárquicas. que comprende directorios y archivos. El módulo os tiene varias funciones que pueden ayudar con la creación, eliminación y modificación de directorios. Éstas incluyen: • mkdir() Esta función se utiliza para crear un directorio, toma el nombre de el directorio a crear como a parámetro. Si el directorio ya existe FileExistsError se genera. • chdir() Esta función se puede utilizar para cambiar el directorio de trabajo actual. Este es el directorio desde el que la aplicación leerá/escribirá de forma predeterminada. • getcwd() Esta función devuelve una cadena que representa el nombre del actual directorio de trabajo. • rmdir() Esta función se utiliza para eliminar/eliminar un directorio. toma el nombre del directorio a eliminar como parámetro. • listdir() Esta función devuelve una lista que contiene los nombres de las entradas en el directorio especificado como parámetro de la función (si no se da ningún nombre, el se utiliza el directorio actual). f.cerrar() con abierto(’texto.txt’, ‘r’) como f: para línea en f: imprimir (línea, fin = ‘’) f = abrir(’texto.txt’, ‘w’) f.write(‘abcdefghijklmnopqrstuvwxyz\n’) f.buscar(10,0) f.escribir(‘HOLA’) f.buscar(6, 0) f.escribir (‘BOO’) 222 18 Leer y escribir archivos
A continuación se muestra un ejemplo sencillo que ilustra el uso de algunas de estas funciones: Tenga en cuenta que ‘..’ es una forma abreviada del directorio principal del directorio actual y ‘.’ es la abreviatura del directorio actual. Un ejemplo del tipo de salida generada por este programa para una configuración específica en una Mac se da a continuación: importar sistema operativo imprimir(‘os.getcwd(:’, os.getcwd()) print(‘Lista de contenidos del directorio’) imprimir(os.listdir()) print(‘Crear midir’) os.mkdir(‘midir’) print(‘Listar el contenido actualizado del directorio’) imprimir(os.listdir()) print(‘Cambiar al directorio mydir’) os.chdir(‘midir’) imprimir(‘os.getcwd(:’, os.getcwd()) print(‘Cambiar de nuevo al directorio principal’) os.chdir(’..’) imprimir(‘os.getcwd(:’, os.getcwd()) print(‘Eliminar directorio mydir’) os.rmdir(‘midir’) print(‘Enumere los contenidos resultantes del directorio’) imprimir(os.listdir()) os.getcwd(: /Usuarios/Compartido/espacios de trabajo/pycharm/pythonintro/archivos de texto Listar el contenido del directorio [‘mi-nuevo-archivo.txt’, ‘miarchivo.txt’, ‘archivo-de-texto1.txt’, ‘archivo de texto2.txt’] Crear midir Listar los contenidos actualizados del directorio [‘mi-nuevo-archivo.txt’, ‘miarchivo.txt’, ‘archivo-de-texto1.txt’, ’textfile2.txt’, ‘midir’] Cambiar al directorio mydir os.getcwd(: /Usuarios/Compartido/espacios de trabajo/pycharm/pythonintro/textfiles/mydir Cambiar de nuevo al directorio principal os.getcwd(: /Usuarios/Compartido/espacios de trabajo/pycharm/pythonintro/archivos de texto Eliminar el directorio mydir Enumere los contenidos resultantes del directorio. [‘mi-nuevo-archivo.txt’, ‘miarchivo.txt’, ‘archivo-de-texto1.txt’, ‘archivo de texto2.txt’] 18.11 directorios 223
18.12 Archivos temporales Durante la ejecución de muchas aplicaciones puede ser necesario crear una plantilla archivo porario que se creará en un punto y se eliminará antes de la aplicación termina Por supuesto, es posible administrar dichos archivos temporales usted mismo, sin embargo, el módulo tempfile proporciona una gama de facilidades para simplificar la creación y gestión de estos archivos temporales. Dentro del módulo de archivo temporal Archivo Temporal, Archivo Temporal Con Nombre, TemporaryDirectory y SpooledTemporaryFile son archivos de alto nivel. objetos que proporcionan una limpieza automática de archivos y directorios temporales. Estos los objetos implementan el Protocolo de administrador de contexto. El módulo tempfile también proporciona la función de nivel inferior mkstemp() y mkdtemp() que se puede usar para crear archivos temporales que requieren que el desarrollador gestionarlos y eliminarlos en el momento oportuno. Las características de alto nivel para el módulo tempfile son: • ArchivoTemporal(modo=‘w+b’) Devuelve un objeto similar a un archivo anónimo que puede ser utilizado como un área de almacenamiento temporal. Al finalizar el contexto administrado (a través de una declaración with) o la destrucción del objeto de archivo, el archivo temporal se ser eliminado del sistema de archivos. Tenga en cuenta que, por defecto, todos los datos se escriben en el archivo temporal en formato binario que generalmente es más eficiente. • NamedTemporaryFile(modo=‘w+b’) Este función opera exactamente como lo hace TemporalFile(), excepto que el archivo tiene un nombre visible en el archivo sistema. • SpooledTemporaryFile(max_size=0, mode=‘w+b’) Esta función funciona exactamente como lo hace TemporalFile(), excepto que los datos se ponen en cola en memoria hasta que el tamaño del archivo exceda max_size, o hasta que el archivo del archivo no () se llama al método, momento en el cual los contenidos se escriben en el disco y se operan. ación procede como con Archivo Temporal(). • DirectorioTemporal(sufijo=Ninguno, prefijo=Ninguno, dir=Ninguno) Esta función crea un directorio temporal. Al finalizar el contexto o destrucción del objeto de directorio temporal el temporal recién creado El directorio y todo su contenido se eliminan del sistema de archivos. Las funciones de nivel inferior incluyen: • mkstemp() Crea un archivo temporal que solo el usuario puede leer o escribir. usuario que lo creó. • mkdtemp() Crea un directorio temporal. El directorio es legible, escribible, y solo se puede buscar mediante el ID de usuario creador. • gettempdir() Devuelve el nombre del directorio utilizado para los archivos temporales. Esto define el valor predeterminado para el directorio temporal predeterminado que se utilizará con las demás funciones de este módulo. A continuación se muestra un ejemplo del uso de la función Archivo Temporal. este codigo importa el módulo de archivo temporal y luego imprime el directorio predeterminado utilizado para 224 18 Leer y escribir archivos
archivos temporales. A continuación, crea un objeto de archivo temporal e imprime su nombre y modo (el modo predeterminado es binario, pero para este ejemplo lo hemos sobrescrito para que se utiliza texto sin formato). Entonces hemos escrito una línea en el archivo. Usando buscar estamos reposicionándonos al inicio del archivo y luego leyendo la línea que acabamos de escrito. El resultado de esto cuando se ejecuta en un Apple Mac es: Tenga en cuenta que el nombre del archivo es ‘4’ y que el directorio temporal no es significativo ¡nombre! 18.13 Trabajar con rutas El módulo pathlib proporciona un conjunto de clases que representan las rutas del sistema de archivos; eso son caminos a través de la jerarquía de directorios y archivos dentro de un sistema operativo estructura de archivos. Fue introducido en Python 3.4. La clase central en este módulo es el Clase de ruta. Un objeto Path es útil porque proporciona operaciones que le permiten manipular y administrar la ruta a un archivo o directorio. La clase Path también responde identifica algunas de las operaciones disponibles desde el módulo os (como mkdir, rename y rmdir) lo que significa que no es necesario trabajar directamente con el módulo del sistema operativo. Un objeto de ruta se crea utilizando la función constructora de ruta; esta función en realidad devuelve un tipo específico de ruta dependiendo del tipo de sistema operativo siendo utilizado como WindowsPath o PosixPath (para sistemas de estilo Unix). importar archivo temporal imprimir(‘archivotemp.gettempdir():’, archivotemp.gettempdir()) temp = archivotemp.ArchivoTemporal(‘w+’) print(’temp.nombre:’, temp.nombre) print(’temp.mode:’, temp.mode) temp.write(’¡Hola mundo!’) búsqueda de temperatura(0) línea = temp.readline() imprimir(’linea:’, linea) archivotemp.gettempdir(): /var/carpetas/6n/8nrnt9f93pn66ypg9s5dq8y80000gn/T nombre temporal: 4 modo temp.: w+ línea: ¡Hola mundo! 18.12 Archivos temporales 225
El constructor Path() toma la ruta para crear, por ejemplo, ‘D:/mydir’ (en Windows) o ‘/Users/user1/mydir’ en una Mac o ‘/var/temp’ en Linux etc. Luego puede usar varios métodos diferentes en el objeto Path para obtener información. mación sobre el camino como: • existe() devuelve Verdadero o Falso dependiendo de si la ruta apunta a un archivo o directorio existente. • is_dir() devuelve True si la ruta apunta a un directorio. Falso si se refiere a ences un archivo. También se devuelve falso si la ruta no existe. • is_file() devuelve True de la ruta que apunta a un archivo, devuelve False si el la ruta no existe o la ruta hace referencia a un directorio. • absolute() Un objeto Path se considera absoluto si tiene una raíz y (si apropiado) una unidad. • is_absolute() devuelve un valor booleano que indica si la ruta es absoluto o no. A continuación se muestra un ejemplo del uso de algunos de estos métodos: La salida de muestra producida por este fragmento de código es: También hay varios métodos en la clase Path que se pueden usar para crear y eliminar directorios y archivos como: • mkdir() se usa para crear una ruta de directorio si no existe. si el camino ya existe, se genera un FileExistsError. • rmdir() elimina este directorio; el directorio debe estar vacío de lo contrario un error será elevado. print(‘Crear objeto Path para el directorio actual’) p = Ruta(’.’) imprimir(‘p:’, p) print(‘p.existe():’, p.existe()) imprimir(‘p.is_dir():’, p.is_dir()) print(‘p.es_archivo():’, p.is_archivo()) print(‘p.absoluta():’, p.absoluta()) desde ruta de importación pathlib Crear objeto Path para el directorio actual pag: . p.existe(): Verdadero p.is_dir(): Verdadero p.is_file(): Falso p.absoluta(): /Usuarios/Compartido/espacios de trabajo/pycharm/pythonintro/archivos de texto 226 18 Leer y escribir archivos
• rename(objetivo) cambia el nombre de este archivo o directorio al objetivo dado. • unlink() elimina el archivo al que hace referencia el objeto de ruta. • joinpath(other) agrega elementos al objeto de ruta, p. ruta.joinpath(’/ temperatura’). • with_name(new_name) devuelve un nuevo objeto de ruta con el nombre cambiado. • El operador ‘/’ también se puede usar para crear nuevos objetos de ruta a partir de rutas existentes por ejemplo ruta/ ‘prueba’/ ‘salida’ que agregaría los directorios prueba y hacia el objeto de ruta. Se pueden usar dos métodos de clase Path para obtener objetos de ruta que representen claves directorios como el directorio de trabajo actual (el directorio en el que se encuentra el programa). lógicamente en ese punto) y el directorio de inicio del usuario que ejecuta el programa: • Path.cwd() devuelve un nuevo objeto de ruta que representa el directorio actual. • Path.home() devuelve un nuevo objeto de ruta que representa la casa del usuario directorio. A continuación se proporciona un ejemplo que utiliza varias de las características anteriores. este ejemplo obtiene un objeto de ruta que representa el directorio de trabajo actual y luego agrega ’texto’ a esto. A continuación, se comprueba el objeto de ruta de resultado para ver si existe la ruta (en la computadora que ejecuta el programa), suponiendo que la ruta no existe, se crea y el método existe() se vuelve a ejecutar. El efecto de crear el directorio se puede ver en el resultado: Un método muy útil en el objeto Path es el método glob(patrón). Este El método devuelve todos los elementos dentro de la ruta que cumplen con el patrón especificado. Por ejemplo, path.glob(’.py’) devolverá todos los archivos que terminan en .py dentro de el camino actual. p = Ruta.cwd() print(‘Configurar nuevo directorio’) nuevodir = p / ‘prueba’ print(‘Comprobar si existe newdir’) print(’nuevodir.existe():’, nuevodir.existe()) print(‘Crear nuevo directorio’) nuevodir.mkdir() print(’nuevodir.existe():’, nuevodir.existe()) Configurar un nuevo directorio Compruebe si existe newdir newdir.exists(): Falso Crear nuevo directorio newdir.exists(): Verdadero 18.13 Trabajar con rutas 227
Tenga en cuenta que ‘**/.py’ indicaría el directorio actual y cualquier subdirectorio. Por ejemplo, el siguiente código devolverá todos los archivos donde el nombre del archivo termine con ‘.txt’ para una ruta determinada: Un ejemplo de la salida generada por este código es: Las rutas que hacen referencia a un archivo también se pueden usar para leer y escribir datos en ese archivo. Para ejemplo, el método open() se puede usar para abrir un archivo que, de forma predeterminada, permite que un archivo ser leído: • open(mode=‘r’) esto se puede usar para abrir el archivo al que hace referencia la ruta objeto. Esto se usa a continuación para leer el contenido de un archivo una línea a la vez (tenga en cuenta que con as se usa aquí para asegurar que el archivo representado por Path esté cerrado): Sin embargo, también hay algunos métodos de alto nivel disponibles que le permiten escribir fácilmente datos en un archivo o leer datos de un archivo. Estos incluyen los métodos Path Métodos write_text y read_text: • write_text(data) abre el archivo apuntado en modo texto y escribe el datos y luego cierra el archivo. • read_text() abre el archivo en modo lectura, lee el texto y cierra el archivo; él luego devuelve el contenido del archivo como una cadena. imprimir(’-’ * 10) para archivo en ruta.glob(’.txt’): imprimir(‘archivo:’, archivo) imprimir(’-’ * 10)
archivo: mi-nuevo-archivo.txt archivo: miarchivo.txt archivo: archivo de texto1.txt archivo: archivo de texto2.txt
p = Ruta(‘mitexto.txt’) con p.open() como f: imprimir (f. línea de lectura ()) 228 18 Leer y escribir archivos
Estos se utilizan a continuación Lo que genera la siguiente salida: 18.14 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://docs.python.org/3/tutorial/inputoutput.html para el Pitón Estándar Tutorial sobre entrada y salida de archivos. • https://pymotw.com/3/os.path/index.html para manipulación independiente de la plataforma ción de nombres de archivo. • https://pymotw.com/3/pathlib/index.html para información sistema de archivos Camino objetos. • https://pymotw.com/3/glob/index.html para la coincidencia de patrones de nombre de archivo usando glob. • https://pymotw.com/3/tempfile/index.html para objetos temporales del sistema de archivos. • https://pymotw.com/3/gzip/index.html para obtener información sobre lectura y escritura Archivos GNU Zip. 18.15 Ejercicio El objetivo de este ejercicio es explorar la creación y el acceso a los contenidos de un archivo Debe escribir dos programas, estos programas se describen a continuación:
- Cree un programa que escriba la fecha de hoy en un archivo; el nombre del archivo puede ser codificado o proporcionado por el usuario. Puede utilizar la fecha y hora.today() dir = Ruta(’./prueba’) imprimir(‘Crear nuevo archivo’) nuevoarchivo = dir / ’texto.txt’ print(‘Escribir algo de texto en el archivo’) newfile.write_text(’¡Hola mundo Python!’) print(‘Volver a leer el texto’) imprimir(nuevoarchivo.leer_texto()) imprimir(‘Eliminar el archivo’) nuevoarchivo.desvincular() Crear nuevo archivo Escribir un texto en el archivo Vuelve a leer el texto ¡Hola Mundo Pitón! Quitar el archivo 18.13 Trabajar con rutas 229
para obtener la fecha y hora actual. Puede usar la función str() para convierta este objeto de fecha y hora en una cadena para que pueda escribirse en un archivo. 2. Cree un segundo programa para recargar la fecha del archivo y convertir la cadena en un objeto de fecha. Puede usar la función datetime.strptime() para convertir una cadena en un objeto de fecha y hora (ver https://docs.python.org/3/library/ datetime.html#datetime.datetime.strptime para obtener documentación sobre esta función). Esta función toma una cadena que contiene una fecha y hora y una segunda cadena que define el formato esperado. Si utiliza el enfoque descrito en el paso 1 anterior para escribir la cadena en un archivo, entonces debería encontrar que lo siguiente define un formato apropiado para analizar date_str de modo que un objeto de fecha y hora se puede crear: objeto_fechahora = fechahora.strptime(fecha_cadena, ‘%Y-%m-%d %H:%M:%S.%f’) 230 18 Leer y escribir archivos
capitulo 19 E/S de flujo 19.1 Introducción En este capítulo exploraremos el modelo Stream I/O que subyace en la forma en que de qué datos se leen y se escriben en fuentes y sumideros de datos. Un ejemplo de un fuente de datos o sumidero es un archivo, pero otro podría ser una matriz de bytes. Este modelo es en realidad lo que se encuentra debajo de los mecanismos de acceso a archivos discutidos en el capítulo anterior. En realidad, no es necesario entender este modelo para poder leer y escribir. datos hacia y desde un archivo, sin embargo, en algunas situaciones es útil tener una comprensión permanente de este modelo para que pueda modificar el comportamiento predeterminado cuando necesario. El resto de este capítulo presenta primero el modelo Stream, discute Python transmite en general y luego presenta las clases proporcionadas por Python. entonces considera cuál es el efecto real de usar la función open() presentada en la última capítulo. 19.2 ¿Qué es una corriente? Los flujos son objetos que sirven como fuentes o sumideros de datos. Al principio, este concepto puede parecer un poco extraño. La forma más fácil de pensar en un flujo es como un conducto de datos. que fluye desde o hacia una piscina. Algunas secuencias leen datos directamente desde la “fuente de la data” y algunos flujos leen datos de otros flujos. Estas últimas corrientes luego hacen algún procesamiento “útil” de los datos, como convertir los datos sin procesar en un formato. La siguiente figura ilustra esta idea. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_19 231
En la figura anterior, el flujo inicial de FileIO lee datos sin procesar de los datos reales fuente (en este caso, un archivo). El BufferedReader luego almacena en búfer la lectura de datos proceso para la eficiencia. Finalmente, TextIOWrapper maneja la codificación de cadenas; eso es convierte cadenas de la representación ASCII típica utilizada en un archivo en la representación interna utilizada por Python (que utiliza Unicode). En este punto, podría preguntarse por qué tener un modelo de flujos; después de todo lo que leemos y escribió datos en archivos sin necesidad de conocer los flujos en el último capítulo? La respuesta es que un flujo puede leer o escribir datos hacia o desde una fuente de datos en lugar de que solo de un archivo. Por supuesto, un archivo puede ser una fuente de datos, pero también lo puede ser un socket, un tubería, una cadena, un servicio web, etc. Por lo tanto, es un modelo de E/S de datos más flexible. 19.3 Secuencias de Python El módulo Python io proporciona las principales funciones de Python para manejar la entrada de datos y salida. Hay tres tipos principales de entrada/salida: E/S de texto, E/S binaria y E/S sin formato. Estas categorías se pueden usar con varios tipos de fuentes/sumideros de datos. Cualquiera que sea la categoría, cada flujo concreto puede tener una serie de propiedades como ser de solo lectura, solo escritura o lectura-escritura. También puede soportar secuencial acceso o acceso aleatorio dependiendo de la naturaleza del sumidero de datos subyacente. Para ejemplo, la lectura de datos de un zócalo o tubería es inherentemente secuencial donde como La lectura de datos de un archivo se puede realizar secuencialmente o a través de un acceso aleatorio. acercarse. Sin embargo, cualquiera que sea el flujo que se utilice, son conscientes del tipo de datos que pueden proceso. Por ejemplo, intentar proporcionar una cadena a una secuencia binaria de solo escritura generará un TypeError. Al igual que la presentación de datos binarios en un flujo de texto, etc. Como sugiere esto, hay varios tipos diferentes de flujo proporcionados por el módulo Python io y algunos de estos se presentan a continuación: 232 19 E/S de flujo
La clase IOBase abstracta está en la raíz de la jerarquía de clases IO de flujo. Abajo esta clase son clases de flujo para E/S sin búfer y con búfer y para E/S orientada a texto. 19.4 IOBase Esta es la clase base abstracta para todas las clases de flujo de E/S. La clase proporciona muchas métodos abstractos que las subclases necesitarán implementar. La clase IOBase (y sus subclases) admiten el protocolo iterador. Este significa que un objeto IOBase (o un objeto de una subclase) puede iterar sobre la entrada datos de la corriente subyacente. IOBase también implementa el Protocolo de administrador de contexto y, por lo tanto, puede ser se utiliza con las sentencias with y with-as. La clase IOBase define un conjunto básico de métodos y atributos que incluyen: • close() vaciar y cerrar el flujo. • cerrado un atributo que indica si el flujo está cerrado. • flush() vacía el búfer de escritura del flujo, si corresponde. • readable() devuelve True si se puede leer el flujo. • readline(size=-1) devuelve una línea del flujo. Si el tamaño se especifica en se leerán la mayoría de los bytes de tamaño. • readline(hint=-1) lee una lista de líneas. Si se especifica una sugerencia, se utiliza para controlar el número de líneas leídas. • seek (desplazamiento [, de dónde]) Este método mueve la corriente del flujo posición/puntero al desplazamiento dado. El significado de la compensación depende de la de donde parámetro. El valor predeterminado para el cual es SEEK_SET. • SEEK_SET o 0: busca desde el inicio de la secuencia (predeterminado); compensación debe puede ser un número devuelto por TextIOBase.tell(), o cero. Cualquier otro valor de compensación produce un comportamiento indefinido. • SEEK_CUR o 1: “buscar” a la posición actual; el desplazamiento debe ser cero, que es un ninguna operación (todos los demás valores no son compatibles). • SEEK_END o 2: buscar hasta el final de la secuencia; el desplazamiento debe ser cero (todos los demás los valores no son compatibles). 19.3 Secuencias de Python 233
• seekable() hace que el flujo sea compatible con seek(). • tell() devuelve la posición/puntero de flujo actual. • writeable() devuelve verdadero si los datos se pueden escribir en la secuencia. • writelines(líneas) escribe una lista de líneas en el flujo. 19.5 Clases de E/S sin procesar/E/S sin búfer Las clases RawIOBase y FileIO proporcionan E/S sin procesar o E/S sin búfer. RawIOBase Esta clase es una subclase de IOBase y es la clase base para raw E/S binaria (también conocida como sin búfer). La E/S binaria sin procesar generalmente proporciona acceso de bajo nivel a un dispositivo OS subyacente o API, y no intenta encapsularlo en alto nivel primitivos (esto es responsabilidad de las clases Buffered I/O y Text I/O que pueden envolver un flujo de E/S sin formato). La clase agrega métodos como: • read(size=-1) Este método lee hasta bytes de tamaño de la secuencia y los devuelve. Si el tamaño no se especifica o es -1, se leen todos los bytes disponibles. • readall() Este método lee y devuelve todos los bytes disponibles dentro del arroyo. • readint(b) Este método lee los bytes en el flujo en un preasignado, objeto tipo bytes de escritura b (por ejemplo, en una matriz de bytes). Devuelve el número de bytes. leer. • write(b) Este método escribe los datos proporcionados por b (un objeto similar a bytes como como una matriz de bytes) en el flujo sin procesar subyacente. FileIO La clase FileIO representa un flujo de E/S binario sin búfer sin procesar vinculado a un archivo de nivel de sistema operativo. Cuando se crea una instancia de la clase FileIO, se puede dado un nombre de archivo y el modo (como ‘r’ o ‘w’, etc.). También se le puede dar una bandera a indicar si el descriptor de archivo asociado con el archivo de nivel de sistema operativo subyacente debe estar cerrado o no. Esta clase se utiliza para la lectura de bajo nivel de datos binarios y está en el corazón de todos Acceso a datos orientado a archivos (aunque a menudo está envuelto por otra secuencia, como un lector o escritor almacenado en búfer). 19.6 Clases de E/S binarias/E/S en búfer Binary IO, también conocido como Buffered IO, es un flujo de filtro que envuelve un RawIOBase de nivel inferior. flujo (como un flujo de FileIO). Las clases que implementan E/S en búfer todas extienden la clase BufferedIOBase y son: BufferedReader Al leer datos de este objeto, una mayor cantidad de datos puede solicitarse desde el flujo sin procesar subyacente y mantenerse en un búfer interno. Los datos almacenados en búfer se pueden devolver directamente en lecturas posteriores. 234 19 E/S de flujo
BufferedWriter Al escribir en este objeto, los datos normalmente se colocan en un tampón interno. El búfer se escribirá en el objeto RawIOBase subyacente. bajo varias condiciones, incluyendo: • cuando el búfer se vuelve demasiado pequeño para todos los datos pendientes; • cuando se llama a flush(); • cuando el objeto BufferedWriter se cierra o se destruye. BufferedRandom Una interfaz almacenada en búfer para flujos de acceso aleatorio. Esta arriba- funcionalidad de puertos seek() y tell(). BufferedRWPair A amortiguado E/S objeto combinatorio dos unidireccional Objetos RawIOBase, uno legible, el otro escribible, en un solo bidireccional punto final Cada una de las clases anteriores envuelve una clase de flujo orientada a bytes de nivel inferior, como la clase io.FileIO, por ejemplo: f = io.FileIO(‘datos.dat’) br = io.BufferedReader(f) imprimir(br.leer()) Esto permite leer datos en forma de bytes desde el archivo ‘data.dat’. Puede por supuesto, también lea datos de una fuente diferente, como un BytesIO en memoria objeto: binary_stream_from_file = io.BufferedReader(io.BytesIO(b’starship.png’)) bytes = binary_stream_from_file.read(4) imprimir (bytes) En este ejemplo, los datos son leídos desde el objeto BytesIO por el Lector almacenado en búfer. Luego se usa el método read() para leer los primeros 4 bytes, el la salida es: Tenga en cuenta la ‘b’ delante de la cadena ‘starship.png’ y el resultado ’estrella’. Este indica que el literal de cadena debe convertirse en un literal de bytes en Python 3. Bytes los literales siempre tienen el prefijo ‘b’ o ‘B’; producen una instancia de los bytes escriba en lugar del tipo str. Solo pueden contener caracteres ASCII. Las operaciones admitidas por flujos almacenados en búfer incluyen, para lectura: • peek(n) devuelve hasta n bytes de datos sin avanzar el puntero de flujo. El número de bytes devueltos puede ser menor o mayor que el solicitado dependiendo de la cantidad de datos disponibles. • read(n) devuelve n bytes de datos como bytes, si n no se proporciona (o es negativo) el leer todos los datos disponibles. • readl(n) lee hasta n bytes de datos usando una sola llamada en el flujo de datos sin procesar. 19.6 Clases de E/S binarias/E/S en búfer 235
Las operaciones admitidas por escritores almacenados en búfer incluyen: • write(bytes) escribe los datos similares a bytes y devuelve el número de bytes escrito. • flush() Este método obliga a que los bytes retenidos en el búfer pasen al flujo sin procesar. 19.7 Clases de flujo de texto Las clases de flujo de texto son la clase TextIOBase y sus dos subclases TextIOWrapper y StringIO. TextIOBase Esta es la clase raíz para todas las clases de Text Stream. Proporciona un interfaz basada en caracteres y líneas para Stream I/O. Esta clase proporciona varios métodos adicionales al definido en su clase padre: • read(size=-1) Este método devolverá como máximo caracteres de tamaño del transmitir como una sola cadena. Si el tamaño es negativo o Ninguno, leerá todos los restantes datos. • readline(size=-1) Este método devolverá una cadena que representa el línea actual (hasta una nueva línea o el final de los datos, lo que ocurra primero). Si el stream ya está en EOF, se devuelve una cadena vacía. Si se especifica el tamaño, como máximo se leerán los caracteres de tamaño. • seek(offset, [, wherece]) cambia la posición/puntero del flujo por el desplazamiento especificado. El parámetro opcional wherede indica dónde se busca debe comenzar desde: – SEEK_SET o 0: (predeterminado) busca desde el inicio de la secuencia. – SEEK_CUR o 1: busca la posición actual; el desplazamiento debe ser cero, que es un No operacion. – SEEK_END o 2: busca hasta el final del flujo; el desplazamiento debe ser cero. • tell() Devuelve la posición/puntero de flujo actual como un número opaco. El número no suele representar un número de bytes en el binario subyacente almacenamiento. • write(s) Este método escribirá la cadena s en la secuencia y devolverá la número de caracteres escritos. TextIOWrapper. Este es un flujo de texto almacenado en búfer que envuelve un binario almacenado en búfer stream y es una subclase directa de TextIOBase. Cuando un TextIOWrapper es creado hay una gama de opciones disponibles para controlar su comportamiento: io.TextIOWrapper(búfer, codificación=Ninguno, errores=Ninguno, nueva línea=No ne, line_buffering=Falso, write_through=Falso) 236 19 E/S de flujo
Dónde • buffer es el flujo binario almacenado en el búfer. • codificación representa la codificación de texto utilizada, como UTF-8. • errores define la política de gestión de errores, como estricta o ignorar. • nueva línea controla cómo se manejan los finales de línea, por ejemplo, si deben ser ignorado (Ninguno) o representado como salto de línea, retorno de carro o salto de línea/carro volver etc • line_buffering si es True, entonces flush() está implícito cuando una llamada para escribir contiene un carácter de nueva línea o un retorno de carro. • write_through si es True, se garantiza que una llamada para escribir no se almacenará en el búfer. El TextIOWrapper se envuelve alrededor de una E/S binaria almacenada en búfer de nivel inferior corriente, por ejemplo: f = io.FileIO(‘datos.txt’) br = io.BufferedReader(f) text_stream = io.TextIOWrapper(br, ‘utf-8’) StringIO Este es un flujo en memoria para E/S de texto. El valor inicial del búfer en poder del objeto StringIO se puede proporcionar cuando se crea la instancia, por ejemplo: Esto genera: in_memory_text_stream <_io.StringIO objeto en 0x10fdfaee8> ser o no ser esa es la cuestion Tenga en cuenta que el búfer subyacente (representado por la cadena pasada al instancia de StringIO) se descarta cuando se llama al método close(). El método getvalue() devuelve una cadena que contiene todo el contenido del buffer. Si se llama después de que se cerró la secuencia, se genera un error. 19.8 Propiedades de transmisión Es posible consultar una secuencia para determinar qué tipos de operaciones admite. Esto se puede hacer usando readable(), seekable() y writeable() métodos. Por ejemplo: in_memory_text_stream = io.StringIO(‘ser o no ser eso es la pregunta’) imprimir (’en_memoria_texto_flujo’, en_memoria_texto_flujo) imprimir (en_memoria_text_stream.getvalue()) in_memory_text_stream.close() 19.7 Clases de flujo de texto 237
El resultado de este fragmento de código es: 19.9 Transmisiones de cierre Todos los flujos abiertos deben estar cerrados. Sin embargo, puede cerrar la transmisión de nivel superior y esto cerrará automáticamente los flujos de nivel inferior, por ejemplo: f = io.FileIO(‘datos.txt’) br = io.BufferedReader(f) text_stream = io.TextIOWrapper(br, ‘utf-8’) imprimir (flujo de texto. leer ()) flujo_de_texto.close() 19.10 Volviendo a la función open() Si las transmisiones son tan buenas, ¿por qué no las usas todo el tiempo? Bueno, en realidad en ¡Python 3 lo haces! La función core open (y de hecho la función io.open()) ambos devuelven un objeto de flujo. El tipo real de objeto devuelto depende del archivo modo especificado, si se está utilizando el almacenamiento en búfer, etc. Por ejemplo: text_stream <_io.TextIOWrapper name=‘myfile.txt’ encoding=‘utf- 8’> text_stream.legible(): Verdadero flujo_de_texto.buscable() Verdadero text_stream.writeable() Falso f = io.FileIO(‘miarchivo.txt’) br = io.BufferedReader(f) text_stream = io.TextIOWrapper(br, codificación=‘utf-8’) imprimir(‘flujo_de_texto’, flujo_de_texto) print(‘flujo_de_texto.legible():’, flujo_de_texto.legible()) print(‘flujo_de_texto.buscable()’, flujo_de_texto.buscable()) print(‘flujo_de_texto.escribible()’, flujo_de_texto.escribible()) flujo_de_texto.close() 238 19 E/S de flujo
Cuando se ejecuta este breve ejemplo, la salida es: Como puede ver en la salida, se han devuelto cuatro tipos diferentes de objetos de la función abrir(). El primero es un TextIOWrapper, el segundo un BufferedReader, el tercero un BufferedWriter y el último es un FileIO objeto. Esto refleja las diferencias en los parámetros pasados al aire libre (0 función. Por ejemplo, f1 hace referencia a io.TextIOWrapper porque debe codificar (convertir) el texto de entrada en Unicode utilizando el esquema de codificación UTF-8. Mientras que f2 contiene un io.BufferedReader porque el modo indica que queremos para leer datos binarios mientras f3 tiene un io.BufferedWriter porque el modo utilizado indica que queremos escribir datos binarios. La llamada final para abrir devuelve un FileIO porque hemos indicado que no queremos almacenar en búfer los datos y así podemos use el nivel más bajo del objeto de flujo. En general, se aplican las siguientes reglas para determinar el tipo de objeto devuelto en función de los modos y la codificación especificados: Clase modo almacenamiento en búfer ArchivoIO binario No BufferedReader ‘rb’ Sí BufferedWriter ‘wb’ Sí BufferedRandom ‘rb+’ ‘wb+’ ‘ab+’ Sí TextIOWrapper cualquier texto Sí importar yo
Flujo de texto
f1 = abrir(‘miarchivo.txt’, modo=‘r’, codificación=‘utf-8’) imprimir (f1)
E/S binaria, también conocida como E/S en búfer
f2 = abrir(‘miarchivo.dat’, modo=‘rb’) imprimir (f2) f3 = abrir(‘miarchivo.dat’, modo=‘wb’) imprimir (f3)
Raw IO, también conocido como Unbufferedf IO
f4 = abrir(’nave estelar.png’, modo=‘rb’, almacenamiento en búfer=0) imprimir (f4) <_io.TextIOWrapper name=‘myfile.txt’ mode=‘r’ encoding=‘utf-8’> <_io.BufferedReader name=‘miarchivo.dat’> <_io.BufferedWriter name=‘miarchivo.dat’> <_io.FileIO name=‘starship.png’ mode=‘rb’ closefd=True> 19.10 Volviendo a la función open() 239
Tenga en cuenta que no todas las combinaciones de modos tienen sentido y, por lo tanto, algunas combinaciones generará un error. En general, no necesita preocuparse por qué transmisión está utilizando o lo que hace esa corriente; sobre todo porque todos los flujos amplían la clase IOBase y así tener un conjunto común de métodos y atributos. Sin embargo, es útil comprender las implicaciones de lo que está haciendo para que usted puede tomar mejores decisiones informadas. Por ejemplo, flujos binarios (que no menos procesamiento) son más rápidos que los flujos orientados a Unicode que deben convertirse de ASCII en Unicode. Además, comprender el papel de los flujos en la entrada y la salida también puede permitirle cambiar la fuente y el destino de los datos sin necesidad de volver a escribir la totalidad de su aplicación. Por lo tanto, puede usar un archivo o stdin para probar y un socket para leer datos en producción. 19.11 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://docs.python.org/3/library/io.html. Esto proporciona el estándar de Python Guía de biblioteca de las principales herramientas disponibles para trabajar con flujos. 19.12 Ejercicio Use el modelo de secuencias subyacente para crear una aplicación que escribirá datos binarios a un archivo. Puede usar el prefijo ‘b’ para crear un literal binario para escribir, por ejemplo b ‘Hola mundo’. A continuación, cree otra aplicación para recargar los datos binarios del archivo e imprimirlo. afuera. 240 19 E/S de flujo
capitulo 20 Trabajar con archivos CSV 20.1 Introducción Este capítulo presenta un módulo que admite la generación de CSV (o Coma Valores separados). 20.2 Archivos CSV El formato CSV (Comma Separated Values) es el formato de importación y exportación más común formato para hojas de cálculo y bases de datos. Sin embargo, CSV no es un estándar preciso con Múltiples aplicaciones diferentes que tienen diferentes convenciones y estándares específicos. El módulo csv de Python implementa clases para leer y escribir datos tabulares en formato CSV. Como parte de esto, admite el concepto de un dialecto que es un CSV. formato utilizado por una aplicación específica o conjunto de programas, por ejemplo, es compatible un dialecto de Excel. Esto permite a los programadores decir, “escriba estos datos en el formato preferido por Excel” o “leer datos de este archivo que fue generado por Excel”, sin conocer los detalles precisos del formato CSV utilizado por Excel. Los programadores también pueden describir los dialectos CSV entendidos por otras aplicaciones. ciones o definir sus propios dialectos CSV de propósito especial. El módulo csv proporciona una variedad de funciones que incluyen: • csv.reader (archivo csv, dialect=‘excel’, **fmtparams) Devuelve un objeto lector que iterará sobre líneas en el archivo csv dado. un opcional se puede dar el parámetro del dialecto. Esta puede ser una instancia de una subclase de la Clase de dialecto o una de las cadenas devueltas por list_dialects() función. Los otros argumentos de palabra clave fmtparams opcionales se pueden dar a anula los parámetros de formato individuales en el dialecto actual. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_20 241
• csv.writer (archivo csv, dialect=‘excel’, **fmtparams) Devoluciones un objeto escritor responsable de convertir los datos del usuario en cadenas delimitadas en el archivo csv dado. Se proporciona un parámetro de dialecto opcional. Los fmtparams Se pueden dar argumentos de palabras clave para anular los parámetros de formato individuales en el dialecto actual. • csv.list_dialects() Devuelve los nombres de todos los dialectos registrados. Para ejemplo, en Mac OS X, la lista predeterminada de dialectos es [’excel’, ’excel-tab’, ‘unix’]. 20.2.1 La clase de escritor de CSV Un CSV Writer se obtiene de la función csv.writer(). El escritor csv admite dos métodos utilizados para escribir datos en el archivo CSV: • csvwriter.writerow(fila) Escribe el parámetro de fila en el archivo del escritor objeto, formateado de acuerdo con el dialecto actual. • csvwriter.writerows(rows) Escribe todos los elementos en filas (un iterable de objetos de fila como se describe arriba) al objeto de archivo del escritor, formateado de acuerdo con al dialecto actual. • Los objetos de escritor también tienen el siguiente atributo público: • csvwriter.dialect Una descripción de solo lectura del dialecto que usa el escritor. El siguiente programa ilustra un uso simple del módulo csv que crea un archivo llamado sample.csv. Como no hemos especificado un dialecto, se utilizará el dialecto predeterminado de ‘Excel’. El El método writerow() se utiliza para escribir cada lista separada por comas de cadenas en el archivo CSV. print(‘Creando archivo CSV’) con open(‘sample.csv’, ‘w’, newline=’’) como archivo csv: escritor = csv.escritor(archivocsv) escritor.escritor([‘Ella te ama’, ‘Septiembre de 1963’]) escritor.escritor([‘Quiero tomar tu mano’, ‘diciembre de 1963’]) escritor.escritor([‘No puedo comprarme amor’, ‘Abr 1964’]) escritor.escritor([‘A Hard Days Night’, ‘Julio de 1964’]) El archivo resultante se puede ver como se muestra a continuación: 242 20 Trabajar con archivos CSV
Sin embargo, como es un archivo CSV, también podemos abrirlo en Excel: 20.2.2 La clase de lector de CSV Un objeto CSV Reader se obtiene de la función csv.reader(). se implementa- modifica el protocolo de iteración. Si se usa un objeto de lector csv con un bucle for, cada vez que se redondea el bucle proporciona la siguiente fila del archivo CSV como una lista, analizada de acuerdo con el actual Dialecto CSV. Los objetos de lector también tienen los siguientes atributos públicos: • csvreader.dialect Una descripción de solo lectura del dialecto que usa el analizador • csvreader.line_num El número de líneas leídas del iterador de origen. Esto no es lo mismo que el número de registros devueltos, ya que los registros pueden abarcar varias lineas A continuación, se proporciona un ejemplo muy simple de cómo leer un archivo CSV usando un archivo csv. objeto lector: print(‘Comenzando a leer el archivo csv’) con open(‘sample.csv’, newline=’’) como archivo csv: lector = csv.lector(archivocsv) para la fila en el lector: imprimir(*fila, sep=’, ‘) print(‘Terminó de leer’) 20.2 Archivos CSV 243
El resultado de este programa, basado en el archivo sample.csv creado anteriormente, es: Comenzando a leer el archivo csv Ella te ama, septiembre de 1963 Quiero tomar tu mano, diciembre de 1963 No puedo comprarme amor , abril de 1964 Una noche de días duros, julio de 1964 Terminado de leer 20.2.3 La clase CSV DictWriter En muchos casos, la primera fila de un archivo CSV contiene un conjunto de nombres (o claves) que defina los campos dentro del resto del CSV. Esa es la primera fila da significado a las columnas y los datos contenidos en el resto del archivo CSV. Por lo tanto, es muy útil capturar esta información y estructurar los datos escritos en un archivo CSV o cargados desde un archivo CSV basado en las claves de la primera fila. El csv.DictWriter devuelve un objeto que se puede usar para escribir valores en el archivo CSV basado en el uso de tales columnas nombradas. El archivo que se utilizará con el DictWriter se proporciona cuando se crea una instancia de la clase. importar csv con open(’names.csv’, ‘w’, newline=’’) como archivo csv: nombres de campo = [’nombre’, ‘apellido’, ‘resultado’] escritor = csv.DictWriter(csvfile, nombres de campo=nombres de campo) escritor.writeheader() escritor.escritor({’nombre’: ‘Juan’, ‘apellido’: ‘Smith’, ‘resultado’: 54}) escritor.escritor({’nombre’: ‘Jane’, ‘apellido’: ‘Lewis’, ‘resultado’: 63}) escritor.escritor({’nombre’: ‘Chris’, ‘apellido’: ‘Davies’, ‘resultado’: 72}) Tenga en cuenta que cuando se crea el DictWriter se debe proporcionar una lista de las claves que se utilizan para las columnas en el archivo CSV. Luego se usa el método writeheader() para escribir la fila del encabezado en el archivo CSV. El método writerow() toma un objeto de diccionario que tiene claves basadas en el teclas definidas para el DictWriter. Luego se utilizan para escribir datos en el CSV (tenga en cuenta que el orden de las teclas en el diccionario no es importante). En el código de ejemplo anterior, el resultado de esto es que un nuevo archivo llamado names.csv se crea que se puede abrir en Excel: Por supuesto, como se trata de un archivo CSV, también se puede abrir en un editor de texto sin formato. 244 20 Trabajar con archivos CSV
20.2.4 La clase CSV DictReader Además del csv.DictWriter, hay un csv.DictReader. El archivo a ser utilizado con DictReader se proporciona cuando se crea una instancia de la clase. Al igual que con el DictReader, la clase DictWriter toma una lista de claves utilizadas para definir el columnas en el archivo CSV. Si los encabezados que se utilizarán para la primera fila se pueden proporcionar aunque esto es opcional (si no se proporciona un conjunto de claves, entonces los valores en el primer fila del archivo CSV se utilizará como los nombres de campo). La clase DictReader proporciona varias funciones útiles, incluida la propiedad de nombres de campo que contiene una lista de las claves/títulos para el archivo CSV como definido por la primera fila del archivo. La clase DictReader también implementa el protocolo de iteración y, por lo tanto, puede ser utilizado en un ciclo for en el que cada fila (después de la primera fila) se devuelve a su vez como un diccionario. El objeto de diccionario que representa cada fila se puede usar para acceder valor de cada columna en función de las claves definidas en la primera fila. A continuación se muestra un ejemplo para el archivo CSV creado anteriormente: importar csv print(‘Empezando a leer el ejemplo de dict CSV’) con open(’names.csv’, newline=’’) como archivo csv: lector = csv.DictReader(archivocsv) para encabezado en reader.fieldnames: imprimir (encabezado, final = ’ ‘) imprimir(’\n——————————’) para la fila en el lector: print(fila[’nombre’], fila[‘apellido’], fila[‘resultado’]) imprimir(‘Terminado’) 20.2 Archivos CSV 245
Esto genera la siguiente salida: Comenzando a leer el ejemplo de dict CSV nombre apellido resultado
Juan Smith 54 jane lewis 63 Chris Davis 72 Hecho 20.3 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://docs.python.org/3/library/csv.html para la documentación estándar de Python. ción sobre lectura y escritura de archivos CSV. • https://pymotw.com/3/csv/index.html para la página Módulo Python de la semana en archivos CSV. • https://pythonprogramming.net/reading-csv-files-python-3 para a tutorial en lectura de archivos CSV. 20.4 Ejercicios En este ejercicio, creará un archivo CSV basado en un conjunto de transacciones almacenadas en un cuenta actual.
- Para hacer esto, primero defina una nueva clase de cuenta para representar un tipo de cuenta bancaria.
- Cuando se instancia la clase, debe proporcionar el número de cuenta, el nombre del titular de la cuenta, un saldo de apertura y el tipo de cuenta (que puede ser una cadena que representa ‘actual’, ‘depósito’ o ‘inversión’, etc.). Esto significa que debe haber un método init y deberá almacenar los datos dentro el objeto.
- Proporcione tres métodos de instancia para la Cuenta; cantidad del depósito), retirar (cantidad) y obtener_saldo (). El comportamiento de estos los métodos deben ser los esperados, el depósito aumentará el saldo, retirar disminuirá el saldo y get_balance() devuelve el saldo actual. Su clase de cuenta también debe mantener un historial de las transacciones en las que está involucrada. en. Una Transacción es un registro de un depósito o retiro junto con una cantidad. Tenga en cuenta que la cantidad inicial en una cuenta puede tratarse como un depósito inicial. 246 20 Trabajar con archivos CSV
La historia podría implementarse como una lista que contiene una secuencia ordenada para actas. Una Transacción en sí podría ser definida por una clase con una acción (depósito o retiro) y una cantidad. Cada vez que se realiza un retiro o un depósito, se debe crear un nuevo registro de transacción. agregado a una lista de historial de transacciones. A continuación, proporcione una función (que podría llamarse algo así como write_ac- count_transactions_to_csv()) que puede tomar una cuenta y luego escribir cada una de las transacciones que mantiene en un archivo CSV, con cada tipo de transacción y el monto de la transacción separado por una coma. La siguiente aplicación de ejemplo ilustra cómo se podría usar esta función: imprimir(‘Iniciando’) acc = cuentas.CuentaActual(‘123’, ‘Juan’, 10.05, 100.0) deposito(23.45) retiro de cuenta (12.33) print(‘Escribir transacciones de cuenta’) write_account_transaction_to_csv(‘cuentas.csv’, acc) imprimir(‘Terminado’) El contenido del archivo CSV sería entonces: 20.4 Ejercicios 247
capitulo 21 Trabajar con archivos de Excel 21.1 Introducción Este capítulo presenta el módulo openpyxl que se puede usar cuando se trabaja con archivos de Excel. Excel es una aplicación de software desarrollada por Microsoft que permite usuarios para trabajar con hojas de cálculo. Es una herramienta muy utilizada y archiva usando el El formato de archivo de Excel se encuentra comúnmente en muchas organizaciones. Está dentro cumplir el estándar de la industria para las hojas de cálculo y, como tal, es una herramienta muy útil para tener en la caja de herramientas de los desarrolladores. 21.2 Archivos de Excel Aunque los archivos CSV son una forma cómoda y sencilla de manejar datos; es muy Es común necesitar poder leer o escribir archivos de Excel directamente. Para ello existen varias bibliotecas disponibles en Python para este propósito. Una biblioteca muy utilizada es la Biblioteca OpenPyXL. Esta biblioteca se escribió originalmente para admitir el acceso a Excel archivos 2010. Es un proyecto de código abierto y está bien documentado. La biblioteca OpenPyXL proporciona facilidades para • leer y escribir libros de Excel, • crear/acceder a hojas de cálculo de Excel, • crear fórmulas de Excel, • creación de gráficos (con el apoyo de módulos adicionales). Como OpenPyXL no es parte de la distribución estándar de Python, deberá instale la biblioteca usted mismo usando una herramienta como Anaconda o pip (por ejemplo, pip instalar openpyxl). Alternativamente, si está utilizando PyCharm, podrá para agregar la biblioteca OpenPyXL a su proyecto. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_21 249
21.3 El Openpyxl. Clase de libro de trabajo El elemento clave en la biblioteca OpenPyXL es la clase Workbook. Esto puede ser importado del módulo: desde el libro de trabajo de importación de openpyxl Se puede crear una nueva instancia del Libro de trabajo (en la memoria) usando el Clase de libro de trabajo (tenga en cuenta que en este punto es puramente una estructura dentro de Python y debe guardarse antes de crear un archivo de Excel real). wb = Libro de trabajo() 21.4 El Openpyxl. Objetos de hoja de trabajo Un libro de trabajo siempre se crea con al menos una hoja de trabajo. Puedes hacerte con el hoja de trabajo actualmente activa usando la propiedad Workbook.active: ws = wb.activo Puede crear hojas de trabajo adicionales usando create_sheet de los libros de trabajo () método: ws = wb.create_sheet(‘Mihoja’) Puede acceder o actualizar el título de la hoja de trabajo utilizando la propiedad del título: ws.title = ‘Nuevo Título’ El color de fondo de la pestaña que contiene este título es blanco por defecto. Puede cambiar este Proporcionar un color RRGGBB código a la hoja de trabajo atributo sheet_properties.tabColor, por ejemplo: ws.sheet_properties.tabColor = “1072BA” 21.5 Trabajando con Celdas Es posible acceder a las celdas dentro de una hoja de cálculo. Se puede acceder directamente a una celda como claves en la hoja de trabajo, por ejemplo: ws[‘A1’] = 42 250 21 Trabajar con archivos de Excel
o celda = ws[‘A1’] Esto devuelve un objeto de celda; puedes obtener el valor de la celda usando el valor propiedad, por ejemplo imprimir (celda.valor) También existe el método Worksheet.cell(). Esto proporciona acceso a las celdas. usando notación de fila y columna: d = ws.cell(fila=4, columna=2, valor=10) También se puede agregar una fila de valores en la posición actual dentro del archivo de Excel usando agregar: ws.append([1, 2, 3]) Esto agregará una fila al archivo de Excel que contiene 1, 2 y 3. Se puede acceder a los rangos de celdas mediante el corte: rango_celda = ws[‘A1’:‘C2’] También se pueden obtener rangos de filas o columnas: columna = ws[‘C’] rango_columnas = ws[‘C:D’] fila10 = ws[10] rango_filas = ws[5:10] El valor de una celda también puede ser una fórmula de Excel como ws[‘A3’] = ‘=SUMA(A1, A2)’ Un libro de trabajo es en realidad solo una estructura en la memoria; debe guardarse en un archivo para almacenamiento permanente. Estos libros de trabajo se pueden guardar usando el método save(). Este El método toma un nombre de archivo y escribe el Libro de trabajo en formato Excel. libro de trabajo = libro de trabajo() … libro de trabajo.save(‘saldos.xlsx’) 21.6 Ejemplo de aplicación de creación de archivos de Excel La siguiente aplicación simple crea un libro de trabajo con dos hojas de trabajo. También contiene una fórmula de Excel simple que suma los valores contenidos en otras celdas: 21.5 Trabajando con Celdas 251
El archivo de Excel generado a partir de esto se puede ver en Excel como se muestra a continuación: desde el libro de trabajo de importación de openpyxl def principal(): print(‘Ejemplo de inicio de escritura de Excel con openPyXL’) libro de trabajo = libro de trabajo()
Obtener la hoja de trabajo activa actual
ws = libro de trabajo.activo ws.title = ‘mi hoja de trabajo’ ws.sheet_properties.tabColor = ‘1072BA’ ws[‘A1’] = 42 ws[‘A2’] = 12 ws[‘A3’] = ‘=SUMA(A1, A2)’ ws2 = workbook.create_sheet(title=‘mi otra hoja’) ws2[‘A1’] = 3.42 ws2.append([1, 2, 3]) ws2.cell(columna=2, fila=1, valor=15) libro de trabajo.save(‘muestra.xlsx’) print(‘Ejemplo de escritura en Excel terminado’) si nombre == ‘principal’: principal() 252 21 Trabajar con archivos de Excel
21.7 Cargar un libro de trabajo desde un archivo de Excel Por supuesto, en muchos casos es necesario no solo crear archivos de Excel para exportar datos sino también para importar datos de un archivo de Excel existente. Esto se puede hacer usando el Función openPyXL load_workbook(). Esta función abre el Excel especificado (en modo de solo lectura de forma predeterminada) y devuelve un objeto Workbook. desde openpyxl importar load_workbook libro de trabajo = cargar_libro de trabajo (nombre de archivo = ‘muestra.xlsx’) Ahora puede acceder a una lista de hojas, sus nombres, obtener la hoja actualmente activa etc. usando las propiedades proporcionadas por el objeto del libro de trabajo: • workbook.active devuelve el objeto de hoja de cálculo activo. • workbook.sheetnames devuelve los nombres (cadenas) de las hojas de trabajo en este libro de trabajo. • workbook.worksheets devuelve una lista de objetos de la hoja de trabajo. La siguiente aplicación de ejemplo lee el archivo de Excel creado anteriormente en este capítulo: desde openpyxl importar load_workbook def principal(): print(‘Comenzando a leer el archivo de Excel usando openPyXL’) libro de trabajo = cargar_libro de trabajo (nombre de archivo = ‘muestra.xlsx’) imprimir (libro de trabajo.activo) imprimir (libro de trabajo. nombres de hojas) imprimir (libro de trabajo.hojas de trabajo) imprimir(’-’ * 10) ws = libro de trabajo [‘mi hoja de trabajo’] imprimir(ws[‘A1’]) imprimir(ws[‘A1’].valor) imprimir(ws[‘A2’].valor) imprimir(ws[‘A3’].valor) imprimir(’-’ * 10) para la hoja en el libro de trabajo: imprimir (hoja.título) imprimir(’-’ * 10) rango_celda = ws[‘A1’:‘A3’] para celda en cell_range: imprimir (celda [0]. valor) imprimir(’-’ * 10) 21.7 Cargar un libro de trabajo desde un archivo de Excel 253
print(‘Terminé de leer el archivo de Excel usando openPyXL’) si nombre == ‘principal’: principal() El resultado de esta aplicación se ilustra a continuación: Comenzando a leer archivos de Excel usando openPyXL <Hoja de trabajo “mi hoja de trabajo”> [‘mi hoja de trabajo’, ‘mi otra hoja’] [<Hoja de trabajo “mi hoja de trabajo”>, <Hoja de trabajo “mi otra hoja de trabajo”>]
<Celda ‘mi hoja de trabajo’.A1> 42 12 =SUMA(A1, A2)
mi hoja de trabajo mi otra hoja
42 12 =SUMA(A1, A2)
Terminé de leer el archivo de Excel usando openPyXL 21.8 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://openpyxl.readthedocs.io/en/stable para obtener documentación sobre OpenPyXL Biblioteca de Python a Excel. 21,9 Ejercicios Usando la clase Cuenta que creó en el último capítulo; escribir la cuenta información de la transacción a un archivo de Excel en lugar de un archivo CSV. Todoestacreaunafunciónllamadawrite_account_transaction_to_excel () que toma el nombre del archivo de Excel y la cuenta a almacenar. La función debería entonces escriba los datos en el archivo utilizando el formato de Excel. 254 21 Trabajar con archivos de Excel
La siguiente aplicación de ejemplo ilustra cómo se podría usar esta función: imprimir(‘Iniciando’) acc = cuentas.CuentaActual(‘123’, ‘Juan’, 10.05, 100.0) deposito(23.45) retiro de cuenta (12.33) print(‘Escribir transacciones de cuenta’) write_account_transaction_to_excel(‘cuentas.xlsx’, acc) imprimir(‘Terminado’) El contenido del archivo de Excel sería entonces: 21,9 Ejercicios 255
capitulo 22 Expresiones regulares en Python 22.1 Introducción Las expresiones regulares son una forma muy poderosa de procesar texto mientras se busca patrones recurrentes; a menudo se usan con datos guardados en archivos de texto sin formato (como log archivos), archivos CSV y archivos de Excel. Este capítulo introduce las expresiones regulares, analiza la sintaxis utilizada para definir un patrón de expresión regular y presenta la Módulo Python re y su uso. 22.2 ¿Qué son las expresiones regulares? Una expresión regular (también conocida como expresión regular o simplemente re) es una secuencia de caracteres (letras, números y caracteres especiales) que forman un patrón que se puede se utiliza para buscar texto para ver si ese texto contiene secuencias de caracteres que coinciden con el patrón. Por ejemplo, puede tener un patrón definido como tres caracteres seguidos de tres números Este patrón podría usarse para buscar dicho patrón en otras cadenas. Por lo tanto, las siguientes cadenas coinciden (o contienen) con este patrón o no lo hacen: Abc123 Coincide con el patrón A123A no coincide con el patrón 123 AAA no coincide con el patrón © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_22 257
Las expresiones regulares se utilizan mucho para buscar información en archivos, por ejemplo • encontrar todas las líneas en un archivo de registro asociado con un usuario específico o un operación, • para validar entradas, como verificar que una cadena sea una dirección de correo electrónico válida o código postal/código postal, etc. El soporte para expresiones regulares está muy extendido dentro de los lenguajes de programación como Java, C#, PHP y particularmente Perl. Python no es una excepción y tiene la módulo incorporado re (así como módulos adicionales de terceros) que admiten Regular Expresiones. 22.3 Patrones de expresiones regulares Puede definir un patrón de expresión regular utilizando cualquier carácter o número ASCII. Por lo tanto, la cadena ‘John’ se puede usar para definir un patrón de expresiones regulares que se puede usar para coincide con cualquier otra cadena que contenga los caracteres ‘J’, ‘o’, ‘h’, ’n’. Así cada uno de los las siguientes cadenas coincidirán con este patrón: • ‘John Hunt’ • ‘Juan Jones’ • ‘Andrés John Smith’ • ‘María Helena Juan’ • ‘Juan Juan Juan’ • ‘Voy a visitar al Juan’ • ‘Una vez vi una película de John Wayne’ Pero las siguientes cadenas no coincidirían con el patrón: • ‘Jon Davies’ en este caso porque la ortografía de John es diferente. • ‘john williams’ en este caso porque la J mayúscula no coincide con la j minúscula. • ¡‘David James’ en este caso porque la cadena no contiene la cadena John! Las expresiones regulares (regexs) usan caracteres especiales para permitir patrones más complejos para ser descrito Por ejemplo, podemos usar los caracteres especiales ‘[]’ para definir un conjunto de caracteres que pueden coincidir. Por ejemplo, si queremos indicar que la J puede ser una mayúscula o minúscula, entonces podemos escribir ‘[Jj]’; esto indica que ya sea ‘J’ o ‘j’ puede coincidir con el primero. • [Jj]ohn: indica que el patrón comienza con una J mayúscula o una j minúscula seguido de ‘ohn’. Ahora, tanto ‘john williams’ como ‘John Williams’ coincidirán con este patrón de expresiones regulares. 258 22 Expresiones regulares en Python
22.3.1
Metacaracteres de patrón
Hay varios caracteres especiales (a menudo denominados metacaracteres) que tienen un
significado específico dentro de un patrón regex, estos se enumeran en la siguiente tabla:
Personaje
Descripción
Ejemplo
[]
un conjunto de personajes
[a-d] caracteres en la secuencia ‘a’ a
’d’
Indica una secuencia especial (también puede
usarse para escapar de caracteres especiales)
‘\d’ indica que el carácter debe ser
un entero
.
Cualquier personaje a excepción de
el carácter de nueva línea
‘J.hn’ indica que puede haber cualquier
carácter después de la ‘J’ y antes de la
‘h’
^
Indica que una cadena debe comenzar con el
siguiente patrón
“^hola” indica que la cadena debe comenzar
con ‘hola’
ps
Indica que una cadena debe terminar con el
patrón anterior
“world$” indica que la cadena debe terminar
con ‘mundo’
*
Cero o más ocurrencias del
patrón anterior
“Python*” indica que estamos buscando
cero o más veces Python está en un
cadena
+
Una o más ocurrencias de precedentes
patrón
“info+” indica que debemos encontrar
información en la cadena al menos una vez
?
Indica cero o 1 ocurrencia del
patrón anterior
“¿John?” indica cero o una instancia
del ‘Juan’
{}
Exactamente el número especificado de
ocurrencias
“Juan{3}” esto indica que esperamos
vea el ‘John’ en la cadena tres veces.
“X{1,2}” indica que puede haber
una o dos X una al lado de la otra en el
cadena
|
Cualquiera o
“Verdadero|OK” indica que estamos buscando
para Verdadero u OK
()
Agrupa una expresión regular;
luego puede aplicar otro operador a
el grupo completo
“(abc|xyz){2}” indica que estamos
buscando la cadena abc o xyz
repetido dos veces
22.3.2
Secuencias Especiales
Una secuencia especial es una combinación de una ‘' (barra invertida) seguida de un carácter
combinación que entonces tiene un significado especial. La siguiente tabla enumera los
secuencias especiales comunes utilizadas en expresiones regulares:
22.3
Patrones de expresiones regulares
259
Secuencia Descripción Ejemplo \A Devuelve una coincidencia si los siguientes caracteres son al principio de la cadena “\AThe” debe comenzar con ‘The’ \b Devuelve una coincidencia donde los caracteres especificados están al principio o al final de una palabra “\bon” o “on\b” indica un la cadena debe comenzar o terminar con ’en’ \B Indica que los siguientes caracteres deben ser presentes en una cadena pero no al principio (o al final) final) de una palabra r”\Bon” o r”on\B” no debe empezar o terminar con ‘on’ \d Devuelve una coincidencia donde la cadena contiene dígitos (números del 0 al 9) “\d” \D Devuelve una coincidencia donde la cadena NO contienen dígitos “\D” \s Devuelve una coincidencia en la que la cadena contiene un carácter de espacio en blanco “\s*” \S Devuelve una coincidencia donde la cadena NO contener un carácter de espacio en blanco “\S” \w Devuelve una coincidencia donde la cadena contiene cualquier caracteres de palabras (caracteres de la A a la Z, dígitos del 0 al 9, y el carácter de subrayado _) “\w” \W Devuelve una coincidencia donde la cadena NO contener ningún carácter de palabra “\W” \Z Devuelve una coincidencia si los siguientes caracteres son presente al final de la cadena “Caza\Z” 22.3.3 Conjuntos Un conjunto es una secuencia de caracteres dentro de un par de corchetes que tienen significados La siguiente tabla proporciona algunos ejemplos. Colocar Descripción [je] Devuelve una coincidencia donde está presente uno de los caracteres especificados (j, e o h) [hacha] Devuelve una coincidencia para cualquier carácter en minúscula, alfabéticamente entre a y x [^zxc] Devuelve una coincidencia para cualquier carácter EXCEPTO z, x y c [0123] Devuelve una coincidencia en la que cualquiera de los dígitos especificados (0, 1, 2 o 3) está presente [0–9] Devuelve una coincidencia para cualquier dígito entre 0 y 9 [0–9][0–9] Devuelve una coincidencia para cualquier número de dos dígitos entre 00 y 99 [a–zA–Z] Devuelve una coincidencia para cualquier carácter alfabéticamente entre a y z o A y Z 260 22 Expresiones regulares en Python
22.4 El módulo Python re El módulo Python re es el módulo integrado proporcionado por Python para trabajar con Expresiones regulares. También puede examinar el módulo de expresiones regulares de terceros (consulte https://pypi. org/project/regex) que es retrocompatible con el módulo re predeterminado pero proporciona funcionalidad adicional. 22.5 Trabajar con expresiones regulares de Python 22.5.1 Uso de cadenas sin procesar Un punto importante a tener en cuenta sobre muchas de las cadenas utilizadas para definir el patrones de expresión es que están precedidos por una ‘r’, por ejemplo, r’/bin/sh$’. La ‘r’ antes de la cadena indica que la cadena debe tratarse como un archivo sin procesar. cadena. Una cadena sin procesar es una cadena de Python en la que todos los caracteres se tratan exactamente como eso; personajes individuales. Significa que la barra invertida (’') se trata como un carácter literal en lugar de como un carácter especial que se utiliza para escapar del siguiente carácter. Por ejemplo, en una cadena estándar, ‘\n’ se trata como un carácter especial que representa enviando una nueva línea, así si escribimos lo siguiente: Obtendremos como salida: Sin embargo, si anteponemos la cadena con una ‘r’, entonces le estamos diciendo a Python que la trate como una cuerda en bruto. Por ejemplo: La salida es ahora Esto es importante para la expresión regular, ya que los caracteres como la barra invertida (’') son usado dentro de patrones para tener un significado de expresión regular especial y por lo tanto hacemos No quiero que Python los procese de la manera normal. s = ‘Hola \n mundo’ huellas dactilares) Hola Mundo s = r’Hola \n mundo’ huellas dactilares) hola \n mundo 22.4 El módulo Python re 261
22.5.2 Ejemplo sencillo El siguiente programa de Python simple ilustra el uso básico del módulo re. Él es necesario importar el módulo re antes de poder usarlo. Cuando se ejecuta este programa, obtenemos el siguiente resultado: Si miramos el código, podemos ver que la cadena que estamos examinando contiene ‘john williams’ y que el patrón usado con esta cadena indica que estamos buscando un secuencia de ‘J’ o ‘j’ seguida de ‘ohn’. Para realizar esta prueba usamos el re. función de búsqueda () que pasa el patrón de expresiones regulares y el texto para probar, como parámetros. Esta función devuelve Ninguno (que se toma como falso por el If declaración) o un objeto de coincidencia (que siempre tiene un valor booleano de True). A partir de Por supuesto, ‘john’ al comienzo de text1 coincide con el patrón, el re.search () La función devuelve un objeto de coincidencia y vemos que aparece el mensaje “Se ha encontrado una coincidencia”. impreso. Tanto el objeto Match como el método search() se describirán con más detalle. abajo; sin embargo, este breve programa ilustra el funcionamiento básico de un Regular Expresión. 22.5.3 El objeto de coincidencia Los objetos de coincidencia son devueltos por las funciones search() y match(). Siempre tienen un valor booleano de True. Las funciones match() y search() devuelven None cuando no hay ninguna coincidencia y un objeto Match cuando se encuentra una coincidencia. Por lo tanto, es posible utilizar un partido objeto con una sentencia if: importar re texto1 = ‘john williams’ patrón = ‘[Jj]ohn’ print(‘buscando’, texto1, ‘buscando el patrón’, patrón) if re.search(patrón, texto1): print(‘Se ha encontrado una coincidencia’) buscando en john williams el patrón [Jj]ohn Se ha encontrado una coincidencia 262 22 Expresiones regulares en Python
Los objetos de coincidencia admiten una variedad de métodos y atributos que incluyen: • match.re El objeto de expresión regular cuyo match() o search() El método produjo esta instancia de coincidencia. • match.string La cadena pasada a match() o search(). • match.start([grupo]) / match.end([grupo]) Devuelve los índices del inicio y final de la subcadena emparejada por grupo. • match.group() devuelve la parte de la cadena donde hubo una coincidencia. 22.5.4 La función de búsqueda () La función search() busca una coincidencia en la cadena y devuelve una Coincidencia objeto si hay una coincidencia. La firma de la función es: El significado de los parámetros es: • patrón este es el patrón de expresión regular que se usará en la coincidencia proceso. • cadena esta es la cadena que se buscará. • banderas estas banderas (opcionales) se pueden utilizar para modificar el funcionamiento de la búsqueda. El módulo re define un conjunto de banderas (o indicadores) que se pueden utilizar para indicar cualquier Comportamientos opcionales asociados con el patrón. Estas banderas incluyen: Bandera Descripción re.IGNORARCASO Realiza coincidencias sin distinción entre mayúsculas y minúsculas re.LOCALE Interpreta las palabras según la configuración regional actual. Esta interpretación afecta el grupo alfabético (\w y \W), así como el comportamiento de límite de palabra (\b y B) re.MULTILINE Hace que $ coincida con el final de una línea (no solo con el final de la cadena) y hace que ^ coincide con el inicio de cualquier línea (no solo con el inicio de la cadena) re.DOTALL Hace que un punto (punto) coincida con cualquier carácter, incluido un salto de línea re.UNICODE Interpreta las letras según el juego de caracteres Unicode. Esta bandera afecta el comportamiento de \w, \W, \b, \B re.VERBOSO Ignora los espacios en blanco dentro del patrón (excepto dentro de un conjunto [] o cuando escapado por una barra invertida) y trata el # sin escape como un marcador de comentario importar re coincidencia = re.buscar(patrón, cadena) si coincide: proceso (coincidencia) re.search(patrón, cadena, banderas=0) 22.5 Trabajar con expresiones regulares de Python 263
Si hay más de una coincidencia, solo se mostrará la primera coincidencia. devuelto: En este caso la salida es A continuación se muestra otro ejemplo del uso de la función de búsqueda(). En este caso el patrón a buscar define tres cadenas alternativas (es decir, la cadena debe contener ya sea Beatles, Adele o Gorillaz): En este caso generamos la salida: 22.5.5 La función de coincidencia () Esta función intenta hacer coincidir un patrón de expresión regular al comienzo de un cadena. La firma de esta función se da a continuación: importar re linea1 = ‘El precio es 23.55’ contieneEnteros = r’\d+’ if re.search(containsIntegers, line1): print(‘La línea 1 contiene un número entero’) demás: print(‘La línea 1 no contiene un número entero’) La línea 1 contiene un número entero importar re
Palabras alternativas
musica = r’Beatles|Adele|Gorillaz’ request = ‘Pon algo de Adele’ si re.search (música, solicitud): print(‘Prende fuego a la lluvia’) demás: imprimir(‘Ninguna Adele disponible’) Prender fuego a la lluvia re.match(patrón, cadena, banderas=0) 264 22 Expresiones regulares en Python
Los parámetros son: • patrón esta es la expresión regular que debe coincidir. • cadena esta es la cadena que se buscará. • banderas modifica las banderas que se pueden utilizar. La función re.match() devuelve un objeto Match en caso de éxito, Ninguno en caso de error. 22.5.6 La diferencia entre hacer coincidir y buscar Python ofrece dos operaciones primitivas diferentes basadas en expresiones regulares: • match() busca una coincidencia solo al comienzo de la cadena, • search() busca una coincidencia en cualquier parte de la cadena. 22.5.7 La función findall() La función findall() devuelve una lista que contiene todas las coincidencias. La firma de este función es: Esta función devuelve todas las coincidencias no superpuestas del patrón en la cadena, como una lista de cadenas. La cadena se escanea de izquierda a derecha y las coincidencias se devuelven en el orden encontró. Si uno o más grupos están presentes en el patrón, entonces se muestra una lista de grupos. devuelto; esta será una lista de tuplas si el patrón tiene más de un grupo. Si no se encuentran coincidencias, se devuelve una lista vacía. A continuación se muestra un ejemplo del uso de la función findall(). este ejemplo busca una subcadena que comience con dos letras y seguida de ‘ai’ y una sola personaje. Se aplica a una oración y devuelve solo la subcadena ‘España’ y ‘plano’. re.findall(patrón, cadena, banderas=0) importar re str = ‘La lluvia en España se queda principalmente en el llano’ resultados = re.findall(’[a-zA-Z]{2}ai.’, str) imprimir (resultados) para s en resultados: huellas dactilares) 22.5 Trabajar con expresiones regulares de Python 265
La salida de este programa es 22.5.8 La función findter() Esta función devuelve un iterador que produce objetos coincidentes para la expresión regular. patrón de sion en la cadena suministrada. La firma para esta función es: La cadena se escanea de izquierda a derecha y las coincidencias se devuelven en el orden encontrado. Los partidos vacíos se incluyen en el resultado. Las banderas se pueden utilizar para modificar los partidos. 22.5.9 La función dividir () La función split() devuelve una lista en la que la cadena se ha dividido en cada coincidencia. La sintaxis de la función split() es El resultado es dividir una cadena por las ocurrencias de patrón. Si captura paréntesis se utilizan en el patrón de expresión regular, entonces el texto de todos los grupos en el patrón también se devuelven como parte de la lista resultante. Si maxsplit es distinto de cero, en ocurren la mayoría de las divisiones maxsplit, y el resto de la cadena se devuelve como el elemento final de la lista. Las banderas se pueden volver a utilizar para modificar las coincidencias. la salida es [‘España’, ’llanura’] España plano re.finditer(patrón, cadena, banderas=0) re.split(patrón, cadena, maxsplit=0, banderas=0) importar re str = ‘Era una calurosa noche de verano’ x = re.split(’\s’, str) imprimir (x) [‘Eso’, ’era’, ‘un’, ‘caliente’, ‘verano’, ’noche’] 266 22 Expresiones regulares en Python
22.5.10 La función sub() La función sub() reemplaza las ocurrencias del patrón de expresión regular en el cadena con la cadena repl. Este método reemplaza todo ocurrencias de el regular patrón de expresión- tern en cadena con repl, sustituyendo todas las apariciones a menos que se proporcione max. Este El método devuelve la cadena modificada. importar re patrón = ‘(Inglaterra|Gales|Escocia)’ input = ‘Inglaterra para el fútbol, Gales para el rugby y Escocia para los juegos de las Highlands’ imprimir (re.sub (patrón, ‘Inglaterra’, entrada)) Que genera: Inglaterra por el fútbol, Inglaterra por el Rugby e Inglaterra por el Juegos de la montaña Puede controlar la cantidad de reemplazos especificando el parámetro de conteo: El siguiente código reemplaza las 2 primeras apariciones: importar re patrón = ‘(Inglaterra|Gales|Escocia)’ input = ‘Inglaterra para el fútbol, Gales para el rugby y Escocia para los juegos de las Highlands’ x = re.sub(patrón, ‘Gales’, entrada, 2) imprimir (x) que produce Gales para el fútbol, Gales para el Rugby y Escocia para el Juegos de la montaña También puede averiguar cuántas sustituciones se realizaron usando el subn() función. Esta función devuelve la nueva cadena y el número de sustituciones en una tupla: re.sub(patrón, repl, cadena, max=0) 22.5 Trabajar con expresiones regulares de Python 267
La salida de esto es: (“Escocia para el fútbol, Escocia para el rugby y Escocia para los juegos de las Highlands’, 3) 22.5.11 La función compilar() La mayoría de las operaciones con expresiones regulares están disponibles como funciones a nivel de módulo (como descrito anteriormente) y como métodos en un objeto de expresión regular compilado. Las funciones a nivel de módulo suelen ser formas simplificadas o estandarizadas de usar la expresión regular compilada. En muchos casos estas funciones son suficientes pero si se requiere un control más detallado, entonces se puede usar una expresión regular compilada. La función compile() compila un patrón de expresión regular en un patrón regular. objeto de expresión lar, que se puede usar para hacer coincidir usando su match(), search() y otros métodos como se describe a continuación. El comportamiento de la expresión se puede modificar especificando un valor de banderas. V Las declaraciones: son equivalentes a pero usando re.compile() y guardando el objeto de expresión regular resultante para la reutilización es más eficiente cuando la expresión se utilizará varias veces en un solo programa. Los objetos de expresión regular compilados admiten los siguientes métodos y atributos: • Pattern.search(cadena, posición, posición final) Escanear a través de cadena buscando la primera ubicación donde esta expresión regular produce una coincidencia y devolver un objeto Match correspondiente. Devolver Ninguno si no hay posición en la cadena importar re patrón = ‘(Inglaterra|Gales|Escocia)’ input = ‘Inglaterra para el fútbol, Gales para el rugby y Escocia para los juegos de las Highlands’ imprimir (re.subn (patrón, ‘Escocia’, entrada)) re.compile(patrón, banderas=0) prog = re.compilar(patrón) resultado = prog.match(cadena) resultado = re.match(patrón, cadena) 268 22 Expresiones regulares en Python
coincide con el patrón. Comenzando en pos si se proporciona y terminando en endpos si es así proporcionado (de lo contrario, procesa toda la cadena). • Pattern.match(string, pos, endpos) Si cero o más caracteres en el comienzo de la cadena coincide con esta expresión regular, devuelve una correspondencia objeto de coincidencia de ing. Devuelva Ninguno si la cadena no coincide con el patrón. El pos y endpos son opcionales y especifican las posiciones inicial y final dentro que buscar. • Pattern.split(string, maxsplit = 0)Idéntico a split() función, usando el patrón compilado. • Pattern.findall(string[, pos[, endpos]]) Similar a findall (), pero también acepta parámetros opcionales pos y endpos que limitan la región de búsqueda como para buscar(). • Pattern.finditer(string[, pos[, endpos]]) Similar a find- función iter(), pero también acepta parámetros opcionales pos y endpos que limita la región de búsqueda como para search(). • Patrón.sub(reemplazo, cadena, cuenta
0)Idéntico a el sub() función, usando el patrón compilado. • Pattern.subn(repl, string, count = 0)Idéntico al subn() función, usando el patrón compilado. • Pattern.pattern la cadena de patrón de la que se extrajo el objeto de patrón. compilado A continuación se muestra un ejemplo del uso de la función compile(). El patrón a ser compilado se define como que contiene 1 o más dígitos (0 a 9): El patrón compilado se puede usar para aplicar métodos como search() a un cadena específica (en este caso contenida en la línea 1). La salida generada por esto es: importar re linea1 = ‘El precio es 23.55’ contieneEnteros = r’\d+’ rePattern = re.compile(contieneEnteros) matchLine1 = rePatrón.buscar(línea1) si línea de coincidencia1: print(‘La línea 1 contiene un número’) demás: print(‘La línea 1 no contiene un número’) La línea 1 contiene un número 22.5 Trabajar con expresiones regulares de Python 269
Por supuesto, el objeto de patrón del compilador admite una variedad de métodos además de search() como lo ilustra el método derramado: La salida de esto es 22.6 Recursos en línea Consulte la documentación de la biblioteca estándar de Python para: • https://docs.python.org/3/howto/regex.html Expresión regular de la biblioteca estándar cómo. • https://pymotw.com/3/re/index.html la página Módulo Python de la semana para el módulo re. Otros recursos en línea incluyen • https://regexone.com Una introducción a las expresiones regulares. • https://www.regular-expressions.info/tutorial.html un tutorial de expresiones regulares. • https://www.regular-expressions.info/quickstart.html expresiones regulares rápidas comenzar. • https://pypi.org/project/regex Una conocida expresión regular de terceros módulo que amplía la funcionalidad ofrecida por el módulo re incorporado. 22.7 Ejercicios Escriba una función de Python para verificar que una cadena dada solo contiene letras (superior mayúscula o minúscula) y números. Por lo tanto, no se permiten espacios ni guiones bajos (’_’). Un ejemplo del uso de esta función podría ser: print(contiene_solo_caracteres_y_números(‘Juan’)) # Verdadero imprimir (contiene_solo_caracteres_y_números (‘John_Hunt’)) # FALSO print(contiene_solo_caracteres_y_números(‘42’)) # Verdadero print(contiene_solo_caracteres_y_números(‘Juan42’)) # Verdadero print(contiene_solo_personajes_y_números(‘Juan 42’)) # Falso Escribe una función para verificar un formato de código postal del Reino Unido (llámalo verificar_códigopostal). El formato de un código postal es de dos letras seguidas de 1 o 2 números, seguidas de un p = re.compilar(r’\W+’) s = ‘Calle principal 20’ imprimir(p.split(s)) [‘20’, ‘Alto’, ‘Calle’] 270 22 Expresiones regulares en Python
espacio, seguido de uno o dos números y finalmente dos letras. Un ejemplo de un El código postal es SY23 4ZZ, otro código postal podría ser BB1 3PO y finalmente podríamos tener AA1 56NN (tenga en cuenta que esto es una simplificación del sistema de códigos postales del Reino Unido, pero es adecuado para nuestros propósitos). Usando la salida de esta función, debería poder ejecutar la siguiente prueba código:
Verdadero
imprimir(“verificar_postcode(‘SY23 3AA’):”, verificar_postcode(‘SY23 33AA’))
Verdadero
imprimir(“verificar_postcode(‘SY23 4ZZ’):”, verificar_postcode(‘SY23 4ZZ’))
Verdadero
imprimir(“verificar_postcode(‘BB1 3PO’):”, verificar_postcode(‘BB1 3PO’))
FALSO
imprimir(“verificar_postcode(‘AA111 NN56’):”, verificar_postcode(‘AA111 NN56’))
Verdadero
imprimir(“verificar_postcode(‘AA1 56NN’):”, verificar_postcode(‘AA1 56NN’))
FALSO
imprimir(“verificar_codigo postal(‘AA156NN’):”, verificar_código postal(‘AA156NN’))
FALSO
imprimir (“verificar_código postal (‘AA NN’):”, verificar_código postal (‘AA NN’))
Escriba una función que extraiga el valor contenido entre dos cadenas o caracteres
como ‘<’ y ‘>’. La función debe tomar tres parámetros, el carácter de inicio,
el carácter final y la cadena a procesar. Por ejemplo, el siguiente fragmento de código:
Debe generar resultados como:
imprimir(extraer_valores(’<’, ‘>’, ‘
Parte V Acceso a la base de datos
capitulo 23 Introducción a las bases de datos 23.1 Introducción Hay varios tipos diferentes de sistemas de bases de datos de uso común en la actualidad, incluidos Bases de datos de objetos, bases de datos NoSQL y (probablemente las más comunes) relacionales Bases de datos. Este capítulo se centra en las bases de datos relacionales tipificadas por base de datos sistemas como Oracle, Microsoft SQL Server y MySQL. La base de datos que vamos a El uso en este libro es MySQL. 23.2 ¿Que es una base de datos? Una base de datos es esencialmente una forma de almacenar y recuperar datos. Por lo general, se utiliza algún tipo de lenguaje de consulta con la base de datos para ayudar seleccione la información para recuperar, como SQL o lenguaje de consulta estructurado. En la mayoría de los casos hay una estructura definida que se utiliza para contener los datos (aunque esto no es cierto para las bases de datos no estructuradas no relacionales o NoSQL más nuevas, como CouchDB o MongoDB). En una base de datos relacional, los datos se guardan en tablas, donde las columnas definen el propiedades o atributos de los datos y cada fila define los valores reales que se celebrada, por ejemplo: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_23 275
En este diagrama hay una tabla llamada estudiantes; se está utilizando para sostener información sobre los estudiantes que asisten a una reunión. La tabla tiene 5 atributos (o columnas) definidas para id, nombre, apellidos, asunto y correo electrónico. En este caso, la identificación es probablemente lo que se conoce como clave principal. la clave principal es una propiedad que se utiliza para identificar de forma única la fila de estudiantes; no se puede omitir y debe ser único (dentro de la tabla). Obviamente, los nombres y los temas bien pueden ser duplicado ya que puede haber más de un alumno estudiando Animación o Juegos y los estudiantes pueden tener el mismo nombre o apellido. Es probable que el correo electrónico columna también es única ya que los estudiantes probablemente no comparten una dirección de correo electrónico, pero nuevamente esto puede no ser necesariamente el caso. En este punto, podría preguntarse por qué los datos en una base de datos relacional se llaman relacional y no tablas o tabular? La razón es por un tema conocido como álgebra relacional que sustenta la teoría de bases de datos relacionales. Álgebra relacional toma su nombre del concepto matemático conocido como relación. Sin embargo, por los propósitos de este capítulo, no necesita preocuparse por esto y solo necesita recuerda que los datos se guardan en tablas. 23.2.1 Relaciones de datos Cuando los datos contenidos en una tabla tienen un vínculo o relación con los datos contenidos en otra tabla, se utiliza un índice o clave para vincular los valores de una tabla a otra. Esto es ilustrado a continuación para una tabla de direcciones y una tabla de personas que viven en ese DIRECCIÓN. Esto muestra, por ejemplo, que ‘Phoebe Gates’ vive en la dirección ‘addr2’ que es 12 Queen Street, Bristol, BS42 6YY. 276 23 Introducción a las bases de datos
Este es un ejemplo de una relación de muchos a uno (a menudo escrita como muchos: 1); eso hay muchas personas que pueden vivir en una dirección (en la anterior, Adam Smith también vive en la dirección ‘addr2’). En las Bases de Datos Relacionales puede haber varios tipos diferentes de relación como: • uno: uno donde solo una fila en una tabla hace referencia a una y solo una fila en otra mesa Un ejemplo de una relación uno a uno podría ser de una persona a un pedido de una pieza única de joyería. • one:many esto es lo mismo que el ejemplo de dirección anterior, sin embargo, en este caso la dirección de la relación se invierte (es decir, una dirección en el la tabla de direcciones puede hacer referencia a varias personas en la tabla de personas). • many:many Aquí es donde muchas filas en una tabla pueden hacer referencia a muchas filas en una segunda mesa. Por ejemplo, muchos estudiantes pueden tomar una clase en particular y un el estudiante puede tomar muchas clases. Esta relación suele implicar una relación intermedia. diate (unir) tabla para mantener las asociaciones entre las filas. 23.2.2 El esquema de la base de datos La estructura de una base de datos relacional se define utilizando un lenguaje de definición de datos o lenguaje de descripción de datos (un DDL). Por lo general, la sintaxis de dicho lenguaje se limita a la semántica (significado) necesarios para definir la estructura de las tablas. Esta estructura se conoce como la base de datos. esquema. Por lo general, el DDL tiene comandos como CREATE TABLE, DROP TABLE (para eliminar una tabla) y ALTER TABLE (para modificar la estructura de una tabla existente). Muchas herramientas provistas con una base de datos le permiten definir la estructura del base de datos sin enredarse demasiado en la sintaxis del DDL; sin embargo lo és útil ser consciente de ello y entender que la base de datos se puede crear en este forma. Por ejemplo, utilizaremos la base de datos MySQL en este capítulo. El mysql 23.2 ¿Que es una base de datos? 277
Workbench es una herramienta que le permite trabajar con bases de datos MySQL para administrar y consultar los datos contenidos en una instancia de base de datos en particular. Para referencias de mySQL y MySQL Workbench, consulte los enlaces al final de este capítulo. Como ejemplo, dentro de MySQL Workbench podemos crear una nueva tabla usando un opción de menú en una base de datos: Usando esto podemos definir interactivamente las columnas que formarán la tabla: Aquí cada nombre de columna, su tipo y si es la clave principal (PK), no se ha especificado vacío (o NN no nulo) o único (UQ). Cuando los cambios son aplicado, la herramienta también le muestra el DDL que se utilizará para crear la base de datos: Cuando se aplica esto, se crea una nueva tabla en la base de datos como se muestra a continuación: 278 23 Introducción a las bases de datos
La herramienta también nos permite completar datos en la tabla; esto se hace ingresando datos en una cuadrícula y presionando aplicar como se muestra a continuación: 23.3 SQL y bases de datos Ahora podemos usar lenguajes de consulta para identificar y devolver datos almacenados en la base de datos. a menudo utilizando criterios específicos. Por ejemplo, digamos que queremos devolver todas las personas que tienen el apellido Jones de la siguiente tabla: Podemos hacer esto especificando que se deben devolver datos donde el apellido es igual a ‘Jones’; en SQL esto se vería así: La declaración SELECT anterior establece que todas las propiedades (columnas o atributos) en se devolverá una fila en la tabla de estudiantes donde el apellido sea igual a ‘Jones’. El el resultado es que se devuelven dos filas: Tenga en cuenta que debemos especificar la tabla que nos interesa y qué datos queremos volver (el ‘*’ después de la selección indica que queremos todos los datos). Si solo fuéramos interesados en sus nombres entonces podríamos usar: SELECCIONE * DE estudiantes donde apellido = ‘Jones’; SELECCIONE el nombre DE los estudiantes donde apellido = ‘Jones’; 23.2 ¿Que es una base de datos? 279
Esto devolvería solo los nombres de los estudiantes: 23.4 Lenguaje de manipulación de datos Los datos también se pueden insertar en una tabla o los datos existentes en una tabla se pueden actualizar. Este se realiza utilizando el lenguaje de manipulación de datos (DML). Por ejemplo, para insertar datos en una tabla, simplemente necesitamos escribir un INSERT SQL declaración que proporciona los valores que se agregarán y cómo se asignan a las columnas en el mesa: Esto agregaría la fila 6 a la tabla de estudiantes con el resultado de que la tabla ahora tendría una fila adicional: Actualizar una fila existente es un poco más complicado ya que primero es necesario identificar la fila a actualizar y luego los datos a modificar. Por lo tanto, una ACTUALIZACIÓN incluye una cláusula where para garantizar que se modifique la fila correcta: El efecto de este código es que la segunda fila en la tabla de estudiantes se modifica con la nueva dirección de correo electrónico: INSERT INTO ’estudiantes’ (‘id’, ’nombre’, ‘apellido’, ‘asignatura’, ‘correo electrónico’) VALORES (‘6’, ‘James’, ‘Andrews’, ‘Juegos’, ‘ja@mi.com’); ACTUALIZAR ’estudiantes’ SET ’email'='grj@my.com’ WHERE ‘id’=‘2’; 280 23 Introducción a las bases de datos
23.5 Transacciones en Bases de Datos Otro concepto importante dentro de una base de datos es el de Transacción. una transacción representa una unidad de trabajo realizada dentro de un sistema de gestión de base de datos (o sistema similar) contra una instancia de base de datos, y es independiente de cualquier otro transacción. Las transacciones en un entorno de base de datos tienen dos propósitos principales • Proporcionar una unidad de trabajo que permita la recuperación de fallas y mantenga un base de datos consistente incluso en casos de falla del sistema, cuando la ejecución se detiene (total o parcialmente). Esto se debe a que todas las operaciones dentro de un se realiza la transacción o no se realiza ninguna de ellas. Así, si una operación provoca un error, entonces todos los cambios realizados por la transacción hasta el momento se revierten y ninguno de ellos habrá sido hecho. • Para proporcionar aislamiento entre programas que acceden a una base de datos al mismo tiempo. Este significa que el trabajo realizado por un programa no interactuará con otro funcionan los programas. Una transacción de base de datos, por definición, debe ser atómica, consistente, aislada y duradero: • Atómico Esto indica que una transacción representa una unidad atómica de trabajo; eso ¿Se realizan todas las operaciones de la transacción o no se realiza ninguna de ellas? realizado. • Consistente Una vez completada la transacción se deben dejar los datos en forma consistente estado con cualquier restricción de datos cumplida (como una fila en una tabla no debe hacer referencia una fila inexistente en otra tabla en una relación de uno a muchos, etc.). • Aislada Se relaciona con los cambios que se realizan por transacciones concurrentes; estos cambios deben estar aislados unos de otros. Es decir, una transacción no puede ver los cambios realizados por otra transacción hasta la segunda transacción completa y todos los cambios se guardan permanentemente en la base de datos. • Duradero Esto significa que una vez que se completa una transacción, los cambios que ha tenido hechas se almacenan permanentemente en la base de datos (hasta que alguna transacción futura modifica esos datos). Los practicantes de bases de datos a menudo se refieren a estas propiedades de las transacciones de bases de datos usando el acrónimo ACID (atómico, consistente, aislado, duradero). No todas las bases de datos admiten transacciones, aunque todas las comerciales, de producción bases de datos de calidad como Oracle, Microsoft SQL Server y MySQL, admiten actas. 23.5 Transacciones en Bases de Datos 281
23.6 Otras lecturas Si quieres saber más sobre bases de datos y sistemas de gestión de bases de datos aquí son algunos recursos en línea: • https://en.wikipedia.org/wiki/Database, que es la entrada de wikipedia para data- bases y por lo tanto actúa como una útil referencia rápida y punto de partida para otros material. • https://en.wikibooks.org/wiki/Introduction_to_Computer_Information_Systems/ Base de datos que proporciona una breve introducción a las bases de datos. • https://www.techopedia.com/6/28832/enterprise/databases/introduction-to-data- bases otro punto de partida útil para profundizar en las bases de datos. • https://en.wikipedia.org/wiki/Object_database para información en Objeto bases de datos • https://en.wikipedia.org/wiki/NoSQL para una introducción a No SQL o no bases de datos relacionales. • https://www.mysql.com/ para la base de datos MySQL. • https://dev.mysql.com/downloads/workbench Inicio de MySQL Workbench página. • https://www.mongodb.com/ para la página de inicio del sitio de MongoDB. • http://couchdb.apache.org/ para la base de datos de Apache Couch. Si desea explorar el tema del diseño de bases de datos (es decir, el diseño de las tablas y enlaces entre tablas en una base de datos), entonces estas referencias pueden ayudar: • https://en.wikipedia.org/wiki/Database_design la entrada de wikipedia para la base de datos diseño. • https://www.udemy.com/cwdatabase-design-introduction/ que cubre la mayor parte de las ideas centrales dentro del diseño de la base de datos. • http://en.tekstenuitleg.net/articles/software/database-design-tutorial/intro.html que proporciona otro tutorial que cubre la mayoría de los elementos centrales de data- diseño básico. Si desea explorar SQL más, consulte: • https://en.wikipedia.org/wiki/SQL el sitio de wikipedia para SQL • https://www.w3schools.com/sql/sql_intro.asp que es el material escolar de W3 en SQL y como tal un excelente recurso. • https://www.codecademy.com/learn/learn-sql, que es un sitio de codecademy para SQL. 282 23 Introducción a las bases de datos
capitulo 24 Python DB-API 24.1 Acceder a una base de datos desde Python El estándar para acceder a una base de datos en Python es Python DB-API. Este especifica un conjunto de interfaces estándar para módulos que desean permitir que Python acceda una base de datos específica. El estándar se describe en PEP 249 (https://www.python.org/ dev/peps/pep-0249): un PEP es una propuesta de mejora de Python. Casi todos los módulos de acceso a la base de datos de Python se adhieren a este estándar. Esto significa que si se está moviendo de una base de datos a otra, o intentando portar un Python programa de una base de datos a otra, entonces las API que encuentre deberían ser muy similar (aunque el SQL procesado por una base de datos diferente también puede diferir). Hay Módulos disponibles para las bases de datos más comunes como MySQL, Oracle, Servidor SQL de Microsoft, etc. 24.2 La DB-API Hay varios elementos clave para DB_API, estos son: • La función de conexión. La función connect() que se utiliza para conectarse a un base de datos y devuelve un objeto de conexión. • Objetos de conexión. Dentro de la DB-API se logra el acceso a una base de datos a través de objetos de conexión. Estos objetos de conexión proporcionan acceso al cursor. objetos. • Los objetos de cursor se utilizan para ejecutar sentencias SQL en la base de datos. • El resultado de una ejecución. Estos son los resultados que se pueden obtener como secuencia de secuencias (como una tupla de tuplas). Por lo tanto, el estándar se puede utilizar para seleccionar, insertar o actualizar información en la base de datos. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_24 283
Estos elementos se ilustran a continuación: El estándar especifica un conjunto de funciones y objetos que se utilizarán para conectarse a un base de datos. Estos incluyen la función de conexión, el objeto de conexión y el Objeto cursor. Los elementos anteriores se describen con más detalle a continuación. 24.2.1 La función de conexión La función de conexión se define como: conectar(parámetros…) Se utiliza para realizar la conexión inicial a la base de datos. La conexión devuelve un Objeto de conexión. Los parámetros requeridos por la función de conexión son data- dependiente de la base. 24.2.2 El objeto de conexión El objeto de conexión es devuelto por la función connect(). La conexión object proporciona varios métodos que incluyen: • close() utilizado para cerrar la conexión una vez que ya no la necesite. La estafa- la conexión quedará inutilizable a partir de este momento. • commit() utilizado para confirmar una transacción pendiente. 284 24 Python DB-API
• rollback() utilizado para revertir todos los cambios realizados en la base de datos desde la confirmación de la última transacción (opcional ya que no todas las bases de datos proporcionan apoyo). • cursor() devuelve un nuevo objeto Cursor para usar con la conexión. 24.2.3 El objeto cursor El objeto Cursor se devuelve desde el método connection.cusor(). Un objeto Cursor representa un cursor de base de datos, que se utiliza para gestionar el contexto. de una operación de búsqueda o la ejecución de un comando de base de datos. Los cursores admiten una variedad de atributos y métodos: • cursor.execute(operación, parámetros) Preparar y ejecutar un operación de la base de datos (como una declaración de consulta o un comando de actualización). Los parámetros se pueden proporcionar como una secuencia o mapeo y estarán vinculados a variables en la operación. Las variables se especifican en una notación específica de la base de datos. • cursor.rowcount un atributo de solo lectura que proporciona el número de filas que la última llamada cursor.execute() devuelta (para declaraciones de estilo seleccionadas) o afectados (para actualizar o insertar declaraciones de estilo). • cursor.description un atributo de solo lectura que proporciona información sobre el columnas presentes en los resultados devueltos por una operación SELECT. • cursor.close() cierra el cursor. A partir de este punto el cursor no será usable. Además, el objeto Cursor también proporciona varios métodos de estilo de búsqueda. Estos Los métodos se utilizan para devolver los resultados de una consulta de base de datos. Los datos devueltos son compuesto por una secuencia de secuencias (como una tupla de tuplas) donde cada interior secuencia representa una sola fila devuelta por la instrucción SELECT. la búsqueda Los métodos definidos por la norma son: • cursor.fetchone() Obtener la siguiente fila de un conjunto de resultados de consulta, devolviendo un secuencia única o Ninguno cuando no hay más datos disponibles. • cursor.fetchall() Obtener todas las filas (restantes) del resultado de una consulta, devolviendo ellos como una secuencia de secuencias. • cursor.fetchman(tamaño) Obtener el siguiente conjunto de filas del resultado de una consulta, devolver una secuencia de secuencias (por ejemplo, una tupla de tuplas). Una sucesión vacía es devuelto cuando no hay más filas disponibles. El número de filas para buscar por llamada está especificado por el parámetro. 24.2 La DB-API 285
24.2.4 Asignaciones de tipos de bases de datos a tipos de Python El estándar DB-API también especifica un conjunto de asignaciones de los tipos utilizados en un base de datos a los tipos utilizados en Python. Para obtener una lista completa, consulte el estándar DB-API en sí mismo, pero las asignaciones clave incluyen: Fecha (año, mes, día) Representa una fecha de base de datos. Tiempo (hora, minuto, segundo) Representa un valor de base de datos de tiempo Marca de tiempo (año, mes, día, hora, minuto, segundo) Contiene un valor de marca de tiempo de la base de datos Cadena Se utiliza para representar cadenas como datos de base de datos. (como VARCHAR) 24.2.5 Generando errores El estándar también especifica un conjunto de Excepciones que se pueden lanzar en diferentes situaciones Estos se presentan a continuación y en la siguiente tabla: El diagrama anterior ilustra la jerarquía de herencia para los errores y las advertencias. asociado a la norma. Tenga en cuenta que tanto la advertencia como el error de DB-API extender la clase Exception del Python estándar; sin embargo, dependiendo de la implementación específica puede haber una o más clases adicionales en la jerarquía archy entre estas clases. Por ejemplo, en el módulo PyMySQL hay un 286 24 Python DB-API
Clase MySQLError que extiende Exception y luego es extendida por ambos Advertencia y error. Tenga en cuenta también que Advertencia y Error no tienen relación entre sí. Este es porque las advertencias no se consideran errores y, por lo tanto, tienen una clase separada jerarquías. Sin embargo, el error es la clase raíz para todas las clases de error de la base de datos. A continuación se proporciona una descripción de cada clase de advertencia o error. Advertencia Se utiliza para advertir sobre problemas como truncamiento de datos durante la inserción, etc. Error La clase base de todas las demás excepciones de error. Error de interfaz Excepción generada por errores relacionados con la base de datos interfaz en lugar de la propia base de datos Error de la base de datos Excepción generada por errores relacionados con la base de datos Error de datos Excepción generada por errores que se deben a problemas con los datos como división por cero, valor numérico fuera de rango, etc. Error operativo Excepción generada por errores relacionados con la base de datos. operación y no necesariamente bajo el control del programador, por ej. se produce una desconexión inesperada, etc. Error de integridad Excepción lanzada cuando la integridad relacional de la base de datos es afectado Error interno Excepción generada cuando la base de datos encuentra un error interno, p.ej. el cursor ya no es válido, la transacción no está sincronizada, etc. Error de programación Excepción planteada por errores de programación, p. tabla no encontrada, error de sintaxis en la declaración SQL, número incorrecto de parámetros especificado, etc. Error no admitido Se generó una excepción en caso de que se usara un método o una API de base de datos que no es compatible con la base de datos, p. solicitando un .rollback() en una conexión que no es compatible transacciones o tiene transacciones desactivadas 24.2.6 Descripciones de fila El objeto Cursor tiene una descripción de atributo que proporciona una secuencia de secuencias; cada subsecuencia proporciona una descripción de uno de los atributos de la datos devueltos por una instrucción SELECT. La secuencia que describe el atributo es compuesto por hasta siete elementos, estos incluyen: • nombre que representa el nombre del atributo, • type_code que indica qué tipo de Python se ha asignado a este atributo a, • display_size el tamaño utilizado para mostrar el atributo, • internal_size el tamaño utilizado internamente para representar el valor, 24.2 La DB-API 287
• precisión si un valor numérico real la precisión soportada por el atributo, • escala indica la escala del atributo, • null_ok indica si los valores nulos son aceptables para este atributo. Los dos primeros elementos (nombre y código de tipo) son obligatorios, los otros cinco son son opcionales y se establecen en Ninguno si no se pueden proporcionar valores significativos. 24.3 Transacciones en PyMySQL Las transacciones se gestionan en PyMySQL a través del objeto de conexión de la base de datos. Este object proporciona el siguiente método: • connection.commit() esto hace que la transacción actual confirme todos los cambios realizados permanentemente en la base de datos. Entonces se inicia una nueva transacción. • connection.rollback() esto hace que todos los cambios que se hayan hecho sean lejos (pero no almacenado permanentemente en la base de datos, es decir, no comprometido) para ser remoto. Entonces se inicia una nueva transacción. El estándar no especifica cómo una interfaz de base de datos debe administrar el cambio transacciones de encendido y apagado (sobre todo porque no todas las bases de datos admiten transacciones). Sin embargo, MySQL admite transacciones y puede funcionar en dos modos; uno admite el uso de transacciones como ya se ha descrito; el otro usa una confirmación automática modo. En el modo de confirmación automática, cada comando enviado a la base de datos (ya sea un instrucción SELECT o una instrucción INSERT/UPDATE) se trata como una instrucción independiente transacción y cualquier cambio se confirma automáticamente al final del estado- mento Este modo de confirmación automática se puede activar en PyMySQL usando: • connection.autocommit(True) activa la confirmación automática (False para desactivar confirmación automática, que es la predeterminada). Otros métodos asociados incluyen • connection.get_autocommit() que devuelve un valor booleano que indica si la confirmación automática está activada o no. • connection.begin() para comenzar explícitamente una nueva transacción. 24.4 Recursos en línea Consulte los siguientes recursos en línea para obtener más información sobre la base de datos de Python API: 288 24 Python DB-API
• https://www.python.org/dev/peps/pep-0249/ Especificación de la API de la base de datos de Python V2.0. • https://wiki.python.org/moin/DatabaseProgramming Programación de bases de datos en Pitón. • https://docs.python-guide.org/scenarios/db/ Bases de datos y Python. 24.4 Recursos en línea 289
capitulo 25 Módulo PyMySQL 25.1 El Módulo PyMySQL El módulo PyMySQL proporciona acceso a una base de datos MySQL desde Python. Él implementa Python DB-API v 2.0. Este módulo es una base de datos Python pura implementación de la interfaz, lo que significa que es portátil a través de diferentes operaciones sistemas; esto es notable porque algunos módulos de interfaz de base de datos son simplemente envoltorios alrededor de otras implementaciones (nativas) que pueden o no estar disponibles en diferentes sistemas operativos. Por ejemplo, una interfaz de base de datos nativa basada en Linux Es posible que el módulo de cara no esté disponible para el sistema operativo Windows. Si usted es nunca va a cambiar entre diferentes sistemas operativos, entonces esto no es un problema por supuesto. Para usar el módulo PyMySQL, deberá instalarlo en su computadora. Este implicará usar una herramienta como Anaconda o agregarla a su proyecto PyCharm. También puedes usar pip para instalarlo:
pip instalar PyMySQL 25.2 Trabajar con el módulo PyMySQL Para usar el módulo PyMySQL para acceder a una base de datos, deberá seguir estos pasos.
Importe el módulo.
Realice una conexión a la máquina host que ejecuta la base de datos y a la base de datos Tu estas usando.
Obtenga un objeto de cursor del objeto de conexión.
Ejecute algo de SQL utilizando el método cursor.execute(). © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_25 291
Obtenga los resultados del SQL usando el objeto del cursor (por ejemplo, fetchall, buscar muchos o buscar uno).
Cierre la conexión a la base de datos. Estos pasos son esencialmente placa de caldera, código que los usará cada vez que accede a una base de datos a través de PyMySQL (o, de hecho, cualquier módulo compatible con DB-API). Tomaremos cada uno de estos pasos a la vez. 25.2.1 Importación del módulo Como el módulo PyMySQL no es uno de los módulos integrados proporcionados por defecto con Python necesitará importar el módulo en su código, por ejemplo usando importar pymsql Tenga cuidado con el caso utilizado aquí ya que el nombre del módulo es pymysql en el código (¡Si intenta importar PyMySQL, Python no lo encontrará!). 25.2.2 Conéctese a la base de datos Cada módulo de base de datos tendrá sus propias especificaciones para conectarse a la base de datos. servidor; estos generalmente implican especificar la máquina en la que se ejecuta la base de datos (como las bases de datos pueden consumir muchos recursos, a menudo se ejecutan en un ordenador físico), el usuario a utilizar para la conexión y cualquier información de seguridad como una contraseña y la instancia de la base de datos a la que conectarse. En la mayoría de los casos un la base de datos es atendida por un sistema de gestión de bases de datos (un DBMS) que puede administrar múltiples instancias de base de datos y, por lo tanto, es necesario especificar qué instancia de base de datos que le interesa. Para MySQL, el servidor de base de datos MySQL es un DBMS que de hecho puede cuidar múltiples instancias de base de datos. Por lo tanto, la función pymysql.connect requiere la siguiente información al conectarse a la base de datos es: • El nombre de la máquina que aloja el servidor de base de datos MySQL, p. servidor de base de datos midominio.com. Si desea conectarse a la misma máquina que su Python se está ejecutando el programa, entonces puede usar localhost. Este es un nombre especial. reservado para la máquina local y evita que tenga que preocuparse por el nombre de su computadora local. • El nombre de usuario que se utilizará para la conexión. La mayoría de las bases de datos limitan el acceso a sus bases de datos a usuarios designados. Estos no son usuarios necesarios como humanos que inician sesión en un sistema, sino más bien entidades a las que se les permite conectarse a la base de datos y realizar determinadas operaciones. Por ejemplo, es posible que un usuario solo pueda leer datos en la base de datos donde otro usuario puede insertar nuevos datos en la 292 25 Módulo PyMySQL
base de datos. Estos usuarios se autentican solicitándoles que proporcionen un contraseña. • La contraseña del usuario. • La instancia de la base de datos a la que conectarse. Como se mencionó en el capítulo anterior, El sistema de administración de bases de datos (DMS) puede administrar múltiples instancias de bases de datos y, por lo tanto, es necesario decir en qué instancia de base de datos está interesado. Por ejemplo:
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’,’nombre de usuario’,‘contraseña’,‘uni- base de datos’) En este caso la máquina a la que nos estamos conectando es ‘localhost’ (que es lo mismo máquina en la que se ejecuta el propio programa Python), el usuario está representado por ’nombre de usuario’ y ‘contraseña’ y la instancia de la base de datos de interés se llama ‘uni-base de datos’. Esto devuelve un objeto Connection según el estándar DB-API. 25.2.3 Obtención del Objeto Cursor Puede obtener el objeto del cursor de la conexión usando el cursor() método:
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() 25.2.4 Uso del objeto Cursor Una vez que haya obtenido el objeto del cursor, puede usarlo para ejecutar una consulta SQL o una declaración de inserción, actualización o eliminación de DML. El siguiente ejemplo utiliza un simple declaración de selección para seleccionar todos los atributos en la tabla de estudiantes para todas las filas actualmente almacenado en la tabla de estudiantes:
ejecutar consulta SQL usando el método execute().
cursor.execute(‘SELECCIONAR * DE estudiantes’) Tenga en cuenta que este método ejecuta la declaración SELECT pero no devuelve el conjunto de resultados directamente. En su lugar, el método de ejecución devuelve un número entero que indica el número de filas afectadas por la modificación o devueltas como parte de la consulta. En el caso de una declaración SELECT, el número devuelto se puede usar para determinar qué tipo de método de obtención utilizar. 25.2 Trabajar con el módulo PyMySQL 293
25.2.5 Obtención de información sobre los resultados El Objeto Cursor también se puede utilizar para obtener información sobre los resultados a ser obtenido, como cuántas filas hay en los resultados y cuál es el tipo de cada atributo en los resultados: • cursor.rowcount() esta es una propiedad de solo lectura que indica el número de filas devueltas para una instrucción SELECT o filas afectadas por una ACTUALIZACIÓN o INSERTAR declaración. • cursor.description() esta es una propiedad de solo lectura que proporciona una descripción de cada atributo en el conjunto de resultados. Cada descripción proporciona la nombre del atributo y una indicación del tipo (a través de un type_code) también como información adicional sobre si el valor puede ser nulo o no y para números información de escala, precisión y tamaño. A continuación se muestra un ejemplo del uso de estas dos propiedades: imprimir(‘cursor.contador de filas’, cursor.contador de filas) print(‘cursor.descripción’, cursor.descripción) A continuación se muestra una muestra de la salida generada por estas líneas: cursor.rowcount 6 cursor.descripción ((‘id’, 3, Ninguno, 11, 11, 0, Falso), (’nombre’, 253, Ninguno, 180, 180, 0, Falso), (‘apellido’, 253, Ninguno, 180, 180, 0, Falso), (‘sujeto’, 253, Ninguno, 180, 180, 0, Falso), (‘correo electrónico’, 253, Ninguno, 180, 180, 0, Falso)) 25.2.6 Obtener resultados Ahora que se ha ejecutado una declaración SELECT exitosa contra la base de datos, podemos obtener los resultados. Los resultados se devuelven como una tupla de tuplas. Como se menciona en el En el último capítulo, hay varias opciones de búsqueda diferentes disponibles, incluida la búsqueda (), fetchmany(tamaño) y fetchall(). En el siguiente ejemplo usamos el opción fetchall() ya que sabemos que solo hay hasta seis filas que se pueden devuelto
Obtener todas las filas y luego iterar sobre los datos
datos = cursor.fetchall() para fila en datos: imprimir(‘fila:’, fila) En este caso, recorremos cada tupla dentro de la recopilación de datos e imprimimos esa fila afuera. Sin embargo, podríamos haber extraído fácilmente la información en la tupla 294 25 Módulo PyMySQL
en elementos individuales. Estos elementos podrían usarse para construir un objeto. que luego podría procesarse dentro de una aplicación, por ejemplo: para fila en datos: id, nombre, apellido, asunto, email = fila alumno = Alumno(id, nombre, apellidos, asunto, email) imprimir (estudiante) 25.2.7 Cerrar la conexión Una vez que haya terminado con la conexión a la base de datos, debe cerrarse.
desconectarse del servidor
conexión.cerrar() 25.3 Ejemplo completo de consulta de PyMySQL Una lista completa que ilustra la conexión a la base de datos, ejecutando un SELECT declaración e imprimir los resultados usando una clase de estudiante se da a continuación: importar pymysql estudiante de clase: def init(self, id, nombre, apellido, asunto, email): self.id = id self.nombre = nombre self.apellido = apellido self.sujeto = sujeto self.email = correo electrónico def str(uno mismo): return ‘Estudiante[’ + str(id) + ‘] ’ + nombre + ’ ’ + apellido + ’ - ’ + asunto + ’ ’ + email
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’, ‘usuario’, ‘contraseña’, ‘uni-base de datos’)
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() 25.2 Trabajar con el módulo PyMySQL 295
La salida de este programa, para la base de datos creada en el último capítulo es se muestra aquí: cursor.rowcount 6 cursor.descripción ((‘id’, 3, Ninguno, 11, 11, 0, Falso), (’nombre’, 253, Ninguno, 180, 180, 0, Falso), (‘apellido’, 253, Ninguno, 180, 180, 0, Falso), (‘sujeto’, 253, Ninguno, 180, 180, 0, Falso), (‘correo electrónico’, 253, Ninguno, 180, 180, 0, Falso)) Estudiante[1] Phoebe Cooke - Animación pc@my.com Estudiante[2] Gryff Jones - Juegos grj@my.com Estudiante[3] Adam Fosh - Música af@my.com Estudiante[4] Jasmine Smith - Juegos js@my.com Estudiante[5] Tom Jones - Música tj@my.com Estudiante[6] James Andrews - Juegos ja@my.com 25.4 Insertar datos en la base de datos Además de leer datos de una base de datos, muchas aplicaciones también necesitan agregar nuevos datos a la base de datos. Esto se hace a través del DML (lenguaje de manipulación de datos) INSERTAR declaración. El proceso para esto es muy similar a ejecutar una consulta contra la base de datos usando una instrucción SELECT; es decir, necesitas hacer una conexión, obtener un objeto de cursor y ejecutar la declaración. La única diferencia aquí es que usted no es necesario buscar los resultados.
ejecutar consulta SQL usando el método execute().
cursor.execute(‘SELECCIONAR * DE estudiantes’) imprimir(‘cursor.contador de filas’, cursor.contador de filas) print(‘cursor.descripción’, cursor.descripción)
Obtener todas las filas y luego iterar sobre los datos
datos = cursor.fetchall() para fila en datos: student_id, nombre, apellido, asunto, email = fila estudiante = Estudiante(student_id, nombre, apellido, materia, correo electrónico) imprimir (estudiante)
desconectarse del servidor
conexión.cerrar() 296 25 Módulo PyMySQL
El resultado de ejecutar este código es que la base de datos se actualiza con una séptima fila por ‘Denise Byrne’. Esto se puede ver en MySQL Workbench si miramos el contenido de la tabla de estudiantes: Hay un par de puntos a tener en cuenta sobre este ejemplo de código. La primera es que nosotros han utilizado las comillas dobles alrededor de la cadena que define el comando INSERT— esto se debe a que una cadena de comillas dobles nos permite incluir comillas simples dentro de esa cadena. Esto es necesario ya que necesitamos citar cualquier valor de cadena pasado al base de datos (como ‘Denise’). La segunda cosa a tener en cuenta es que, por defecto, la interfaz de la base de datos PyMySQL requiere que el programador decida cuándo confirmar o revertir una transacción. Una transacción se introdujo en el último capítulo como una unidad atómica de trabajo que debe puede completarse o en su totalidad o retrotraerse para que no se realicen cambios. Sin embargo, la forma en que indicamos que una transacción está completada es llamando importar pymysql
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’, ‘usuario’, ‘contraseña’, ‘uni-base de datos’)
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() intentar:
Ejecutar el comando INSERTAR
cursor.execute(“INSERTAR EN Alumnos (id, nombre, apellido, asunto, correo electrónico) VALORES (7, ‘Denise’, ‘Byrne’, ‘Historia’, ‘db@mi.com’)”)
Confirmar los cambios en la base de datos
conexión.commit() excepto:
Algo salió mal
revertir los cambios
conexión.rollback()
Cerrar la conexión a la base de datos
conexión.cerrar() 25.4 Insertar datos en la base de datos 297
el método commit() en la conexión de la base de datos. A su vez podemos indicar que nosotros desea revertir la transacción actual llamando a rollback(). En cualquier caso, una vez que se ha invocado el método, se inicia una nueva transacción para cualquier otro actividad de la base de datos. En el código anterior, hemos usado un bloque de prueba para asegurarnos de que si todo tiene éxito, confirmaremos los cambios realizados, pero si se lanza una excepción (de cualquier tipo) revertiremos la transacción; este es un patrón común. 25.5 Actualización de datos en la base de datos Si somos capaces de insertar nuevos datos en la base de datos, es posible que también queramos actualizar la datos en una base de datos, por ejemplo para corregir alguna información. Esto se hace usando el instrucción UPDATE que debe indicar qué fila existente se está actualizando como así como cuáles deberían ser los nuevos datos.
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’, ‘usuario’, ‘contraseña’, ‘uni-base de datos’)
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() intentar:
Ejecutar el comando ACTUALIZAR
cursor.execute(“ACTUALIZAR estudiantes SET email = ‘denise@my.com’ DONDE id = 7”)
Confirmar los cambios en la base de datos
conexión.commit() excepto:
revertir los cambios si hay una excepción/error
conexión.rollback()
Cerrar la conexión a la base de datos
conexión.cerrar() importar pymysql En este ejemplo, estamos actualizando al estudiante con id 7 para que su correo electrónico la dirección se cambiará a ‘denise@my.com’. Esto se puede comprobar examinando el contenido de la tabla de estudiantes en MySQL Workbench: 298 25 Módulo PyMySQL
25.6 Eliminación de datos en la base de datos Finalmente, también es posible eliminar datos de una base de datos, por ejemplo si un estudiante deja su curso. Esto sigue el mismo formato que los dos ejemplos anteriores con la diferencia de que se usa la instrucción DELETE en su lugar: importar pymysql
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’, ‘usuario’, ‘contraseña’, ‘uni-base de datos’)
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() intentar:
Ejecutar el comando ELIMINAR
cursor.execute(“ELIMINAR DE estudiantes DONDE id = 7”)
Confirmar los cambios en la base de datos
conexión.commit() excepto:
revertir los cambios si hay una excepción/error
conexión.rollback()
Cerrar la conexión a la base de datos
conexión.cerrar() En este caso hemos borrado al alumno con id 7. Lo podemos ver de nuevo en el MySQL Workbench examinando el contenido de la tabla de estudiantes después de este código ha corrido: 25.6 Eliminación de datos en la base de datos 299
25.7 Creación de tablas No son solo datos que puede agregar a una base de datos; si lo deseas puedes programar- crear automáticamente nuevas tablas para usarlas con una aplicación. Este proceso sigue exactamente el mismo patrón que los utilizados para INSERTAR, ACTUALIZAR y ELIMINAR. El la única diferencia es que el comando enviado a la base de datos contiene un CREAR instrucción con una descripción de la tabla que se va a crear. Esto se ilustra a continuación: importar pymysql
Abrir conexión a la base de datos
conexión = pymysql.connect(’localhost’, ‘usuario’, ‘contraseña’, ‘uni-base de datos’)
preparar un objeto de cursor usando el método cursor()
cursor = conexión.cursor() intentar:
Ejecutar comando CREAR
cursor.execute(“CREATE TABLE log (mensaje VARCHAR(100) NO NULO)”)
Confirmar los cambios en la base de datos
conexión.commit() excepto:
revertir los cambios si hay una excepción/error
conexión.rollback()
Cerrar la conexión a la base de datos
conexión.cerrar() Esto crea un nuevo registro de tabla dentro de la base de datos uni; esto se puede ver mirando en las tablas enumeradas para la base de datos uni dentro de MySQL Workbench. 300 25 Módulo PyMySQL
25,8 Recursos en línea Consulte los siguientes recursos en línea para obtener más información sobre la base de datos de Python API: • https://pymysql.readthedocs.io/en/latest/ Sitio de documentación de PyMySQL. • https://github.com/PyMySQL/PyMySQL Repositorio del concentrador Git para PyMySQL biblioteca. 25,9 Ejercicios En este ejercicio, creará una base de datos y tablas basadas en un conjunto de transacciones. almacenado en una cuenta corriente. Puede usar la clase de cuenta que creó en el CSV y el capítulo de Excel para esto. Necesitará dos tablas, una para la información de la cuenta y otra para la Historial de transacciones. La clave principal de la tabla de información de la cuenta se puede utilizar como clave externa para la tabla de historial de transacciones. Luego escriba una función que tome un objeto Cuenta y llene las tablas con los datos correspondientes. Para crear la tabla de información de la cuenta, puede usar el siguiente DDL: Recuerde tener cuidado con los números enteros y decimales si está creando un SQL cadena como: declaración = “INSERTAR en transacciones (idtransactions, type, cantidad, cuenta) VALORES (” + str(id) + “, ‘” + acción + “’, " + str(cantidad) + “, " + str(número_cuenta) + “)” CREAR TABLA acc_info (idacc_info INT NOT NULL, nombre VARCHAR(255) NO NULO, CLAVE PRINCIPAL (idacc_info)) Mientras que para la tabla de transacciones puede usar: transacciones CREATE TABLE (idtransactions INT NOT NULL, tipo VARCHAR(45) NO NULO, monto VARCHAR(45) NO NULO, cuenta INT NO NULO, CLAVE PRIMARIA (idtransactions))” 25,8 Recursos en línea 301
Parte VI Inicio sesión
capitulo 26 Introducción al registro 26.1 Introducción Muchos lenguajes de programación tienen bibliotecas de registro comunes, incluidas Java y C# y, por supuesto, Python también tiene un módulo de registro. De hecho, el registro de Python El módulo ha sido parte de los módulos integrados desde Python 2.3. Este capítulo explica por qué debe agregar el registro a sus programas, lo que debería (y no debería) iniciar sesión y por qué simplemente usar la función print() no es suficiente 26.2 ¿Por qué iniciar sesión? El registro suele ser un aspecto clave de cualquier aplicación de producción; esto es porque es importante proporcionar información adecuada para permitir futuras investigaciones después de algún evento o problema en dichas aplicaciones. Estas investigaciones incluyen: • Diagnóstico de fallas; es por eso que una aplicación falló/se bloqueó. • Identificar comportamientos inusuales o inesperados; que podría no causar la aplicación falle pero que puede dejarla en un estado inesperado o donde los datos puede estar dañado, etc. • Identificar problemas de rendimiento o capacidad; en tales situaciones la aplicación se está desempeñando como se esperaba porque no cumple con algunos requisitos no funcionales asociado con la velocidad a la que está operando o su capacidad de escalar como el crece la cantidad de datos o el número de usuarios. • Tratar con un intento de comportamiento malicioso en el que algún agente externo es intentar afectar el comportamiento del sistema o adquirir información que no deberían tener acceso a etc. Esto podría suceder, por ejemplo, si usted está creando una aplicación web de Python y un usuario intenta piratear su servidor web. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_26 305
• Cumplimiento normativo o legal. En algunos casos, registros de ejecución del programa. puede ser necesario por razones reglamentarias o legales. Esto es particularmente cierto de la sector financiero donde los registros deben mantenerse durante muchos años en caso de que haya un necesidad de investigar el comportamiento de las organizaciones o de los individuos. 26.3 ¿Cuál es el propósito del registro? En general, por lo tanto, hay dos razones generales para registrar lo que está haciendo una aplicación durante su funcionamiento: • Con fines de diagnóstico, de modo que los eventos/pasos registrados se puedan utilizar para analizar el comportamiento del sistema cuando algo sale mal. • Con fines de auditoría que permitan un posterior análisis del comportamiento del sistema para comerciales, legales o reglamentarios. Por ejemplo, en este caso para determinar quién hizo qué con qué y cuándo. Sin dicha información registrada, es imposible saber después del evento qué sucedió. Por ejemplo, si todo lo que sabe es que una aplicación se bloqueó (inesperadamente dejó de ejecutarse rápidamente) ¿cómo puede determinar en qué estado se encontraba la aplicación, ¿Qué funciones, métodos, etc. se estaban ejecutando y qué declaraciones se ejecutan? Recuerde que aunque un desarrollador puede haber estado usando un IDE para ejecutar su aplicaciones durante el desarrollo y posiblemente haya estado usando la función de depuración instalaciones disponibles que le permiten ver qué funciones o métodos, declaraciones y incluso los valores variables son lugar; no es así como funcionan la mayoría de los sistemas de producción. En En general, un sistema Python de producción se ejecutará desde una línea de comando o posiblemente a través de un atajo (en un cuadro de Windows) para simplificar la ejecución del programa. Todo lo que el usuario sabrá es que algo falló o que el comportamiento que esperaba no ocurrió, ¡si es que están al tanto de cualquier problema! Por lo tanto, los registros son clave para el análisis posterior al evento de fallas, comportamiento inesperado o para el análisis del funcionamiento del sistema por motivos comerciales. 26.4 ¿Qué debe registrar? Una pregunta que podría estar considerando en este momento es “¿qué información ¿debo registrarme?’. Una aplicación debe registrar suficiente información para que los investigadores posteriores al evento puede entender lo que estaba sucediendo, cuándo y dónde. En general esto significa que querrá registrar la hora del mensaje de registro, el módulo/nombre de archivo, la función nombre o nombre del método en ejecución, potencialmente el nivel de registro que se está utilizando (ver más adelante) y en algunos casos, los valores de los parámetros/estado del entorno, programa o clase involucrado. 306 26 Introducción al registro
En muchos casos, los desarrolladores registran la entrada (y en menor medida) la salida de un función o método. Sin embargo, también puede ser útil registrar lo que sucede en la sucursal. puntos dentro de una función o método para que la lógica de la aplicación pueda ser seguido. Todas las aplicaciones deben registrar todos los errores/excepciones. Aunque se necesita cuidado para asegurarse de que esto se haga correctamente. Por ejemplo, si se detecta una excepción y luego se lanza varias veces, no es necesario registrarlo cada vez que se atrapa. En efecto hacer esto puede hacer que los archivos de registro sean mucho más grandes, causar confusión cuando el problema es están siendo investigados y dan como resultado gastos generales innecesarios. Un enfoque común es registrar una excepción donde se genera y captura por primera vez y no registrarla después de eso. 26.5 Qué no registrar La siguiente pregunta a considerar es “¿qué información no debo registrar?”. Un área general que no se debe registrar es cualquier información personal o confidencial, incluida Cualquier información que pueda ser utilizada para identificar a un individuo. Este tipo de información se conoce como PII o información de identificación personal. Tal información incluye • identificaciones de usuario y contraseñas, • correos electrónicos, • datos de nacimiento, lugar de nacimiento, • información financiera de identificación personal, como detalles de cuentas bancarias, crédito datos de la tarjeta, etc., • información biométrica, • información médica/de salud, • información personal emitida por el gobierno, como detalles del pasaporte, licencia de conducir número, números de seguro social, números de seguro nacional, etc., • información organizativa oficial, como registros profesionales y membresías; números de beca, • direcciones físicas, números de teléfono (línea fija), números de teléfono móvil, • información relacionada con la verificación, como el apellido de soltera de la madre, nombres de mascotas, escuela, primera escuela, película favorita, etc., • también incluye cada vez más información en línea relacionada con las redes sociales, como Cuentas de Facebook o LinkedIn. Todo lo anterior es información confidencial y gran parte de ella se puede utilizar para identificar un individuo; ninguna de esta información debe registrarse directamente. Eso no significa que no pueda y no deba registrar que un usuario inició sesión; tú bien puede necesitar hacer eso. Sin embargo, la información debería al menos estar ofuscada. y no debe incluir ninguna información no requerida. Por ejemplo, puede grabar que un usuario representado por alguna identificación intentó iniciar sesión en un momento específico y si tuvieron éxito o no. Sin embargo, no debe registrar su contraseña y puede 26.4 ¿Qué debe registrar? 307
no registre el ID de usuario real; en su lugar, puede registrar un ID que se puede usar para asignar a su ID de usuario real. También debe tener cuidado al registrar directamente la entrada de datos en una aplicación. directamente en un archivo de registro. Una forma en que un agente malicioso puede atacar una aplicación (particularmente una aplicación web) es al intentar enviar cantidades muy grandes de datos (como parte de un campo o como parámetro de una operación). Si la aplicación registra ciegamente todos los datos que se le envían, entonces los archivos de registro pueden llenarse muy rápidamente. Este puede hacer que la aplicación utilice el almacén de archivos y se llene y provoque problemas potenciales para todo el software que utiliza el mismo almacén de archivos. Esta forma de ataque es conocido como ataque de inyección de registro (o archivo de registro) y está bien documentado (ver https:// www.owasp.org/index.php/Log_Injection que es parte del muy respetado Open Proyecto de Seguridad de Aplicaciones Web). Otro punto a tener en cuenta es que no basta con registrar un error. Esto no es manejo de errores; registrar un error no significa que lo haya manejado; solo que tu haberlo notado. Una aplicación aún debe decidir cómo debe gestionar el error o excepción. En general, también debe apuntar a registros vacíos en un sistema de producción; eso es solo se debe registrar la información que debe registrarse en un sistema de producción (a menudo información sobre errores, excepciones u otro comportamiento inesperado). Sin embargo, durante la prueba se requiere mucho más detalle para que la ejecución del se debe seguir el sistema. Por lo tanto, debería ser posible seleccionar cuánto la información se registra según el entorno en el que se ejecuta el código (es decir, dentro de un entorno de prueba o dentro de un entorno de producción). Un último punto a tener en cuenta es que es importante registrar la información en el lugar correcto. Muchas aplicaciones (y organizaciones) registran información general en un archivo de registro, los errores y las excepciones a otro y la información de seguridad a un tercero. Por lo tanto es importante saber dónde se envía la información de registro y dónde no enviarla información al registro incorrecto. 26.6 ¿Por qué no usar simplemente la impresión? Suponiendo que desea registrar información en su aplicación, la siguiente pregunta es ¿Cómo deberías hacer eso? A lo largo de este libro hemos estado usando Python print() función para imprimir información que indica resultados generados por nuestro código pero también a veces lo que está pasando con una función o un método, etc. Por lo tanto, debemos considerar si usar la función print() es la mejor manera de registrar información. De hecho, usar print() para registrar información en un sistema de producción es casi nunca la respuesta correcta, esto es por varias razones: • La función print() por defecto escribe cadenas en la salida estándar (stdout) o salida de error estándar (stderr) que por defecto dirige la salida a la consola/ Terminal. Por ejemplo, cuando ejecuta una aplicación dentro de un IDE, la salida es 308 26 Introducción al registro
que se muestra en la ventana Consola. Si ejecuta una aplicación desde el comando entonces la salida se dirige de nuevo a esa ventana de comando/terminal. Ambos estos están bien durante el desarrollo, pero ¿qué pasa si el programa no se ejecuta desde un ventana de comandos, tal vez en su lugar es iniciado por el sistema operativo automáticamente (como es típico de numerosos servicios como un servicio de impresión o un sitio web servidor). En este caso, no hay una ventana de terminal/consola a la que enviar los datos; en cambio, los datos simplemente se pierden. Como sucede, los flujos de salida stdout y stderr puede ser dirigido a un archivo (o archivos). Sin embargo, esto generalmente se hace cuando el se inicia el programa y se puede omitir fácilmente. Además solo está el opción de enviar todos los stdout a un archivo específico o todos los errores de salida al stderr. • Otro problema con el uso de la función print() es que todas las llamadas a print serán producción. Al usar la mayoría de los registradores, es posible especificar el nivel de registro requerido. Estos diferentes niveles de registro permiten generar diferentes cantidades de información dependiendo del escenario. Por ejemplo, en una producción confiable bien probada sistema, es posible que solo deseemos que se registre información crítica o relacionada con errores. Este reducirá la cantidad de información que recopilamos y reducirá cualquier impacto en el rendimiento introducido al iniciar sesión en la aplicación. Sin embargo, durante fases de prueba, es posible que deseemos un nivel de registro mucho más detallado. • En otras situaciones, es posible que deseemos cambiar el nivel de registro que se utiliza para una ejecución sistema de producción sin necesidad de modificar el código real (ya que esto tiene la potencial de introducir errores en el código). En su lugar, nos gustaría tener la Facilidad para cambiar externamente la forma en que se comporta el sistema de registro, por ejemplo a través de un archivo de configuración. Esto permite a los administradores del sistema modificar la cantidad y el detalle de la información que se está registrando. normalmente también permite cambiar la designación de la información de registro. • Finalmente, al usar la función print(), un desarrollador puede usar cualquier formato les gusta, pueden incluir una marca de tiempo en el mensaje o no, pueden incluir el nombre del módulo o función/método o no pueden incluir parámetros de no. El uso de un sistema de registro por lo general estandariza la información generada a lo largo con el mensaje de registro. Por lo tanto, todos los mensajes de registro tendrán (o no tendrán) un tiempo- tamp, o todos los mensajes incluirán (o no incluirán) información sobre la función o método en el que se generaron, etc. 26.7 Recursos en línea Para obtener más información sobre el registro, consulte lo siguiente: • https://en.wikipedia.org/wiki/Log_file Una página de wikipedia sobre registro. • https://www.codeproject.com/Articles/42354/The-Art-of-Logging Un interés- ing artículo sobre el arte de la tala. • www.owasp.org/index.php El Abierto Web Solicitud Seguridad Proyecto (OWASP). 26.6 ¿Por qué no usar simplemente la impresión? 309
capitulo 27 Iniciar sesión en Python 27.1 El módulo de registro Python ha incluido un módulo de registro integrado desde Python 2.3. Este módulo, el módulo de registro, define funciones y clases que implementan un registro flexible ging framework que se puede usar en cualquier aplicación/script de Python o en Python bibliotecas/módulos. Aunque los diferentes marcos de registro difieren en los detalles específicos de lo que oferta; casi todos ofrecen los mismos elementos centrales (aunque se usan diferentes nombres). a veces se usa). El módulo de registro de Python no es diferente y los elementos centrales que componen el marco de registro y su canalización de procesamiento se muestran a continuación (Tenga en cuenta que se podría dibujar un diagrama muy similar para los marcos de registro en Java, Scala, C++, etc.). El siguiente diagrama ilustra un programa de Python que usa el Python incorporado marco de registro para registrar mensajes en un archivo. Los elementos centrales del marco de registro (algunos de los cuales son opcionales) son se muestra arriba y se describe a continuación: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_27 311
• Mensaje de registro El es el mensaje que se registrará desde la aplicación. • Registrador Proporciona el punto de entrada/interfaz de los programadores al sistema de registro. La clase Logger proporciona una variedad de métodos que se pueden usar para registrar mensajes en diferentes niveles • Controlador Los controladores determinan dónde enviar un mensaje de registro, controladores predeterminados incluyen controladores de archivos que envían mensajes a un archivo y controladores HTTP que envían mensajes a un servidor web. • Filtro Este es un elemento opcional en la canalización de registro. Se pueden usar para filtrar aún más la información que se va a registrar proporcionando un control detallado de qué mensajes de registro se envían realmente (por ejemplo, a un archivo de registro). • Formateador Se utilizan para formatear el mensaje de registro según sea necesario. Esto puede implica agregar marcas de tiempo, módulo e información de función/método, etc. al mensaje de registro original. • Información de configuración El registrador (y los controladores, filtros y formateadores) se pueden configurar mediante programación en Python o mediante archivos de configuración. Estos archivos de configuración se pueden escribir usando clave-valor pares o en un archivo YAML (que es un lenguaje de marcado simple). YAML significa ¡Otro lenguaje de marcado más! Vale la pena señalar que gran parte del marco de registro está oculto para el desarrollador que realmente solo ve el registrador; el resto de la tubería de registro es ya sea configurado por defecto o a través de información de configuración de registro típicamente en el forma de un archivo de configuración de registro. 27.2 el registrador El registrador proporciona la interfaz de programadores para la canalización de registro. Un objeto Logger se obtiene de la función getLogger() definida en el módulo de registro. El siguiente fragmento de código ilustra la adquisición del valor predeterminado logger y usarlo para registrar un mensaje de error. Tenga en cuenta que el módulo de registro debe ser importado: El resultado de esta breve aplicación se registra en la consola, ya que es el configuración por defecto: Esto debería usarse con algo inesperado. registro de importación registrador = registro.getLogger() logger.error(‘Esto debería usarse con algo inesperado’ 312 27 Iniciar sesión en Python
27.3 Control de la cantidad de información registrada Los mensajes de registro en realidad están asociados con un nivel de registro. Estos niveles de registro están destinados para indicar la gravedad del mensaje que se está registrando. Hay seis registros diferentes. niveles asociados con el marco de registro de Python, estos son: • NOTSET En este nivel, no se lleva a cabo ningún registro y el registro se activa de forma efectiva. apagado. • DEBUG Este nivel está destinado a proporcionar información detallada, típicamente de interés cuando un desarrollador está diagnosticando un error o problemas dentro de una aplicación. • INFO Se espera que este nivel proporcione menos detalles que el nivel de registro DEBUG como se espera que proporcione información que pueda utilizarse para confirmar que el la aplicación funciona como se esperaba. • ADVERTENCIA Se utiliza para proporcionar información sobre un evento inesperado o una indicación de algún problema probable que un desarrollador o administrador del sistema tal vez desee investigar más a fondo. • ERROR Se utiliza para proporcionar información sobre algún tema o problema grave que la aplicación no ha podido manejar y eso probablemente signifique que la aplicación no puede funcionar correctamente. • CRÍTICO Este es el nivel más alto de emisión y está reservado para situaciones críticas. ciones como aquellas en las que el programa ya no puede seguir ejecutándose. Los niveles de registro son relativos entre sí y están definidos en una jerarquía. cada registro nivel tiene un valor numérico asociado con él como se muestra a continuación (aunque debe nunca es necesario utilizar los números). Por lo tanto, INFO es un nivel de registro más alto que DEBUG, en turn ERROR es un nivel de registro más alto que WARNING, INFO, DEBUG, etc. Asociado con el nivel de registro con el que se registra un mensaje, un registrador también tiene un nivel de registro asociado con él. El registrador procesará todos los mensajes que están en el nivel de registro de los registradores o por encima de ese nivel. Por lo tanto, si un registrador tiene un nivel de registro de ADVERTENCIA luego registrará todos los mensajes registrados usando la advertencia, error y crítico niveles de registro. En términos generales, una aplicación no utilizará el nivel DEBUG en una producción sistema. Esto generalmente se considera inapropiado ya que solo está diseñado para depurar escenarios. El nivel INFO puede considerarse apropiado para un sistema de producción aunque es probable que produzca grandes cantidades de información, ya que normalmente rastrea la ejecución de funciones y métodos. Si una aplicación ha sido bien probada y verificado, entonces solo son realmente las advertencias y los errores los que deberían ocurrir o ser motivo de preocupación. Por lo tanto, no es raro que el valor predeterminado sea el nivel de ADVERTENCIA para la producción. 27.3 Control de la cantidad de información registrada 313
sistemas (de hecho, esta es la razón por la cual el nivel de registro predeterminado se establece en ADVERTENCIA dentro del sistema de registro de Python). Si ahora observamos el siguiente código que obtiene el objeto registrador predeterminado y luego usa varios métodos de registro diferentes, podemos ver el efecto de los niveles de registro en La salida: registro de importación registrador = registro.getLogger() logger.debug(‘Esto es para ayudar con la depuración’) logger.info(‘Esto es solo para información’) logger.warning(’¡Esto es una advertencia!’) logger.error(‘Esto debería usarse con algo inesperado’) logger.critical(‘Algo serio’) El nivel de registro predeterminado se establece en advertencia y, por lo tanto, solo los mensajes registrados en el se imprimirá el nivel de advertencia o superior: ¡Esto es una advertencia! Esto debería usarse con algo inesperado. Algo serio Como se puede ver en esto, los mensajes registrados en el nivel de depuración e información han sido ignorado Sin embargo, el objeto Logger nos permite cambiar el nivel de registro mediante programación. utilizando el método setLevel(), por ejemplo, logger.setLevel(logging. DEBUG) o a través de logging.basicConfig(level = logging.DEBUG) función; ambos establecerán el nivel de registro en DEBUG. Tenga en cuenta que el nivel de registro debe configurarse antes de obtener el registrador. Si agregamos uno de los enfoques anteriores para establecer el nivel de registro en el anterior programa cambiaremos la cantidad de información de registro generada: registro de importación registro.basicConfig(nivel=registro.DEBUG) registrador = registro.getLogger() logger.warning(’¡Esto es una advertencia!’) logger.info(‘Esto es solo para información’) logger.debug(‘Esto es para ayudar con la depuración’) logger.error(‘Esto debería usarse con algo inesperado logger.critical(‘Algo serio’) 314 27 Iniciar sesión en Python
Esto ahora generará todos los mensajes de registro ya que la depuración es el nivel de registro más bajo. Por supuesto, podemos desactivar el registro configurando el nivel de registro en NOTSET registrador.setLevel(registro.NOTSET) Alternativamente, puede establecer el atributo Loggers disabled en True: registro.Logger.disabled = Verdadero 27.4 Métodos de registro La clase Logger proporciona una serie de métodos que se pueden utilizar para controlar lo que se registrado incluyendo: • setLevel(nivel) Establece el nivel de registro de este registrador. • getEffectiveLevel() Devuelve el nivel de registro de este registrador. • isEnabledFor(level) Comprueba si este registrador está habilitado para el registro nivel especificado. • depuración (mensaje) registra mensajes en el nivel de depuración. • info(mensaje) registra mensajes en el nivel de información. • advertencia (mensaje) registra mensajes en el nivel de advertencia. • error(mensaje) registra mensajes en el nivel de error. • Critical(mensaje) registra los mensajes en el nivel crítico. • excepción(mensaje) Este método registra un mensaje en el nivel de error. Sin embargo, solo se puede usar dentro de un controlador de excepciones e incluye una pila rastro de cualquier excepción asociada, por ejemplo: registro de importación registrador = registro.getLogger() intentar: imprimir(‘comenzando’) X = 1 / 0 imprimir (x) excepto: logger.exception(‘un mensaje de excepción’) imprimir(‘Terminado’) • log(nivel, mensaje) registra los mensajes en el nivel de registro especificado como el primero parámetro. 27.3 Control de la cantidad de información registrada 315
Además, existen varios métodos que se utilizan para administrar los controladores y filtros: • addFilter(filtro) Este método agrega el filtro de filtro especificado a este registrador • removeFilter(filtro) El filtro especificado se elimina de este registrador objeto. • addHandler(handler) El controlador especificado se agrega a este registrador. • removeHandler(handler) Elimina el controlador especificado de este registrador 27.5 Registrador predeterminado Un registrador predeterminado (o raíz) siempre está disponible desde el marco de registro. Se puede acceder a este registrador a través de las funciones definidas en el módulo de registro. Estas funciones permiten que los mensajes se registren en diferentes niveles utilizando métodos como como info(), error(), advertencia() pero sin necesidad de obtener una referencia a primero un objeto registrador. Por ejemplo: registro de importación
Establecer el nivel del registrador raíz
registro.basicConfig(nivel=registro.DEBUG)
Usar registrador raíz (predeterminado)
logging.debug(‘Esto es para ayudar con la depuración’) logging.info(‘Esto es solo para información’) logging.warning(’¡Esto es una advertencia!’) logging.error(‘Esto debe usarse con algo inesperado’ logging.critical(‘Algo serio’) Este ejemplo establece el nivel de registro para el registrador raíz o predeterminado en DEBUG (el el valor predeterminado es ADVERTENCIA). A continuación, utiliza el registrador predeterminado para generar un rango de registro mensajes a diferentes niveles (desde DEBUG hasta CRITICAL). La salida de este programa se da a continuación: DEBUG:root:Esto es para ayudar con la depuración INFO:root:Esto es solo para información ADVERTENCIA:raíz:¡Esto es una advertencia! ERROR:root:Esto debería usarse con algo inesperado CRÍTICO:raíz:Algo serio Tenga en cuenta que el formato utilizado por defecto con el registrador raíz imprime el nivel de registro, el nombre del registrador que genera la salida y el mensaje. A partir de esto se puede ver que es la raíz más larga la que genera la salida. 316 27 Iniciar sesión en Python
27.6 Registradores de nivel de módulo La mayoría de los módulos no usarán el registrador raíz para registrar información, sino que usarán un registrador de nivel de módulo o con nombre. Tal registrador se puede configurar independientemente de el registrador raíz. Esto permite a los desarrolladores activar el registro solo para un módulo en lugar de que para una aplicación completa. Esto puede ser útil si un desarrollador desea investigar un problema que se encuentra dentro de un solo módulo. Los ejemplos de código anteriores en este capítulo han usado la función getLogger() sin parámetros para obtener un objeto registrador, por ejemplo: registrador = registro.getLogger() Esta es realmente otra forma de obtener una referencia al registrador raíz que es utilizado por las funciones de registro independientes como logging.info(), log- función ging.debug(), por lo tanto: logging.warning(‘mi advertencia’) y yo logger.warning(‘mi advertencia’ Tener exactamente el mismo efecto; la única diferencia es que la primera versión implica menos código. Sin embargo, también es posible crear un registrador con nombre. Este es un registrador separado objeto que tiene su propio nombre y potencialmente puede tener su propio nivel de registro, controladores y formateadores, etc. Para obtener un registrador con nombre, pase una cadena de nombre en el método getLogger(): logger1 = logging.getLogger(‘mi registrador’) Esto devuelve un objeto registrador con el nombre ‘mi registrador’. Tenga en cuenta que esto puede ser un nuevo objeto registrador, sin embargo, si cualquier otro código dentro del sistema actual tiene solicitó previamente un registrador llamado ‘mi registrador’, entonces ese objeto registrador será volvió al código actual. Por lo tanto, múltiples llamadas a getLogger() con el mismo name siempre devolverá una referencia al mismo objeto Logger. Es una práctica común utilizar el nombre del módulo como nombre del registrador; ya que solo debe existir un módulo con un nombre específico dentro de cualquier sistema específico. No es necesario codificar el nombre del módulo, ya que se puede obtener usando el atributo del módulo name, por lo tanto, es común ver: registrador2 = registro.getLogger(nombre) 27.6 Registradores de nivel de módulo 317
Podemos ver el efecto de cada una de estas declaraciones al imprimir cada registrador: registrador = registro.getLogger() print(‘Registrador raíz:’, registrador) logger1 = logging.getLogger(‘mi registrador’) print(‘Registrador con nombre:’, registrador1) registrador2 = registro.getLogger(nombre) print(‘Registrador de módulo:’, registrador2) Cuando se ejecuta el código anterior, la salida es: Registrador raíz: <raíz de RootLogger (ADVERTENCIA)> Registrador con nombre: <Registrador mi registrador (ADVERTENCIA)> Registrador del módulo: <Registrador principal (ADVERTENCIA)> Esto muestra que cada registrador tiene su propio nombre (el código se ejecutó en el principal módulo y, por lo tanto, el nombre del módulo era main) y los tres registradores tienen un nivel de registro efectivo de ADVERTENCIA (que es el predeterminado). 27.7 Jerarquía de registradores De hecho, existe una jerarquía de registradores con el registrador raíz en la parte superior de esta jerarquía. Todos los registradores con nombre están debajo del registrador raíz. El nombre de un registrador puede ser en realidad un valor jerárquico separado por puntos como como util, util.lib y util.lib.printer. Registradores que están más abajo la jerarquía son hijos de registradores más arriba en la jerarquía de registradores. Por ejemplo, dado un registrador llamado lib, estará debajo del registrador raíz pero sobre el registrador con el nombre util.lib. Este registrador estará a su vez por encima del registrador llamado util.lib.printer. Esto se ilustra en el siguiente diagrama: 318 27 Iniciar sesión en Python
La jerarquía de nombres de registradores es análoga a la jerarquía de paquetes de Python, y idéntico a él si organiza sus registradores por módulo usando el rec- construcción recomendada logging.getLogger(name). Esta jerarquía es importante cuando se considera el nivel de registro. Si un nivel de registro no ha se ha configurado para el registrador actual, entonces mirará a su padre para ver si ese registrador tiene un nivel de registro establecido. Si lo hace, ese será el nivel de registro utilizado. Esta búsqueda respalda el registrador jerarquía continuará hasta que se encuentre un nivel de registro explícito o hasta que el registrador raíz sea encontrado que tiene un nivel de registro predeterminado de ADVERTENCIA. Esto es útil ya que no es necesario establecer explícitamente el nivel de registro para cada registrador objeto utilizado en una aplicación. En su lugar, solo es necesario establecer el nivel de registro raíz (o para una jerarquía de módulos, un punto apropiado en la jerarquía de módulos). Esto puede entonces anularse cuando se requiera específicamente. 27,8 Formateadores Hay dos niveles en los que puede formatear los mensajes registrados, estos están dentro el mensaje de registro pasado a un método de registro (como info() o warn()) y a través de la configuración de nivel superior que indica qué información adicional se puede agregar al mensaje de registro individual. 27.8.1 Formateo de mensajes de registro El mensaje de registro puede tener caracteres de control que indiquen qué valores deben ser colocado dentro del mensaje, por ejemplo: logger.warning(’%s está establecido en %d’, ‘count’, 42) Esto indica que la cadena de formato espera recibir una cadena y un número. El Los parámetros que se sustituirán en la cadena de formato siguen la cadena de formato como un lista de valores separados por comas. 27.8.2 Formato de salida de registro La canalización de registro se puede configurar para incorporar información estándar con cada mensaje de registro. Esto se puede hacer globalmente para todos los controladores. También es posible establecer mediante programación un formateador específico en un controlador individual; esto se discute en la siguiente sección. Para configurar globalmente el formato de salida para los mensajes de registro, use el archivo logging. basicConfig() utilizando el formato de parámetro nombrado. 27.7 Jerarquía de registradores 319
El formato parámetro acepta a cadena eso poder contener LogRecord atributos organizados como mejor le parezca. Hay una lista completa de LogRecord atributos a los que se puede hacer referencia en https://docs.python.org/3/library/logging. html#logrecord-atributos. Los clave son: • args una tupla que enumera los argumentos utilizados para llamar a la función asociada o método. • asctime indica la hora en que se creó el mensaje de registro. • filename el nombre del archivo que contiene la declaración de registro. • module el nombre del módulo (la parte del nombre del nombre del archivo). • funcName el nombre de la función o método que contiene la instrucción de registro. • levelname el nivel de registro de la sentencia de registro. • enviar el mensaje de registro en sí como se proporciona al método de registro. El efecto de algunos de estos se ilustra a continuación. registro de importación logging.basicConfig(formato=’%(asctime)s %(mensaje)s’, nivel=registro.DEBUG) registrador = registro.getLogger(nombre) def hacer_algo(): logger.debug(‘Esto es para ayudar con la depuración’) logger.info(‘Esto es solo para información’) logger.warning(’¡Esto es una advertencia!’) logger.error(‘Esto debería usarse con algo inesperado’) logger.critical(‘Algo serio’) hacer algo() El programa anterior genera las siguientes declaraciones de registro: 2019-02-20 16:50:34,084 Esto es para ayudar con la depuración 2019-02-20 16:50:34,084 Esto es solo para información 2019-02-20 16:50:34,085 ¡Esto es una advertencia! 2019-02-20 16:50:34,085 Esto debería usarse con algo inesperado 2019-02-20 16:50:34,085 Algo serio Sin embargo, podría ser útil conocer el nivel de registro asociado con el registro declaraciones, así como la función desde la que se llamaron las declaraciones de registro. Es posible obtener esta información cambiando la cadena de formato pasada al función logging.basicConfig(): 320 27 Iniciar sesión en Python
Que ahora generará la salida dentro de la información de nivel de registro y el función involucrada: 2019-02-20 16:54:16,250[DEBUG] do_something: Esto es para ayudar con depuración 2019-02-20 16:54:16,250[INFO] do_something: Esto es solo para información 2019-02-20 16:54:16,250[ADVERTENCIA] hacer_algo: Este es un ¡advertencia! 2019-02-20 16:54:16,250[ERROR] hacer_algo: Esto debería ser usado con algo inesperado 2019-02-20 16:54:16,250[CRÍTICO] hacer_algo: Algo grave Incluso podemos controlar el formato de la información de fecha y hora asociada con el declaración de registro usando el parámetro datafmt de logging.basicConfig() función: logging.basicConfig(formato=’%(asctime)s %(mensaje)s’, datefmt=’%m/%d/%Y %I:%M:%S %p’, nivel=registro.DEBUG) Esta cadena de formato utiliza las opciones de formato utilizadas por datetime.strp- función time() (ver https://docs.python.org/3/library/datetime.html#strftime- strptime-behavior) para obtener información sobre los caracteres de control, en este caso • %m: mes como un número decimal con ceros, p. 01, 11, 12. • %d: día del mes como un número decimal con ceros, p. 01, 12 etc • %Y: año con siglo como número decimal, p. 2020. • %I: hora (reloj de 12 horas) como un número decimal con ceros, p. 01, 10 etc • %M: minuto como un número decimal con ceros, p. 0, 01, 59, etc • %S: segundo como un número decimal con ceros, p. 00, 01, 59, etc • %p: AM o PM. Por lo tanto, la salida generada usando la cadena datefmt anterior es: 20/02/2019 05:05:18 p. m. Esto es para ayudar con la depuración 20/02/2019 05:05:18 p. m. Esto es solo para información 20/02/2019 05:05:18 p. m. ¡Esto es una advertencia! 20/02/2019 05:05:18 p. m. Esto debería usarse con algo inesperado 20/02/2019 05:05:18 PM Algo serio Para establecer un formateador en un controlador individual, consulte la siguiente sección. logging.basicConfig(format=’%(asctime)s[%(levelname)s] %(funcName)s: %(mensaje)s’, nivel=registro.DEBUG) 27,8 Formateadores 321
27,9 Recursos en línea Para obtener más información sobre el marco de registro de Python, consulte lo siguiente: • https://docs.python.org/3/library/logging.html La documentación estándar de la biblioteca ción en las instalaciones de registro en Python. • https://docs.python.org/3/howto/logging.html Una guía sobre cómo iniciar sesión desde la documentación de la biblioteca estándar de Python. • https://pymotw.com/3/logging/index.html Módulo Python de registro de la semana página. 27.10 Ejercicios Este ejercicio implicará agregar registro a la clase de cuenta que ha estado trabajando en este libro. Debe agregar métodos de registro a cada uno de los métodos de la clase usando el métodos de depuración o información. También debe obtener un registrador de módulos para la cuenta. clases 322 27 Iniciar sesión en Python
capitulo 28 Registro avanzado 28.1 Introducción En este capítulo profundizamos en la configuración y modificación de Python módulo de registro. En particular, veremos los controladores (utilizados para determinar la mensajes de registro de fo de destino), filtros que pueden ser utilizados por los controladores para proporcionar control detallado de la salida del registro y los archivos de configuración del registrador. Concluimos el capítulo teniendo en cuenta los problemas de rendimiento asociados con el registro. 28.2 manipuladores Dentro de la tubería de registro, son los controladores los que envían el mensaje de registro a su final. destino. De forma predeterminada, el controlador está configurado para dirigir la salida a la consola/asociación de terminal. con el programa en ejecución. Sin embargo, esto se puede cambiar para enviar el registro mensajes a un archivo, a un servicio de correo electrónico, a un servidor web, etc. O, de hecho, a cualquier combinación de estos, ya que puede haber varios controladores configurados para un registrador. Este se muestra en el siguiente diagrama: © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_28 323
En el diagrama anterior, el registrador ha sido configurado para enviar todos los mensajes de registro a cuatro diferentes controladores que permiten escribir un mensaje de registro en la consola, en una web servidor a un archivo y a un servicio de correo electrónico. Tal comportamiento puede ser necesario porque: • El servidor web permitirá a los desarrolladores acceder a una interfaz web que les permite ver los archivos de registro incluso si no tienen permiso para acceder a un servidor de producción. • El archivo de registro garantiza que todos los datos de registro se almacenen permanentemente en un archivo dentro del almacén de archivos. • Se puede enviar un mensaje de correo electrónico a un sistema de notificación para que alguien sea notificado que hay un problema a ser investigado. • Es posible que la consola aún esté disponible para los administradores del sistema que deseen mire los mensajes de registro generados. El marco de registro de Python viene con varios controladores diferentes como sugerencia. mencionados anteriormente y enumerados a continuación: • logging.Stream Handler envía mensajes a salidas como stdout, estándar, etc. • logging.FileHandler envía mensajes de registro a los archivos. Hay varios variedades de File Handler además del FileHandler básico, estos incluyen el logging.handlers.RotatingFileHandler (que rotará el registro archivos basado en a máximo archivo tamaño) y controladores.de.registro. TimeRotatingFileHandler (que rota el archivo de registro en el tiempo especificado) intervalos, p. a diario). • logging.handlers.SocketHandler que envía mensajes a un TCP/IP socket donde puede ser recibido por un servidor TCP. • logging.handlers.SMTPHandler que envía mensajes por el SMTP (Protocolo simple de transferencia de correo) a un servidor de correo electrónico. • logging.handlers.SysLogHandler que envía mensajes de registro a un sistema Unix programa de registro del sistema. 324 28 Registro avanzado
• logging.handlers.NTEventLogHandler que envía un mensaje a un Registro de eventos de Windows. • logging.handlers.HTTPHandler que envía mensajes a un HTTP servidor. • logging.NullHandler que no hace nada con los mensajes de error. esto es a menudo utilizado por desarrolladores de bibliotecas que desean incluir el inicio de sesión en sus aplicaciones pero espere que los desarrolladores configuren un controlador apropiado cuando usen la biblioteca. Todos estos controladores se pueden configurar mediante programación oa través de un archivo de configuración. 28.2.1 Configuración del controlador de salida raíz El siguiente ejemplo utiliza la función logging.basicConfig() para configurar el registrador raíz para usar un FileHandler que escribirá los mensajes de registro en un archivo llamado ‘ejemplo.log’: Tenga en cuenta que si no se especifica un controlador para un registrador con nombre, delega la salida al registrador principal (en este caso, la raíz). El archivo generado para el programa anterior es mostrado a continuación: registro de importación
Establece un controlador de archivos en el registrador raíz para
guardar mensajes de registro en el archivo ejemplo.log
logging.basicConfig(filename=‘example.log’ ,level=logging.DEBUG)
Si no se establece explícitamente ningún controlador en el registrador de nombres
delegará los mensajes al registrador principal para que los maneje
registrador = registro.getLogger(nombre) logger.debug(‘Esto es para ayudar con la depuración’) logger.info(‘Esto es solo para información’) logger.warning(’¡Esto es una advertencia!’ ) logger.error(‘Esto debería usarse con algo inesperado’ ) logger.critical(‘Algo serio’ ) 28.2 manipuladores 325
Como se puede ver en esto, el formateador predeterminado ahora está configurado para un Controlador de archivos. Este FileHandler agrega el nivel de mensaje de registro antes del registro mensaje mismo. 28.2.2 Configuración programática del controlador También es posible crear un controlador mediante programación y configurarlo para el registrador. Este se hace instanciando una de las clases de manejador existentes (o subclasificando un controlador existente, como la clase de controlador raíz o FileHander, etc.). El El controlador instanciado se puede agregar como un controlador para el registrador (recuerde el El registrador puede tener múltiples controladores, por eso el método se llama addHandler. () en lugar de algo como setHandler). A continuación se muestra un ejemplo de configuración explícita de FileHandler para un registrador: El resultado de ejecutar este código es que se crea un archivo de registro con los mensajes registrados: registro de importación
La configuración básica vacía desactiva el controlador de consola predeterminado
registro.basicConfig() registrador = registro.getLogger(nombre) registrador.setLevel(registro.DEBUG)
crear un controlador de archivos que se registre en el archivo especificado
file_handler = logging.FileHandler(‘detailed.log’)
Agregue el controlador al registrador
registrador.addHandler(file_handler)
código de ‘aplicación’
def hacer_algo(): logger.debug(‘mensaje de depuración’) logger.info(‘mensaje de información’) registrador.advertencia(‘mensaje de advertencia’) logger.error(‘mensaje de error’) logger.critical(‘mensaje crítico’) logger.info(‘Iniciando’) hacer algo() logger.info(‘Terminado’) 326 28 Registro avanzado
Dado que esto es mucho más código que usar la función basicConfig(); el La pregunta aquí podría ser ‘¿Por qué molestarse?’. La respuesta es doble: • Puede tener diferentes controladores para diferentes registradores en lugar de establecer el controlador para ser utilizado centralmente. • Cada controlador puede tener su propio conjunto de formatos para que el inicio de sesión en un archivo tenga un formato diferente. formato para iniciar sesión en la consola. Podemos establecer el formato para el controlador instanciando el registro. Clase de formateador con una cadena de formato adecuada. El objeto formateador puede entonces ser aplicado a un manejador usando el método setFormatter() en el manejador objeto. Por ejemplo, podemos modificar el código anterior para incluir un formateador que luego es establecer en el controlador de archivos como se muestra a continuación. El archivo de registro ahora generado se modifica de tal manera que cada mensaje incluye una marca de tiempo, el nombre de la función (o módulo si está en el nivel del módulo), así como el mensaje de registro en sí.
crear un controlador de archivos que se registre en el archivo especificado
manejador_archivo = logging.FileHandler(‘detailed.log’ )
Crear formateador para file_handler
formateador = logging.Formatter(’%(asctime)s - %(funcName)s - %(mensaje)s’ ) file_handler.setFormatter(formateador) registrador.addHandler(file_handler) 28.2 manipuladores 327
28.2.3 Controladores múltiples Como se sugirió en la sección anterior, podemos crear múltiples controladores para enviar registros mensajes a diferentes lugares; por ejemplo desde la consola, a archivos e incluso servidores de correo electrónico. El siguiente programa ilustra la configuración de un controlador de archivos y un controlador de consola para un registrador de nivel de módulo. Para hacer esto, creamos dos manejadores, el manejador_de_archivos y el controlador. controlador_único. Como efecto secundario, también podemos darles diferentes niveles de registro y diferentes formateadores. En este caso, file_handler hereda el nivel de registro del logger en sí (que es DEBUG) mientras que console_handler tiene su nivel de registro establecido explícitamente en ADVERTENCIA. Esto significa que se registrarán diferentes cantidades de información al archivo de registro que la salida de la consola. También hemos establecido diferentes formateadores en cada controlador; en este caso el archivo de registro El formateador del controlador proporciona más información que el formateador de controladores de la consola. Luego, ambos controladores se agregan al registrador antes de que se use.
Múltiples manejadores y formateadores
registro de importación
Configure el registrador raíz predeterminado para que no haga nada
logging.basicConfig(handlers=[logging.NullHandler()])
Obtenga el registrador de nivel de módulo y establezca el nivel en DEBUG
registrador = registro.getLogger(nombre) registrador.setLevel(registro.DEBUG)
Crear controlador de archivos
file_handler = logging.FileHandler(‘detailed.log’)
Crear controlador de consola con un nivel de registro más alto
controlador_consola = registro.StreamHandler() console_handler.setLevel(registro.ADVERTENCIA)
Crear formateador para el controlador de archivos
fh_formatter = registro.Formatter( %(nombre)s.%(funcName)s: %(mensaje)s’, fechafmt=
-%Y %I:%M:%S %pag’) manejador_archivo.setFormatter(fh_formatter) 328 28 Registro avanzado
La salida de este programa ahora se divide entre el archivo de registro y la consola fuera, como se muestra a continuación: 28.3 filtros Los controladores pueden utilizar filtros para proporcionar un control más detallado de la salida del registro. Se puede agregar un filtro a un registrador usando el método logger.addFilter(). Se puede crear un filtro extendiendo la clase logging.Filter y
Crear formateador para el controlador de consola
console_formatter = registro.Formatter(’%(asctime)s - %(nombre de función)s - %(mensaje)s’) controlador_consola.setFormatter(formateador_consola)
Agregue los controladores al registrador
registrador.addHandler(console_handler) registrador.addHandler(file_handler)
código de ‘aplicación’
def hacer_algo(): logger.debug(‘mensaje de depuración’) logger.info(‘mensaje de información’) registrador.advertencia(‘mensaje de advertencia’) logger.error(‘mensaje de error’) logger.critical(‘mensaje crítico’) logger.info(‘Iniciando’) hacer algo() logger.info(‘Terminado’) 28.2 manipuladores 329
implementando el método filter(). Este método toma un registro de registro. este registro El registro se puede validar para determinar si el registro debe generarse o no. Si se debe generarse, luego se devuelve True, si el registro debe ignorarse False debe ser devuelto. En el siguiente ejemplo, se define un filtro llamado MyFilter que filtrará todos los mensajes de registro que contengan la cadena ‘John’. Se agrega como un filtro al registrador y luego se generan dos mensajes de registro. El resultado muestra que solo el mensaje de registro que no contiene la cadena Se emite ‘Juan’: 28.4 Configuración del registrador Todos los ejemplos hasta ahora en este capítulo han utilizado la configuración programática del marco de registro. Esto es ciertamente factible como muestran los ejemplos, pero no requerir un cambio de código si desea modificar el nivel de registro para cualquier registrador en particular, o para cambiar dónde un controlador en particular enruta los mensajes de registro. Para la mayoría de los sistemas de producción, una mejor solución es usar una configuración externa que se carga cuando se ejecuta la aplicación y se utiliza para controlar dinámicamente calcular el marco de registro. Esto permite a los administradores del sistema y a otros cambiar el nivel de registro, el destino del registro, el formato del registro, etc. sin necesidad de cambiar el código. registro de importación clase MiFiltro(registro.Filtro): def filter(self, registro): si ‘Juan’ en record.msg: falso retorno demás: volver verdadero logging.basicConfig(formato=’%(asctime)s %(mensaje)s’, nivel=registro.DEBUG) registrador = registro.getLogger() logger.addFilter(MiFiltro()) logger.debug(‘Esto es para ayudar con la depuración’) logger.info(‘Esta es información sobre John’) 2019-02-20 17:23:22,650 Esto es para ayudar con la depuración 330 28 Registro avanzado
El archivo de configuración de registro se puede escribir usando varios formatos estándar de JSON (la notación de objetos de Java Script), a YAML (otro lenguaje de marcado más) o como un conjunto de pares clave-valor en un archivo.conf. Para más información sobre el diferentes opciones disponibles, consulte la documentación del módulo de registro de Python. En este libro exploraremos brevemente el formato de archivo YAML utilizado para configurar registradores El código YAML anterior se almacena en un archivo llamado logging.conf.yaml; sin embargo, puede llamar a este archivo de cualquier forma que sea significativa. El archivo YAML siempre comienza con un número de versión. Este es un valor entero que representa la versión del esquema YAML (actualmente, solo puede ser el valor 1). Todo otras claves en el archivo son opcionales, incluyen: • formateadores: enumera uno o más formateadores; cada formateador tiene un nombre que actúa como una clave y luego un valor de formato que es una cadena que define el formato de un mensaje de registro. • filtros: esta es una lista de nombres de filtros y un conjunto de definiciones de filtros. • controladores: esta es una lista de controladores con nombre. Cada definición de controlador se compone de un conjunto de pares de valores clave donde las claves definen la clase utilizada para el filtro (obligatorio), el nivel de registro del filtro (opcional), el formateador a usar con el controlador (opcional) y una lista de filtros para aplicar (opcional). • registradores: proporciona uno o más registradores con nombre. Cada registrador puede indicar el registro nivel (opcional) y una lista de controladores (opcional). La opción de propagación puede ser se utiliza para detener la propagación de mensajes a un registrador principal (configurándolo en False). • root: esta es la configuración para el registrador raíz. versión 1 formateadores: mi formateador: formato: ‘%(asctime)s [%(levelname)s] %(name)s.%(funcName)s: %(mensaje)s’ manipuladores: consola: clase: registro.StreamHandler nivel: DEPURAR formateador: mi formateador corriente: ext://sys.stdout madereros: miLogger: nivel: DEPURAR controladores: [consola] propagar: no raíz: nivel: ERROR controladores: [consola] 28.4 Configuración del registrador 331
Este archivo se puede cargar en una aplicación Python utilizando el módulo PyYAML. Esto proporciona un analizador YAML que puede cargar un archivo YAML como una estructura de diccionario que se puede pasar a la función logging.config.dictConfig(). Como esto es un debe abrirse y cerrarse para garantizar que el recurso se maneje de manera adecuada. atentamente; por lo tanto, se gestiona mejor utilizando la declaración with-as como se muestra a continuación: Esto abrirá el archivo YAML en modo de solo lectura y lo cerrará cuando los dos las declaraciones han sido ejecutadas. Este fragmento se utiliza en la siguiente aplicación que carga la configuración del registrador desde el archivo YAML: con open(’logging.config.yaml’ , ‘r’) como f: config = yaml.safe_load(f.read()) registro.config.dictConfig(config) registro de importación importar registro.config importar yaml con open(’logging.config.yaml’, ‘r’) como f: config = yaml.safe_load(f.read()) registro.config.dictConfig(config) registrador = registro.getLogger(‘miRegistrador’)
código de ‘aplicación’
def hacer_algo(): logger.debug(‘mensaje de depuración’) logger.info(‘mensaje de información’) registrador.advertencia(‘mensaje de advertencia’) logger.error(‘mensaje de error’) logger.critical(‘mensaje crítico’) logger.info(‘Iniciando’) hacer algo() logger.info(‘Terminado’) 332 28 Registro avanzado
El resultado de esto utilizando el archivo YAML anterior es: 28.5 Consideraciones de rendimiento El rendimiento al iniciar sesión siempre debe ser una consideración. En general, deberías tiene como objetivo evitar realizar cualquier trabajo innecesario cuando el registro está deshabilitado (o deshabilitado). habilitado para el nivel que se está utilizando). Esto puede parecer obvio, pero puede ocurrir en varios formas inesperadas. Un ejemplo es la concatenación de cadenas. Si un mensaje que se va a registrar implica una cadena concatenación; entonces esa concatenación de cadenas siempre se realizará cuando un registro se está invocando el método. Por ejemplo: Esto siempre dará como resultado que la cadena se genere para el conteo y el total. antes de realizar la llamada a la función de depuración; incluso si el nivel de depuración no está activado en. Sin embargo, el uso de una cadena de formato evitará esto. El formateo involucrado será solo se realizará si la cadena se utilizará en un mensaje de registro. Por lo tanto, debe utilice siempre el formato de cadena para completar los mensajes de registro. Para erxmaple: Otra optimización potencial es usar logger.isEnabledFor (nivel) como protección contra la ejecución de la declaración de registro. esto puede ser util en situaciones en las que se debe realizar una operación asociada para apoyar la operación de registro y esta operación es costosa. Por ejemplo: 2019-02-21 16:20:46,466 [INFO] myLogger.<módulo>: Iniciando 2019-02-21 16:20:46,466 [DEPURACIÓN] myLogger.do_something: depuración mensaje 2019-02-21 16:20:46,466 [INFO] myLogger.do_something: información mensaje 2019-02-21 16:20:46,466 [ADVERTENCIA] myLogger.do_something: advertir mensaje 2019-02-21 16:20:46,466 [ERROR] myLogger.do_something: error mensaje 2019-02-21 16:20:46,466 [CRÍTICO] myLogger.do_something: mensaje crítico 2019-02-21 16:20:46,466 [INFO] myLogger.<módulo>: Listo logger.debug(‘Cuenta: ’ + cuenta + ‘, total: ’ + total) logger.debug(’ Cuenta: %d, total: %d ‘, cuenta, 42) si logger.isEnabledFor(registro.DEBUG): logger.debug(‘Mensaje con %s, %s’, caro_func1(), caro_func2()) 28.4 Configuración del registrador 333
Ahora las dos funciones costosas solo se ejecutarán si el nivel de registro DEBUG Está establecido. 28.6 Ejercicios Usando el registro que agregó a la clase Cuenta en el último capítulo, debe cargar la información de configuración de registro de un archivo YAML similar al utilizado en este capítulo. Esto debe cargarse en el programa de aplicación utilizado para impulsar la cuenta. clases 334 28 Registro avanzado
Parte VII Concurrencia y Paralelismo
capitulo 29 Introducción a la concurrencia y paralelismo 29.1 Introducción En este capítulo introduciremos los conceptos de concurrencia y paralelismo. Nosotros también consideraremos brevemente el tema relacionado de la distribución. Después de esto consideraremos sincronización de procesos, por qué los enfoques orientados a objetos son adecuados para moneda y paralelismo antes de terminar con una breve discusión de hilos versus procesos. 29.2 concurrencia El diccionario define la concurrencia como dos o más eventos o circunstancias que suceden o existen al mismo tiempo. En Ciencias de la Computación, la concurrencia se refiere a la capacidad de diferentes partes o unidades de un programa, algoritmo o problema para ser ejecutado al mismo tiempo, potencialmente en múltiples procesadores o múltiples núcleos. Aquí, un procesador se refiere a la unidad central de procesamiento (o CPU) o una computadora mientras que el núcleo se refiere a la idea de que un chip de CPU puede tener múltiples núcleos o procesadores en eso. Originalmente, un chip de CPU tenía un solo núcleo. Ese es el chip de la CPU tenía un solo unidad de procesamiento en él. Sin embargo, con el tiempo, para aumentar el rendimiento de la computadora, los fabricantes de hardware agregaron núcleos o unidades de procesamiento adicionales a los chips. Así un chip de CPU de doble núcleo tiene dos unidades de procesamiento, mientras que un chip de CPU de cuatro núcleos tiene cuatro unidades de procesamiento Esto significa que en cuanto al sistema operativo de la computadora es en cuestión, tiene varias CPU en las que puede ejecutar programas. Ejecutar el procesamiento al mismo tiempo, en múltiples CPU, puede sustancialmente mejorar el rendimiento general de una aplicación. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_29 337
Por ejemplo, supongamos que tenemos un programa que llamará a tres funciones pendientes, estas funciones son: • hacer una copia de seguridad de los datos actuales que tiene el programa, • imprimir los datos actualmente en poder del programa, • ejecutar una animación utilizando los datos actuales. Supongamos que estas funciones se ejecutan secuencialmente, con los siguientes tiempos: • la función de copia de seguridad tarda 13 s, • la función de impresión tarda 15 s, • la función de animación tarda 10 s. Esto daría como resultado un total de 38 s para realizar las tres operaciones. esto se ilustra gráficamente a continuación: Sin embargo, las tres funciones son completamente independientes entre sí. Eso es que no dependen unos de otros para ningún resultado o comportamiento; ellos no necesitan uno de las otras funciones para completar antes de que puedan completar, etc. Por lo tanto, podemos ejecutar cada función simultáneamente. Si el sistema operativo subyacente y el lenguaje del programa que se utiliza son compatibles múltiples procesos, entonces potencialmente podemos ejecutar cada función en un proceso separado al mismo tiempo y obtener una aceleración significativa en el tiempo total de ejecución. Si la aplicación inicia las tres funciones al mismo tiempo, entonces el máximo El tiempo antes de que el proceso principal pueda continuar será de 15 segundos, ya que ese es el tiempo que tarda la función más larga para ejecutar. Sin embargo, el programa principal puede ser capaz de controlar Continúe tan pronto como se inicien las tres funciones, ya que tampoco depende del 338 29 Introducción a la concurrencia y el paralelismo
resulta de cualquiera de las funciones; por lo tanto, el retraso puede ser insignificante (aunque normalmente habrá un pequeño retraso a medida que se configura cada proceso). Esto es mostrado gráficamente a continuación: 29.3 Paralelismo A menudo se hace una distinción en Ciencias de la Computación entre concurrencia y paralelismo. En concurrencia, las tareas independientes separadas se realizan potencialmente al mismo tiempo. tiempo. En el paralelismo, una tarea grande y compleja se divide en un conjunto de subtareas. El las subtareas representan parte del problema general. Cada subtarea se puede ejecutar en el Mismo tiempo. Por lo general, es necesario combinar los resultados de las subtareas para generar un resultado global. Estas subtareas también son muy similares si no funcionalmente exactamente igual (aunque en general cada invocación de subtarea habrá sido suministrada con datos diferentes). Por lo tanto, el paralelismo es cuando se ejecutan varias copias de la misma funcionalidad al mismo tiempo. mismo tiempo, pero con datos diferentes. Algunos ejemplos de dónde se puede aplicar el paralelismo incluyen: • Un motor de búsqueda web. Tal sistema puede buscar en muchas, muchas páginas web. Cada vez que lo haga, debe enviar una solicitud al sitio web apropiado, recibir el resultado y procesar los datos obtenidos. Estos pasos son los mismos si es el el sitio web de la BBC, el sitio web de Microsoft o el sitio web de la Universidad de Cambridge. Por lo tanto, las solicitudes se pueden ejecutar secuencialmente o en paralelo. • Procesamiento de imágenes. Una imagen grande se puede dividir en porciones para que cada slice se puede analizar en paralelo. 29.2 concurrencia 339
El siguiente diagrama ilustra la idea básica detrás del paralelismo; un programa principal dispara tres subtareas, cada una de las cuales se ejecuta en paralelo. El programa principal entonces espera para completar todas las subtareas antes de combinar los resultados de la subtareas antes de que pueda continuar. 29.4 Distribución Al implementar una solución concurrente o paralela, donde los procesos resultantes run suele ser un detalle de implementación. Conceptualmente, estos procesos podrían ejecutarse en el mismo procesador, máquina física o en una máquina remota o distribuida. Como dicha distribución, en la que se resuelven problemas o se ejecutan procesos compartiendo el trabajo a través de múltiples máquinas físicas, a menudo está relacionado con la concurrencia y paralelismo. Sin embargo, no hay ningún requisito para distribuir el trabajo entre máquinas físicas, de hecho, hacerlo suele implicar un trabajo extra. Para distribuir el trabajo a una máquina remota, los datos y, en muchos casos, el código, deben estar transferidos y puestos a disposición de la máquina remota. Esto puede resultar en importantes retrasos en la ejecución del código de forma remota y puede compensar cualquier rendimiento potencial ventajas de usar una computadora separada físicamente. Como resultado, muchos concurrentes / las tecnologías paralelas por defecto ejecutan código en un proceso separado en el mismo máquina. 29.5 computación en red Grid Computing se basa en el uso de una red de computadoras débilmente acopladas, en que cada computadora puede tener un trabajo enviado, que se ejecutará hasta su finalización antes de devolver un resultado. 340 29 Introducción a la concurrencia y el paralelismo
En muchos casos, la red se compone de un conjunto heterogéneo de computadoras (más bien que todas las computadoras son iguales) y pueden estar geográficamente dispersas. Estos Las computadoras pueden estar compuestas tanto por computadoras físicas como por máquinas virtuales. Una máquina virtual es una pieza de software que emula una computadora completa y se ejecuta en algún hardware subyacente que se comparte con otras máquinas virtuales. Cada Virtual Machine cree que es la única computadora en el hardware; sin embargo lo virtual Todas las máquinas comparten los recursos de la computadora física. Múltiples virtuales por lo tanto, las máquinas pueden ejecutarse simultáneamente en la misma computadora física. Cada virtual máquina proporciona su propio hardware virtual, incluyendo CPU, memoria, discos duros, interfaces de red y otros dispositivos. A continuación, el hardware virtual se asigna a la hardware real en la máquina física que ahorra costos al reducir la necesidad de sistemas físicos de hardware junto con los costos de mantenimiento asociados, así como reduciendo las demandas de energía y enfriamiento de múltiples computadoras. Dentro de una grilla, el software se usa para administrar los nodos de la grilla y enviar trabajos a esos nodos. Dicho software recibirá los trabajos a realizar (programas a ejecutar y información sobre el entorno, como bibliotecas para usar) de los clientes de la cuadrícula. Estos trabajos generalmente se agregan a una cola de trabajos antes de que un programador de trabajos los envíe. a un nodo dentro de la grilla. Cuando los resultados son generados por el trabajo son recogidos del nodo y devueltos al cliente. Esto se ilustra a continuación: 29.5 computación en red 341
El uso de grillas puede hacer que la distribución de procesos concurrentes/paralelos entre un conjunto de máquinas físicas y virtuales mucho más fácil. 29.6 Concurrencia y Sincronización La concurrencia se relaciona con la ejecución de múltiples tareas al mismo tiempo. En muchos casos estas tareas no están relacionadas entre sí, como imprimir un documento y actualizar la interfaz de usuario. En estos casos, las tareas separadas son completamente independientes y puede ejecutar al mismo tiempo sin ninguna interacción. En otras situaciones, es necesario que interactúen múltiples tareas simultáneas; por ejemplo, donde una o más tareas producen datos y una o más tareas consumen esos datos. Este se refiere a menudo como una relación productor-consumidor. En otras situaciones, todos los procesos paralelos deben haber alcanzado el mismo punto antes de que se produzca algún otro comportamiento. ejecutado. Otra situación que se puede dar es que queramos asegurarnos de que sólo uno la tarea concurrente ejecuta una pieza de código sensible a la vez; este código debe por lo tanto estar protegido contra el acceso concurrente. Las bibliotecas concurrentes y paralelas deben proporcionar instalaciones que permitan tal que se produzca la sincronización. 29.7 Orientación a Objetos y Concurrencia Los conceptos detrás de la programación orientada a objetos se prestan particularmente bien a los conceptos asociados a la concurrencia. Por ejemplo, un sistema puede ser descrito como un conjunto de objetos discretos que se comunican entre sí cuando es necesario ensayo En Python, solo un objeto puede ejecutarse en cualquier momento dentro de un único intérprete. Sin embargo, al menos conceptualmente, no hay razón para que esto se debe hacer cumplir la restricción. Los conceptos básicos detrás de la orientación a objetos todavía mantener, incluso si cada objeto se ejecuta dentro de un proceso independiente separado. 342 29 Introducción a la concurrencia y el paralelismo
Tradicionalmente, el envío de un mensaje se trata como una llamada de procedimiento, en la que el la ejecución del objeto que llama se bloquea hasta que se devuelve una respuesta. Sin embargo, podemos extender este modelo simplemente para ver cada objeto como un ejecutable concurrente programa, con actividad que comienza cuando se crea el objeto y continúa incluso cuando se envía un mensaje a otro objeto (a menos que se requiera la respuesta para más Procesando). En este modelo, puede haber muchos objetos (simultáneos) ejecutándose al mismo tiempo. Por supuesto, esto introduce problemas asociados con la asignación de recursos. cación, etc. pero no más que en cualquier sistema concurrente. Una implicación del modelo de objetos concurrentes es que los objetos son más grandes que en el enfoque tradicional de un solo hilo de ejecución, debido a la sobrecarga de tener cada objeto como un hilo de ejecución separado. Gastos generales como la necesidad de un programador para manejar estos hilos de ejecución y mecanismos de asignación de recursos significa que no es factible tener enteros, caracteres, etc. como procesos separados. 29.8 Hilos V Procesos Como parte de esta discusión, es útil comprender qué se entiende por proceso. Un proceso es una instancia de un programa de computadora que está siendo ejecutado por el Sistema operativo. Cualquier proceso tiene tres elementos clave; el programa que se está ejecutando, los datos utilizados por ese programa (como las variables utilizadas por el programa) y el estado del proceso (también conocido como el contexto de ejecución del programa). Un subproceso (Python) es un proceso ligero preventivo. Se considera que un subproceso es preventivo porque cada subproceso tiene la oportunidad de ejecutar como el hilo principal en algún momento. Cuando un hilo llega a ejecutarse, lo hará ejecutar hasta • finalización, • hasta que esté esperando algún tipo de E/S (Entrada/Salida), • duerme por un período de tiempo, • se ha ejecutado durante 15 ms (el umbral actual en Python 3). Si el subproceso no se ha completado cuando ocurre una de las situaciones anteriores, entonces se deja de ser el hilo de ejecución y se ejecutará otro hilo en su lugar. Este significa que un subproceso se puede interrumpir en medio de la realización de una serie de pasos relacionados. Un subproceso se considera un proceso ligero porque no posee su propio espacio de direcciones y no es tratado como una entidad separada por el host operativo sistema. En cambio, existe dentro de un proceso de una sola máquina que usa la misma dirección espacio. Es útil tener una idea clara de la diferencia entre un hilo (ejecutándose dentro de un proceso de una sola máquina) y un sistema multiproceso que utiliza procesos separados en el hardware subyacente. 29.7 Orientación a Objetos y Concurrencia 343
29,9 Algo de terminología El mundo de la programación concurrente está lleno de terminología que quizás no conozcas. familiar con. Algunos de esos términos y conceptos se describen a continuación: • Invocaciones asíncronas versus síncronas. La mayor parte del método, función o las invocaciones de procedimientos que habrá visto en la programación representan syn- invocaciones cronicas. Una llamada de función o método sincrónico es aquella que bloquea la ejecución del código de llamada hasta que regresa. Tales llamadas son típicamente dentro de un solo hilo de ejecución. Las llamadas asincrónicas son aquellas en las que el flujo de control regresa inmediatamente a la persona que llama y la persona que llama puede ejecutar en su propio hilo de ejecución. Permitir que tanto la persona que llama como la llamada continúen Procesando. • Código de no bloqueo versus código de bloqueo. El código de bloqueo es un término que se utiliza para describir el código que se ejecuta en un subproceso de ejecución, a la espera de alguna actividad para completo, lo que hace que uno de los subprocesos de ejecución más separados también sea demorado. Por ejemplo, si un hilo es el productor de algunos datos y otros los subprocesos son los consumidores de esos datos, entonces los pasos del consumidor no pueden continuar hasta que el productor genera los datos para que ellos los consuman. A diferencia de, no bloquear significa que ningún subproceso puede retrasar indefinidamente a otros. • Código concurrente versus paralelo. El código concurrente y el código paralelo son similar, pero diferente en un aspecto significativo. La concurrencia indica que dos o más actividades están progresando aunque es posible que no se estén ejecutando. cortando en el mismo punto en el tiempo. Esto generalmente se logra mediante la continua intercambiar procesos competitivos entre ejecución y no ejecución. Este El proceso se repite hasta que al menos uno de los hilos de ejecución (Threads) ha completaron su tarea. Esto puede ocurrir porque dos subprocesos comparten el mismo procesador físico con cada uno se le da un período de tiempo corto en el que progreso antes de que el otro tenga un corto período de tiempo para progresar. los dos hilos se dice que están compartiendo el tiempo de procesamiento usando una técnica conocida como tiempo rebanar El paralelismo, por otro lado, implica que hay múltiples procesadores disponible permitiendo cada hilo a ejecutar en su propio procesador simultáneamente. 29.10 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://en.wikipedia.org/wiki/Concurrency_(computer_science) página de Wikipedia sobre concurrencia. • https://en.wikipedia.org/wiki/Virtual_machine Wikipedia página en Virtual Máquinas. 344 29 Introducción a la concurrencia y el paralelismo
• https://en.wikipedia.org/wiki/Parallel_computing Wikipedia página en par- alelismo. • http://tutorials.jenkov.com/java-concurrency/concurrency-vs-parallelism.html Tutorial de concurrencia versus paralelismo. • https://www.redbooks.ibm.com/redbooks/pdfs/sg246778.pdf Libro rojo de IBM sobre una introducción a Grid Computing. 29.10 Recursos en línea 345
capitulo 30 enhebrar 30.1 Introducción Threading es una de las formas en que Python le permite escribir programas que Tarea múltiple; que parece hacer más de una cosa a la vez. Este capítulo presenta el módulo de subprocesos y utiliza un breve ejemplo para ilustrar cómo se pueden utilizar estas características. 30.2 Hilos En Python, la clase Thread del módulo de subprocesamiento representa una actividad que se ejecuta en un hilo de ejecución separado dentro de un solo proceso. Estos hilos de ejecución son hilos de ejecución ligeros y preventivos. Un hilo es ligero porque no posee su propio espacio de direcciones y no se trata como un entidad por el sistema operativo host; no es un proceso. En cambio, existe dentro de un proceso de una sola máquina que utiliza el mismo espacio de direcciones que otros subprocesos. 30.3 Estados de subprocesos Cuando se crea por primera vez un objeto hilo, existe, pero aún no se puede ejecutar; debe ser comenzó. Una vez que se ha iniciado, se puede ejecutar; es decir, es elegible para ser programado para su ejecución. Puede alternar entre correr y estar ejecutable bajo el control del planificador. El programador es responsable de administrar múltiples subprocesos que desean obtener algo de tiempo de ejecución. Un objeto de subproceso permanece ejecutable o en ejecución hasta que finaliza su método run(); momento en el que ha terminado su ejecución y ahora está muerto. Todos los estados entre © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_30 347
no iniciado y muerto se considera que indica que el subproceso está vivo (y por lo tanto puede funcionar en algún momento). Esto se muestra a continuación: Un subproceso también puede estar en estado de espera; por ejemplo, cuando está esperando otro subproceso para terminar su trabajo antes de continuar (posiblemente porque necesita los resultados producido por ese hilo para continuar). Esto se puede lograr usando el join() método y también se ilustra arriba. Una vez que el segundo hilo completa la espera el hilo volverá a ser ejecutable. El subproceso que se está ejecutando actualmente se denomina subproceso activo. Hay algunos puntos a tener en cuenta sobre los estados de subprocesos: • Se considera que un subproceso está vivo a menos que su método run() finalice después de que se puede considerar muerto. • Un subproceso activo puede estar en ejecución, ejecutable, en espera, etc. • El estado ejecutable indica que el procesador puede ejecutar el subproceso, pero no se está ejecutando actualmente. Esto se debe a que una prioridad igual o superior el proceso ya se está ejecutando, y el subproceso debe esperar hasta que el procesador se vuelve libre. Por lo tanto, el diagrama muestra que el programador puede mover un hilo entre el estado de ejecución y ejecutable. De hecho, esto podría suceder muchas veces como el subproceso se ejecuta durante un tiempo, luego se elimina del procesador por el programador y agregado a la cola de espera, antes de ser devuelto al procesador de nuevo en una fecha posterior. 30.4 Crear un hilo Hay dos formas de iniciar un nuevo hilo de ejecución: • Pasar una referencia a un objeto invocable (como una función o método) al Constructor de clases de subprocesos. Esta referencia actúa como el destino del hilo para ejecutar. 348 30 enhebrar
• Cree una subclase de la clase Thread y redefina el método run() para realizar el conjunto de acciones que el subproceso debe realizar. Veremos ambos enfoques. Como un hilo es un objeto, puede tratarse como cualquier otro objeto: puede enviarse mensajes, puede tener variables de instancia y puede proporcionar métodos. Por lo tanto, la todos los aspectos de subprocesos múltiples de Python se ajustan al modelo orientado a objetos. Este simplifica en gran medida la creación de sistemas de subprocesos múltiples, así como el mantenimiento capacidad y claridad del software resultante. Una vez que se crea una nueva instancia de un subproceso, debe iniciarse. Antes de que se inicie, no puede funcionar, aunque existe. 30.5 Creación de instancias de la clase de subprocesos La clase Thread se puede encontrar en el módulo de subprocesos y, por lo tanto, debe ser importado antes de su uso. La clase Thread define un único constructor que ocupa a seis argumentos opcionales: subprocesamiento de clase. Subproceso (grupo = Ninguno, objetivo=Ninguno, nombre=Ninguno, argumentos=(), kwargs={}, demonio=Ninguno) El constructor Thread siempre debe llamarse usando argumentos de palabras clave; el significado de estos argumentos es: • el grupo debe ser Ninguno; reservado para futuro extensión cuando se implementa una clase ThreadGroup. • target es el objeto invocable que será invocado por el método run(). valores predeterminados a Ninguno, lo que significa que no se llama nada. • nombre es el nombre del hilo. Por defecto, un nombre único se construye de la forma “Hilo-N” donde N es un número entero. • args es la tupla de argumentos para la invocación de destino. El valor predeterminado es (). si uno solo se proporciona el argumento, la tupla no es necesaria. Si varios argumentos son pro- dado que cada argumento es un elemento dentro de la tupla. • kwargs es un diccionario de argumentos de palabras clave para la invocación de destino. El valor predeterminado es {}. • daemon indica si este subproceso se ejecuta como un subproceso daemon o no. Si no Ninguno, daemon establece explícitamente si el subproceso es demoníaco. Si ninguno (el predeterminado), la propiedad demoníaca se hereda del subproceso actual. 30.4 Crear un hilo 349
Una vez que se crea un subproceso, debe iniciarse para que sea elegible para la ejecución utilizando el método Thread.start(). Lo siguiente ilustra un programa muy simple que crea un hilo que ejecute la función simple_worker(): de subprocesos de importación Subproceso def trabajador_simple(): imprimir(‘hola’)
Crea un hilo nuevo y ábrelo
El hilo ejecutará la función simple_worker
t1 = Subproceso (objetivo = trabajador_simple) t1.inicio() En este ejemplo, el hilo t1 ejecutará la función simple_worker. El código principal será ejecutado por el hilo principal que está presente cuando el programa empieza; por lo tanto, hay dos subprocesos utilizados en el programa anterior; principal y t1. 30.6 La clase de hilo La clase Thread define todas las facilidades requeridas para crear un objeto que pueda ejecutar dentro de su propio proceso ligero. Los métodos clave son: • start() Inicia la actividad del hilo. Debe llamarse como máximo una vez por subproceso objeto. Hace arreglos para que el método run() del objeto sea invocado en un hilo de mando. Este método generará un RuntimeError si se llama más de una vez en el mismo objeto hilo. • Método run() que representa la actividad del hilo. Puede anular esto método en una subclase. El método run() estándar invoca el objeto invocable pasado al constructor del objeto como el argumento de destino, si lo hay, con posición y palabra clave argumentos tomado de los argumentos de args y kwargs, respectivamente. No debe llamar a este método directamente. • join(timeout = None) Espere hasta que finalice el hilo que envió este mensaje. Esto bloquea el subproceso de llamada hasta el subproceso cuyo método join() se llama termina Cuando el argumento de tiempo de espera está presente y no es Ninguno, debe ser un número de punto flotante que especifica un tiempo de espera para la operación en segundos (o fracciones de los mismos). Un hilo se puede unir () muchas veces. • nombre Una cadena utilizada únicamente con fines de identificación. No tiene semántica. Múltiples subprocesos pueden recibir el mismo nombre. El nombre inicial lo establece el constructor. Dar un nombre a un subproceso puede ser útil para fines de depuración. • ident El ‘identificador de subproceso’ de este subproceso o Ninguno si el subproceso no ha sido comenzó. Este es un entero distinto de cero. 350 30 enhebrar
• está_vivo() Devolver si el hilo es vivo. Este método devuelve True justo antes de que comience el método run() hasta justo después de run() el método termina. La función del módulo threading.enumerate() re- convierte una lista de todos los subprocesos vivos. • daemon Un valor booleano que indica si este subproceso es un subproceso daemon (Verdadero) o no (Falso). Esto debe configurarse antes de llamar a start(), de lo contrario se genera un RuntimeError. Su valor predeterminado se hereda de la creación hilo. Todo el programa de Python se cierra cuando no hay subprocesos vivos que no sean daemon. izquierda. A continuación se muestra un ejemplo que ilustra el uso de algunos de estos métodos: de subprocesos de importación Subproceso def trabajador_simple(): imprimir(‘hola’) t1 = Subproceso (objetivo = trabajador_simple) t1.inicio() imprimir(t1.obtenerNombre()) imprimir (t1.ident) imprimir(t1.is_alive()) Esto produce: Hola Hilo-1 123145441955840 Verdadero El método join() puede hacer que un subproceso espere a que se complete otro. Para ejemplo, si queremos que el subproceso principal espere hasta que se complete un subproceso antes de imprimir el mensaje hecho; entonces podemos hacer que se una a ese hilo: de subprocesos de importación Subproceso desde el tiempo de importación del sueño trabajador definido(): para i en el rango (0, 10): imprimir(’.’, fin=’’, ras=Verdadero) dormir(1) imprimir(‘Iniciando’)
Crear objeto de lectura con referencia a la función de trabajo
t = Subproceso (objetivo = trabajador)
Iniciar el objeto hilo
30.6 La clase de hilo 351
t.inicio()
Espere a que se complete el hilo
t.unirse() imprimir(’\nTerminado’) Ahora el mensaje ‘Terminado’ no debe imprimirse hasta después del subproceso de trabajo ha terminado como se muestra a continuación: A partir de ………. Hecho 30.7 Funciones del módulo de enhebrado Hay un conjunto de funciones de módulo de subprocesamiento que admiten trabajar con hilos; estas funciones incluyen: • threading.active_count() Devuelve el número de objetos Thread actualmente. alquilado vivo. El recuento devuelto es igual a la longitud de la lista devuelta por enumerar(). • threading.current_thread() Devuelve el objeto Thread actual, cor- respondiendo al hilo de control de la persona que llama. Si el hilo de control de la persona que llama fue no creado a través del módulo de subprocesos, un objeto de subproceso ficticio con se devuelve la funcionalidad limitada. • threading.get_ident() Devuelve el ‘identificador de subproceso’ del actual hilo. Este es un entero distinto de cero. Los identificadores de hilo se pueden reciclar cuando un el hilo sale y se crea otro hilo. • threading.enumerate()Retorna una lista de todos los objetos Thread actualmente vivo. La lista incluye subprocesos daemon, objetos subprocesos ficticios creados por current_thread() y el hilo principal. Excluye hilos terminados y subprocesos que aún no se han iniciado. • threading.main_thread()Retorna el objeto Thread principal. 30.8 Pasar argumentos a un hilo Muchas funciones esperan recibir un conjunto de valores de parámetros cuando se ejecutan; estos argumentos aún deben pasarse a la función cuando se ejecutan a través de un hilo separado. Estos parámetros se pueden pasar a la función que se ejecutará a través de el parámetro args, por ejemplo: 352 30 enhebrar
de subprocesos de importación Subproceso desde el tiempo de importación del sueño def trabajador(mensaje): para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) imprimir(‘Iniciando’) t1 = Subproceso (objetivo = trabajador, args = ‘A’) t2 = Subproceso (objetivo = trabajador, args = ‘B’) t3 = Subproceso (objetivo = trabajador, args = ‘C’) t1.inicio() t2.inicio() t3.inicio() imprimir(‘Terminado’) En este ejemplo, la función del trabajador toma un mensaje para que se imprima 10 veces dentro de un bucle. Dentro del ciclo, el hilo imprimirá el mensaje y luego se dormirá por un segundo. Esto permite que se ejecuten otros subprocesos, ya que el subproceso debe esperar a que se duerma. tiempo de espera para terminar antes de volver a ser ejecutable. Luego se crean tres hilos t1, t2 y t3, cada uno con un mensaje diferente. Nota que la función del trabajador () se puede reutilizar con cada subproceso como cada invocación de la función se le pasarán sus propios valores de parámetro. Entonces se inician los tres hilos. Esto quiere decir que en este punto está el principal subproceso y tres subprocesos de trabajo que son Ejecutables (aunque solo un subproceso ejecutar a la vez). Cada uno de los tres subprocesos de trabajo ejecuta la impresión de la función de trabajador () ya sea la letra A, B o C diez veces. Esto significa que una vez iniciado cada hilo imprimirá una cadena, dormirá durante 1 s y luego esperará hasta que se seleccione para ejecutarse nuevamente, esto se ilustra en el siguiente diagrama: 30.8 Pasar argumentos a un hilo 353
La salida generada por este programa se ilustra a continuación: A partir de ABCTerminado ABCACBABCABCCBAABCABCABCBAC Observe que el subproceso principal finaliza después de que los subprocesos de trabajo solo se hayan impreso una sola letra cada uno; sin embargo, siempre que haya al menos un subproceso que no sea daemon ejecutar el programa no terminará; ya que ninguno de estos hilos está marcado como un subproceso daemon el programa continúa hasta que el último subproceso ha terminado de imprimirse la décima de sus letras. Observe también cómo cada uno de los subprocesos tiene la oportunidad de ejecutarse en el procesador antes vuelve a dormir; así podemos ver las letras A, B y C todas mezcladas. 30,9 Ampliación de la clase de subprocesos El segundo enfoque para crear un Thread mencionado anteriormente fue subclasificar el Clase de hilo. Para hacer esto debes
- Defina una nueva subclase de Thread.
- Anule el método run().
- Defina un nuevo método init() que llame a la clase principal init() para pasar los parámetros requeridos al constructor de la clase Thread. Esto se ilustra a continuación, donde la clase WorkerThread pasa el nombre, parámetros de destino y daemon hasta el constructor de la superclase Thread. Una vez que haya hecho esto, puede crear una instancia del nuevo WorkerThread class y luego inicie esa instancia. imprimir(‘Iniciando’) t = subproceso de trabajo () t.inicio() imprimir(’\nTerminado’) 354 30 enhebrar
La salida de esto es: A partir de . Hecho ……… Nota eso él es común a llamar cualquier subclases de el Hilo clase, SomethingThread, para dejar claro que es una subclase de la clase Thread y debe tratarse como si fuera un hilo (que por supuesto lo es). 30.10 Subprocesos de demonios Un subproceso se puede marcar como un subproceso daemon estableciendo la propiedad daemon en verdadero ya sea en el constructor o más tarde a través de la propiedad de acceso. Por ejemplo: de subprocesos de importación Subproceso desde el tiempo de importación del sueño def trabajador(mensaje): para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) imprimir(‘Iniciando’)
Crear un hilo demonio
d = Thread(daemon=True, target=worker, args=‘C’) d.inicio() dormir(5) imprimir(‘Terminado’) Esto crea un subproceso de daemon en segundo plano que ejecutará la función worker(). Estos subprocesos se utilizan a menudo para tareas de limpieza (como datos de fondo). copias de seguridad, etc). Como se mencionó anteriormente, un subproceso daemon no es suficiente por sí solo para mantener el finalice el programa actual. Esto significa que el subproceso del daemon mantendrá bucle hasta que termine el hilo principal. Como el hilo principal duerme durante 5 s, eso permite el subproceso daemon para imprimir alrededor de 5 cadenas antes de que finalice el subproceso principal. Esto se ilustra con el siguiente resultado: A partir de CCCCCDone 30,9 Ampliación de la clase de subprocesos 355
30.11 Nomenclatura de subprocesos Los subprocesos se pueden nombrar; que puede ser muy útil a la hora de depurar una aplicación con varios hilos. En el siguiente ejemplo, se han creado tres subprocesos; dos han sido dado explícitamente un nombre relacionado con lo que están haciendo, mientras que el del medio tiene se ha quedado con el nombre predeterminado. Luego comenzamos los tres hilos y usamos el función threading.enumerate() para recorrer todo el actual en vivo hilos imprimiendo sus nombres: La salida de este programa se da golpe: A B C Hilo principal obrero Hilo-1 demonio ABCBACACBCBACBAABCCBACBACBA Como puede ver, además del subproceso de trabajo y el subproceso de daemon, hay un MainThread (que inicia todo el programa) y Thread-1 que es el subproceso al que hace referencia la variable t2 y utiliza el nombre de subproceso predeterminado. 356 30 enhebrar
30.12 Subproceso de datos locales En algunas situaciones, cada subproceso requiere su propia copia de los datos en los que está trabajando. con; esto significa que la memoria compartida (montón) es difícil de usar ya que es inherentemente compartido entre todos los hilos. Para superar esto, Python proporciona un concepto conocido como datos Thread-Local. Los datos locales de subprocesos son datos cuyos valores están asociados con un subproceso en lugar de con la memoria compartida. Esta idea se ilustra a continuación: Para crear datos locales de subprocesos, solo es necesario crear una instancia de subprocesamiento. local (o una subclase de esto) y almacenar atributos en él. Las instancias serán hilo específico; lo que significa que un subproceso no verá los valores almacenados por otro subproceso. Por ejemplo: desde subprocesos import Thread, local, currentThread de randint de importación aleatoria def mostrar_valor(datos): intentar: val = datos.valor excepto AttributeError: print(subprocesoactual().nombre, ’ - Sin valor todavía’) demás: print(SubprocesoActual().nombre, ’ - valor =’, val) def trabajador(datos): mostrar_valor(datos) datos.valor = randint(1, 100) mostrar_valor(datos) print(SubprocesoActual().nombre, ’ - Inicio’) datos_locales = locales() mostrar_valor(datos_locales) 30.12 Subproceso de datos locales 357
para i en el rango (2): t = Hilo(nombre=‘W’ + str(i), target=trabajador, args=[datos_locales]) t.inicio() mostrar_valor(datos_locales) print(SubprocesoActual().nombre, ’ - Listo’) La salida de esto es Subproceso principal: inicio MainThread - Sin valor todavía W0 - Sin valor todavía W0 - valor = 20 W1 - Sin valor todavía W1 - valor = 90 MainThread - Sin valor todavía Subproceso principal - Listo El ejemplo presentado arriba define dos funciones. • La primera función intenta acceder a un valor en el objeto de datos locales del subproceso. Si el el valor no está presente, se genera una excepción (AttributeError). El La función show_value () detecta la excepción o procesa con éxito el datos. • La función de trabajador llama a show_value() dos veces, una antes de establecer un valor en el objeto de datos local y una vez después. Como esta función será ejecutada por separado subprocesos, el nombre del subproceso actual se imprime mediante show_value () función. La función principal crea un objeto de datos locales usando la función local() del biblioteca de subprocesos. Luego llama al mismo show_value(). A continuación, crea dos hilos para ejecutar la función de trabajador al pasarles el objeto local_data; cada entonces se inicia el hilo. Finalmente, vuelve a llamar a show_value(). Como se puede ver en la salida, un subproceso no puede ver los datos establecidos por otro subproceso en el objeto local_data (incluso cuando el nombre del atributo es el mismo). 30.13 Temporizadores La clase Timer representa una acción (o tarea) para ejecutar después de una cierta cantidad de tiempo ha transcurrido. La clase Timer es una subclase de Thread y como tal también funciona como ejemplo de creación de hilos personalizados. 358 30 enhebrar
Los temporizadores se inician, como con los subprocesos, llamando a su método start(). el temporizador se puede detener (antes de que comience su acción) llamando al método cancel(). El el intervalo que esperará el temporizador antes de ejecutar su acción puede no ser exactamente el mismo como el intervalo especificado por el usuario, ya que otro subproceso puede estar ejecutándose cuando el temporizador desea comenzar. La firma del constructor de la clase Timer es: Temporizador (intervalo, función, args = Ninguno, kwargs = Ninguno) A continuación se muestra un ejemplo del uso de la clase Timer: del temporizador de importación de subprocesos definitivamente hola(): imprimir(‘hola’) imprimir(‘Iniciando’) t = Temporizador (5, hola) t.inicio() imprimir(‘Terminado’) En este caso, el temporizador ejecutará la función de saludo después de un retraso inicial de 5 s. 30.14 El bloqueo de intérprete global El bloqueo de intérprete global (o GIL) es un bloqueo global dentro del intérprete de CPython que fue diseñado para evitar posibles interbloqueos entre múltiples tareas triples. Está diseñado para proteger el acceso a los objetos de Python evitando múltiples los subprocesos se ejecuten al mismo tiempo. En su mayor parte, no necesita preocuparse por el GIL, ya que está en un nivel más bajo que los programas que va a escribir. Sin embargo, cabe señalar que la GIL es controvertida porque impide programas de Python multiproceso aprovechen al máximo los sistemas de multiprocesador. elementos en determinadas situaciones. Esto se debe a que para poder ejecutar un hilo se debe obtener el GIL y solo uno hilo a la vez puede contener el GIL (que es el bloqueo que representa). Esto significa que Python actúa como una sola máquina con CPU; solo una cosa puede funcionar a la vez. Un hilo solo entregará el GIL si duerme, tiene que esperar algo (como alguna E/S) 30.13 Temporizadores 359
o ha mantenido el GIL durante un cierto período de tiempo. Si el tiempo máximo que un el subproceso puede contener el GIL se ha cumplido, el programador liberará el GIL de ese subproceso (resultando que detiene la ejecución y ahora tiene que esperar hasta que tenga el GIL volvió a él) y seleccionará otro hilo para obtener el GIL y comenzar a ejecutar. Por lo tanto, es imposible que los subprocesos estándar de Python se aprovechen de la múltiples CPU típicamente disponibles en hardware de computadora moderno. Una solución a esto es usar la biblioteca de multiprocesamiento de Python descrita en el próximo capítulo. 30.15 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://docs.python.org/3/library/threading.html La biblioteca estándar de Python documentación sobre Threading. • https://pymotw.com/3/threading/ La página Módulo Python de la semana en Enhebrar. • https://pythonprogramming.net/threading-tutorial-python/ Tutorial sobre Python Módulo de roscado. 30.16 Ejercicio Cree una función llamada impresora() que tome un mensaje y un valor máximo para utilizar durante un período para dormir. Dentro de la función, cree un bucle que itere 10 veces. dentro del bucle • generar un número aleatorio desde 0 hasta el período máximo especificado y luego dormir durante ese periodo de tiempo. Puede usar la función random.randint() para esto. • Una vez que el período de suspensión haya terminado, imprima el mensaje pasado al función. • Luego vuelva a hacer un bucle hasta que esto se haya repetido 10 veces. Ahora cree cinco subprocesos para ejecutar cinco invocaciones de la función que produjo anteriormente y comience los cinco subprocesos. Cada subproceso debe tener un tiempo max_sleep diferente. Un programa de ejemplo para ejecutar la función de impresora cinco veces a través de un conjunto de subprocesos es dada a continuación: t1 = Subproceso (objetivo = impresora, args = (‘A’, 10)) t2 = Subproceso (objetivo = impresora, args = (‘B’, 5)) t3 = Subproceso (objetivo = impresora, args = (‘C’, 15)) t4 = Subproceso (objetivo = impresora, args = (‘D’, 7)) t5 = Subproceso (objetivo = impresora, args = (‘E’, 2)) t1.inicio() 360 30 enhebrar
t2.inicio() t3.inicio() t4.inicio() t5.inicio() A continuación se muestra un ejemplo del tipo de salida que esto podría generar: BAEAEABEDAEAEBEDCECBEEEADCDBBDABCADBBDABADCDCDCCCC 30.16 Ejercicio 361
capitulo 31 multiprocesamiento 31.1 Introducción La biblioteca de multiprocesamiento admite la generación de archivos separados (operativos). nivel del sistema) procesos para ejecutar el comportamiento (como funciones o métodos) utilizando una API que es similar a la API Threading presentada en el último capítulo. Se puede utilizar para evitar la limitación introducida por Global Interpreter Lock (el GIL) mediante el uso de procesos de sistema operativo separados en lugar de ligeros subprocesos (que se ejecutan dentro de un solo proceso). Esto significa que la biblioteca de multiprocesamiento permite a los desarrolladores explotar el entorno de múltiples procesadores del hardware informático moderno que normalmente tiene múltiples núcleos de procesador que permiten múltiples operaciones/comportamientos ejecutar en paralelo; esto puede ser muy importante para el análisis de datos, procesamiento de imágenes, Aplicaciones de animación y juegos. La biblioteca de multiprocesamiento también presenta algunas características nuevas, en particular la Objeto de grupo para paralelizar la ejecución de un objeto invocable (por ejemplo, funciones y métodos) que no tiene equivalente dentro de la API Threading. 31.2 La clase de proceso La clase Process es el equivalente de la biblioteca de multiprocesamiento a la Clase de subprocesos en la biblioteca de subprocesos. Se puede utilizar para ejecutar un objeto invocable como una función en un proceso separado. Para ello es necesario crear una nueva instancia de la clase Process y luego llamar al método start() en ella. Métodos como join() también están disponibles para que un proceso pueda esperar a otro proceso completar antes de continuar, etc. La principal diferencia es que cuando se crea un nuevo proceso, se ejecuta dentro de un proceso separado en los sistemas operativos subyacentes (como Windows, Linux o © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_31 363
Mac OS). Por el contrario, un subproceso se ejecuta dentro del mismo proceso que el original programa. Esto significa que el proceso es administrado y ejecutado directamente por el sistema operativo en uno de los procesadores que forman parte de la computadora subyacente hardware. La ventaja de esto es que puede explotar el paralelismo subyacente inherentes al hardware físico de la computadora. La desventaja es que un Proceso toma más trabajo para configurar que los hilos más ligeros. El constructor de la clase Process proporciona el mismo conjunto de argumentos que la clase Thread, a saber: clase multiprocesamiento.Proceso(grupo=Ninguno, objetivo=Ninguno, nombre=Ninguno, argumentos=(), kwargs={}, demonio=Ninguno) • el grupo siempre debe ser Ninguno; existe únicamente por compatibilidad con el API de subprocesamiento. • target es el objeto invocable que será invocado por el método run(). por defecto a Ninguno, lo que significa que no se llama nada. • nombre es el nombre del proceso. • args es la tupla de argumentos para la invocación de destino. • kwargs es un diccionario de argumentos de palabras clave para la invocación de destino. • argumento del demonio conjuntos el bandera del demonio de proceso a Verdadero o Falso. Si es Ninguno (el valor predeterminado), este indicador se heredará del proceso de creación. Al igual que con la clase Thread, el constructor Process siempre debe llamarse utilizando argumentos de palabras clave. La clase Process también proporciona un conjunto de métodos similar a la clase Thread. • start() Inicia la actividad del proceso. Esto debe llamarse como máximo una vez por objeto de proceso. Hace arreglos para que el método run() del objeto sea invocado en un proceso separado. • join([tiempo de espera]) Si el tiempo de espera del argumento opcional es Ninguno (predeterminado), el método se bloquea hasta que finaliza el proceso unido. Si el tiempo de espera es positivo número, él bloques en la mayoría de los segundos de tiempo de espera. Nota eso el método devuelve None si su proceso finaliza o si el método expira. 364 31 multiprocesamiento
• is_alive() Devuelve si el proceso está vivo. Aproximadamente, un objeto de proceso es vivo desde el momento en que el método start () regresa hasta que el proceso secundario termina La clase de proceso también tiene varios atributos: • nombre El nombre del proceso. El nombre es una cadena utilizada con fines de identificación. solo. No tiene semántica. A varios procesos se les puede dar el mismo nombre. Él puede ser útil para fines de depuración. • daemon La bandera del demonio del proceso, un valor booleano. Esto debe establecerse antes de llamar a start(). El valor predeterminado se hereda de la creación proceso. Cuando un proceso finaliza, intenta terminar todos sus procesos secundarios demoníacos. procesos. Tenga en cuenta que un proceso demoníaco no puede crear procesos secundarios. • pid Devuelve el ID del proceso. Antes de que se genere el proceso, será Ninguno. • exitcode El código de salida del proceso. Esto será Ninguno si el proceso aún no ha terminado. terminado. Un valor negativo -N indica que el niño fue despedido por señal n Además de estos métodos y atributos, la clase Process también define métodos adicionales relacionados con el proceso que incluyen: • terminar() Finaliza el proceso. • kill() Igual que terminar() excepto que en Unix la señal SIGKILL es utilizado en lugar de la señal SIGTERM. • close() Cierra el objeto Proceso, liberando todos los recursos asociados con él. ValueError es aumentó si el subyacente proceso es aún correr. Una vez que close() regresa con éxito, la mayoría de los otros métodos y atributos del objeto Process generará un ValueError. 31.3 Trabajar con la clase de proceso El siguiente programa simple crea tres objetos de proceso; cada uno corre el función trabajador(), con los argumentos de cadena A, B y C respectivamente. Estos Luego se inician tres objetos de proceso usando el método start(). 31.2 La clase de proceso 365
Es esencialmente lo mismo que el programa equivalente para hilos pero con el Se utiliza la clase de proceso en lugar de la clase Thread. El resultado de esta aplicación se muestra a continuación: A partir de Hecho ABCABCABCABCABCABCABCACBACBACB La principal diferencia entre las versiones Thread y Process es que el La versión de proceso ejecuta la función de trabajo en procesos separados, mientras que en la Versión de subproceso todos los subprocesos comparten el mismo proceso. 31.4 Formas alternativas de iniciar un proceso Cuando se llama al método start() en un proceso, se pueden utilizar tres enfoques diferentes. iniciar el proceso subyacente están disponibles. Estos enfoques se pueden establecer mediante el multiprocessing.set_start_method() que toma una cadena que indica el enfoque a utilizar. Los mecanismos reales de iniciación del proceso disponibles dependen de el sistema operativo subyacente: • ‘spawn’ El proceso principal inicia un nuevo proceso de interpretación de Python. El niño el proceso solo heredará los recursos necesarios para ejecutar los objetos del proceso método ejecutar(). En particular, los identificadores y descriptores de archivo innecesarios del del proceso de importación de multiprocesamiento desde el tiempo de importación del sueño def trabajador(mensaje): para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) imprimir(‘Iniciando’) t2 = Proceso (objetivo = trabajador, args = ‘A’) t3 = Proceso (objetivo = trabajador, args = ‘B’) t4 = Proceso (objetivo = trabajador, args = ‘C’) t2.inicio() t3.inicio() t4.inicio() imprimir(‘Terminado’) 366 31 multiprocesamiento
el proceso padre no se heredará. Comenzar un proceso usando este método es bastante lento en comparación con el uso de fork o forkserver. Disponible en Unix y Ventanas. Este es el valor predeterminado en Windows. • ‘bifurcación’ El proceso principal usa os.fork() para bifurcar el intérprete de Python. El proceso hijo, cuando comienza, es efectivamente idéntico al proceso padre. Todos los recursos del padre son heredados por el proceso hijo. Disponible solo en Sistemas operativos tipo Unix. Este es el valor predeterminado en Unix, Linux y Mac OS. • ‘forkserver’ En este caso se inicia un proceso de servidor. A partir de entonces, siempre que se necesita un nuevo proceso, el proceso principal se conecta al servidor y solicita que bifurca un nuevo proceso. El proceso del servidor de bifurcación es de un solo subproceso, por lo que es seguro para que use os.fork(). No se heredan recursos innecesarios. Disponible en Plataformas de estilo Unix que admiten el paso de descriptores de archivos a través de conductos Unix. Se debe usar set_start_method() para establecer el método de inicio (y esto solo debe establecerse una vez dentro de un programa). Esto se ilustra a continuación, donde se especifica el método de inicio de generación: del proceso de importación de multiprocesamiento desde la importación de multiprocesamiento set_start_method desde el tiempo de importación del sueño importar sistema operativo def trabajador(mensaje): print(’nombre del módulo:’, nombre) imprimir(‘proceso padre:’, os.getppid()) imprimir(‘id del proceso:’, os.getpid()) para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) def principal(): imprimir(‘Iniciando’) print(‘ID del proceso de la aplicación raíz:’, os.getpid()) set_start_method(‘spawn’) t = Proceso (objetivo = trabajador, args = ‘A’) t.inicio() imprimir(‘Terminado’) si nombre == ‘principal’: principal() El resultado de esto se muestra a continuación: A partir de Identificación del proceso de la aplicación raíz: 6281 Hecho 31.4 Formas alternativas de iniciar un proceso 367
nombre del módulo: principal proceso padre: 6281 identificación del proceso: 6283 AAAAAAAAAA Tenga en cuenta que el proceso principal y los ID del proceso actual se imprimen para el trabajador (), mientras que el método main() imprime solo su propia identificación. Esto muestra que la identificación del proceso de aplicación principal es la misma que la identificación de los padres del proceso de trabajo. Alternativamente, es posible usar el método get_context() para obtener un objeto de contexto. Los objetos de contexto tienen la misma API que el multiprocesamiento. módulo y le permite utilizar múltiples métodos de inicio en el mismo programa, por ejemplo: ctx = multiprocesamiento.get_context(‘spawn’) q = ctx.Cola() p = ctx.Process(objetivo = foo, argumentos = (q,)) 31.5 Usando una piscina La creación de procesos es costosa en términos de recursos informáticos. Por lo tanto, sería ser útil para poder reutilizar procesos dentro de una aplicación. La clase de la piscina proporciona dichos procesos reutilizables. La clase Pool representa un conjunto de procesos de trabajo que se pueden utilizar para realizar un conjunto de operaciones simultáneas y paralelas. El Pool proporciona métodos que permitir que las tareas se descarguen a estos procesos de trabajo. La clase Pool proporciona un constructor que toma varios argumentos: clase multiprocesamiento.pool.Pool(procesos, inicializador, initargs, maxtareasporniño, contexto) Estos representan: • procesos es el número de obrero procesos a usar. Si el proceso esses es Ninguno, entonces se usa el número devuelto por os.cpu_count(). • inicializador Si el inicializador no es Ninguno, cada proceso de trabajo llame al inicializador (* initargs) cuando comience. • maxtasksperchild es el número de tareas que puede completar un proceso de trabajo antes de que salga y se reemplace con un nuevo proceso de trabajo, para permitir que no se use recursos a liberar. El maxtasksperchild predeterminado es Ninguno, que significa que los procesos de trabajo vivirán tanto como el grupo. 368 31 multiprocesamiento
• context se puede utilizar para especificar el contexto utilizado para iniciar el trabajador procesos. Por lo general, un grupo se crea utilizando la función multiprocesamiento. Piscina(). Alternativamente, el grupo se puede crear usando el método Pool() de un objeto de contexto. La clase Pool proporciona una variedad de métodos que se pueden usar para enviar el trabajo al procesos de trabajo gestionados por el grupo. Tenga en cuenta que los métodos del objeto Pool solo debe ser llamado por el proceso que creó el grupo. El siguiente diagrama ilustra el efecto de enviar algún trabajo o tarea a la piscina. De la lista de procesos disponibles, se selecciona un proceso y la tarea se pasado al proceso. El proceso entonces ejecutará la tarea. Al finalizar cualquier se devuelven los resultados y el proceso vuelve a la lista disponible. Si cuando una tarea es enviado al grupo no hay procesos disponibles, entonces la tarea se agregará a una cola de espera hasta que haya un proceso disponible para manejar la tarea. El más simple de los métodos proporcionados por el Pool para la presentación de trabajos es el mapa método: pool.map(func, iterable, chunksize=Ninguno) Este método devuelve una lista de los resultados obtenidos al ejecutar la función en paralelo contra cada uno de los elementos en el parámetro iterable. • El parámetro func es el objeto invocable que se ejecutará (como una función o un método). • El iterable se usa para pasar cualquier parámetro a la función. • Este método corta el iterable en varios fragmentos que envía al grupo de procesos como tareas separadas. El tamaño (aproximado) de estos trozos puede ser especificado estableciendo chunksize en un entero positivo. El método bloquea hasta el resultado está listo. 31.5 Usando una piscina 369
El siguiente programa de muestra ilustra el uso básico de Pool y map() método. desde el grupo de importación de multiprocesamiento
def trabajador(x): print(‘En trabajador con: ‘, x) volver x * x
def principal(): con Pool (procesos = 4) como grupo: print(pool.map(trabajador, [0, 1, 2, 3, 4, 5]))
si nombre == ‘principal’: principal() Tenga en cuenta que el objeto Pool debe cerrarse una vez que haya terminado con él; somos por lo tanto, usar la declaración ‘con como’ descrita anteriormente en este libro para manejar el Pool de recursos limpiamente (se asegurará de que el Pool se cierre cuando el bloque de código dentro de la declaración with as se completa). La salida de este programa es En trabajador con: 0 En trabajador con: 1 En trabajador con: 2 En trabajador con: 3 En trabajador con: 4 En trabajador con: 5 [0, 1, 4, 9, 16, 25] Como se puede ver en esta salida, la función map() se usa para ejecutar seis instancias de la función trabajador() con los valores proporcionados por la lista de integrantes gers. Cada instancia es ejecutada por un proceso de trabajo administrado por el Pool. Sin embargo, tenga en cuenta que el Pool solo tiene 4 procesos de trabajo, esto significa que el las dos últimas instancias de la función del trabajador deben esperar hasta que dos de la función del trabajador Los procesos han terminado el trabajo que estaban haciendo y se pueden reutilizar. Esto puede actuar como un manera de limitar, o controlar, cuánto trabajo se realiza en paralelo. Una variante del método map() es el método imap_unordered(). Este El método también aplica una función dada a un iterable pero no intenta mantener el orden de los resultados. Se puede acceder a los resultados a través del iterable devuelto por el función. Esto puede mejorar el rendimiento del programa resultante. El siguiente programa modificó la función worker() para devolver su resultado en lugar de imprimirlo. Luego se puede acceder a estos resultados al iterarlos a medida que aparecen. se producen a través de un bucle for: 370 31 multiprocesamiento
Como el nuevo método obtiene resultados tan pronto como están disponibles, el orden en que los resultados que se devuelven pueden ser diferentes, como se muestra a continuación: En trabajador con: 0 En trabajador con: 1 En trabajador con: 3 En trabajador con: 2 En trabajador con: 4 En trabajador con: 5 0 1 9 dieciséis 4 25 Otro método disponible en la clase Pool es Pool.apply_async() método. Este método permite que las operaciones/funciones se ejecuten de forma asíncrona permitiendo que las llamadas al método regresen inmediatamente. Eso es tan pronto como la llamada al método se hace, el control se devuelve al código de llamada que puede continuar inmediatamente. Cualquier resultado que se recopile de las operaciones asincrónicas se puede obtener ya sea proporcionando una función de devolución de llamada o utilizando el método get() de bloqueo para obtener un resultado. A continuación se muestran dos ejemplos, el primero utiliza el método de bloqueo get(). Este El método esperará hasta que haya un resultado disponible antes de continuar. El segundo enfoque utiliza una función de devolución de llamada. La función de devolución de llamada se llama cuando hay un resultado disponible; el resultado se pasa a la función. 31.5 Usando una piscina 371
La salida de esto es: En trabajador con: 6 Resultado de asíncrono: 36 En trabajador con: 4 En recopilar_resultados: 16 31.6 Intercambio de datos entre procesos En algunas situaciones es necesario que dos procesos intercambien datos. sin embargo, el dos objetos de proceso no comparten memoria ya que se ejecutan en operaciones separadas procesos a nivel de sistema. Para evitar esto, la biblioteca de multiprocesamiento proporciona la Función de tubería (). La función Pipe() devuelve un par de objetos connection.Connection conectados por una tubería que por defecto es dúplex (bidireccional). Los dos objetos de conexión devueltos por Pipe() representan los dos extremos de la tubo. Cada objeto de conexión tiene métodos send() y recv() (entre otros). Esto permite que un proceso envíe datos a través del método send() de un extremo del objeto de conexión. A su vez, un segundo proceso puede recibir esos datos a través de la recepción () método del otro objeto de conexión. Esto se ilustra a continuación: desde el grupo de importación de multiprocesamiento def recoger_resultados(resultado): print(‘En recopilar_resultados: ‘, resultado) def trabajador(x): print(‘En trabajador con: ‘, x) volver x * x def principal(): con Pool (procesos = 2) como grupo:
obtener un ejemplo basado
res = pool.apply_async(trabajador, [6]) print(‘Resultado de asíncrono: ‘, res.get(timeout=1)) con Pool (procesos = 2) como grupo:
ejemplo basado en devolución de llamada
pool.apply_async(trabajador, argumentos=[4], devolución de llamada = recopilar_resultados) si nombre == ‘principal’: principal() 372 31 multiprocesamiento
Una vez que un programa ha terminado con una conexión, debe cerrarse usando close (). El siguiente programa ilustra cómo se utilizan las conexiones de tuberías: El resultado de este ejemplo de Pipe es: Principal - Comenzando, creando la Tubería Principal - Configuración del proceso Principal - Iniciando el proceso Principal - Espere una respuesta del proceso hijo Trabajador: ahora comenzó a dormir durante 1 segundo Trabajador: envío de datos a través de Pipe Trabajador: cierre del extremo de la conexión del trabajador Hola 31.6 Intercambio de datos entre procesos 373
Principal: cierre del proceso padre al final de la conexión Principal - Listo Tenga en cuenta que los datos en una canalización pueden corromperse si dos procesos intentan leer o escribir en el mismo extremo de la tubería al mismo tiempo. Sin embargo, no hay riesgo de corrupción de procesos que usan diferentes extremos de la tubería al mismo tiempo. 31.7 Compartir estado entre procesos En general, si se puede evitar, entonces no debe compartir el estado entre procesos. Sin embargo, si es inevitable, la biblioteca de multiprocesamiento proporciona dos formas en las que el estado (datos) se puede compartir, estos son memoria compartida (como compatible con multiprocessing.Value y multiprocessing.Array) y proceso del servidor. 31.7.1 Procesar memoria compartida Los datos se pueden almacenar en un mapa de memoria compartida mediante un multiprocesamiento.Valor o multiprocesamiento.Array. Estos datos pueden ser accedidos por múltiples procesos. El constructor para el tipo multiprocessing.Value es: multiprocesamiento.Valor (typecode_or_type, *args, lock = True) Dónde: • typecode_or_type determina el tipo del objeto devuelto: es un tipo ctypes o un código de tipo de un carácter. Por ejemplo, ’d’ indica un doble precisión flotante e ‘i’ indica un entero con signo. • *args se pasa al constructor del tipo. • lock Si lock es True (el valor predeterminado), se crea un nuevo objeto de bloqueo recursivo para sincronizar el acceso al valor. Si el bloqueo es Falso, acceda al devuelto objeto no estará automáticamente protegido por un candado, por lo que no necesariamente será proceso seguro. El constructor para multiprocessing.Array es multiprocesamiento.Array multiprocesamiento.Array(typecode_or_type, tamaño_o_inicializador, bloqueo = Verdadero) 374 31 multiprocesamiento
Dónde: • typecode_or_type determina el tipo de los elementos devueltos formación. • size_or_initializer Si size_or_initializer es un número entero, entonces determina nes la longitud de la matriz, y la matriz será inicialmente puesto a cero De lo contrario, size_or_initializer es una secuencia que se usa para inicializar la matriz y cuya longitud determina la longitud de la matriz. • Si el bloqueo es Verdadero (el valor predeterminado), se crea un nuevo objeto de bloqueo para sincronizar acceso al valor. Si el bloqueo es Falso, no se podrá acceder al objeto devuelto. estar protegido automáticamente por un candado, por lo que no será necesariamente “seguro para el proceso”. A continuación, se muestra un ejemplo que utiliza tanto el tipo de valor como el de matriz: de proceso de importación de multiprocesamiento, valor, matriz def trabajador(n, a): n.valor = 3.1415927 para i en el rango (len (a)): a[i] = -a[i] def principal(): imprimir(‘Iniciando’) número = Valor(’d’, 0.0) matriz = matriz(‘i’, rango(10)) p = Proceso (objetivo = trabajador, args = (num, arr)) p.inicio() p.unirse() imprimir(num.valor) imprimir(*arr) imprimir(‘Terminado’) si nombre == ‘principal’: principal() 31.8 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre el multiprocesamiento: • https://docs.python.org/3/library/multiprocessing.html El Pitón estándar Biblioteca de documentación sobre Multiprocesamiento. • https://pymotw.com/3/multiprocessing La página Módulo Python de la semana en Multiprocesamiento. • https://pythonprogramming.net/multiprocessing-python-intermediate-python- tutorial Tutorial sobre el módulo Multiprocesamiento de Python. 31.7 Compartir estado entre procesos 375
31,9 Ejercicios Escriba un programa que pueda encontrar el factorial de cualquier número dado. Por ejemplo, encuentre el factorial del número 5 (¡a menudo escrito como 5!) que es 1 * 2 * 3 * 4 * 5 y es igual a 120 El factorial no está definido para números negativos y el factorial de Cero es 1; eso es 0! = 1. A continuación, modifique el programa para ejecutar varios cálculos factoriales en paralelo. Reúna todos los resultados en una lista e imprímala. Puede usar el enfoque que desee para ejecutar múltiples procesos, aunque un Pool podría ser un buen enfoque para usar. Su programa debe calcular los factoriales de 5, 8, 10, 15, 3, 6 y 4 en paralelo. 376 31 multiprocesamiento
capitulo 32 Sincronización entre subprocesos/procesos 32.1 Introducción En este capítulo veremos varias instalaciones soportadas tanto por el método de subprocesamiento y bibliotecas de multiprocesamiento que permiten la sincronización y la cooperación entre hilos o procesos. En el resto de este capítulo veremos algunas de las formas en que Python admite la sincronización entre múltiples subprocesos y procesos. Nota que la mayoría de las bibliotecas se reflejan entre subprocesos y multiprocesamiento, por lo que que las mismas ideas básicas se mantienen para ambos enfoques con, en lo principal, muy similar API. Sin embargo, no debe mezclar y combinar subprocesos y procesos. Si usted es usando Threads, entonces solo debe usar las instalaciones de la biblioteca de subprocesos. A su vez, si está utilizando Procesos, solo debe usar las instalaciones en el biblioteca de multiprocesamiento. Los ejemplos dados en este capítulo usarán uno o otra de las tecnologías, pero son relevantes para ambos enfoques. 32.2 usando una barrera El uso de una barrera de subprocesamiento (o barrera de multiprocesamiento) es una de las formas más simples en las que la ejecución de un conjunto de Subprocesos (o Procesos) puede ser sincronizado Los hilos o procesos involucrados en la barrera se conocen como las partes que están participando en la barrera. Cada una de las partes de la barrera puede trabajar de forma independiente hasta llegar al punto de barrera en el código. La barrera representa un punto final que todas las partes deben alcanzar antes de continuar. se puede desencadenar la conducta. En el momento en que todas las partes alcanzan la barrera es Es posible activar opcionalmente una acción posterior a la fase (también conocida como llamada de barrera). atrás). Esta acción posterior a la fase representa un comportamiento que debe ejecutarse cuando © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_32 377
todas las partes llegan a la barrera pero antes de permitir que esas partes continúen. El La acción posterior a la fase (la devolución de llamada) se ejecuta en un solo hilo (o proceso). una vez que es completado, entonces todas las partes se desbloquean y pueden continuar. Esto se ilustra en el siguiente diagrama. Los subprocesos t1, t2 y t3 están todos involucrados en la barrera. Cuando el hilo t1 alcanza la barrera, debe esperar hasta que sea liberado por la barrera. De manera similar, cuando t2 alcanza la barrera, debe esperar. Cuando t3 finalmente llega a la barrera se invoca la devolución de llamada. Una vez que la devolución de llamada ha completado el La barrera libera los tres subprocesos que luego pueden continuar. A continuación se muestra un ejemplo del uso de un objeto Barrera. Nótese que la función siendo invocado en cada hilo también debe cooperar en el uso de la barrera como el código se ejecutará hasta el método barrier.wait() y luego esperará hasta que todos los demás subprocesos también han llegado a este punto antes de que se les permita continuar. La barrera es una clase que se puede utilizar para crear un objeto de barrera. Cuando el Se instancia la clase de barrera, se puede proporcionar con tres parámetros: dónde • partidos el número de partidos individuales que participarán en la Barrera. • acción es un objeto invocable (como una función) que, cuando se proporciona, será llamado después de que todas las partes hayan entrado en la barrera y justo antes de soltar el centro comercial. • tiempo de espera Si se proporciona un “tiempo de espera”, se utiliza como valor predeterminado para todos los wait() llama a la barrera. Así, en el siguiente código b = Barrera (3, acción = devolución de llamada) Indica que habrá tres partes involucradas en la Barrera y que el La función de devolución de llamada se invocará cuando los tres alcancen la barrera (sin embargo, el timeout se deja como el valor predeterminado Ninguno). El objeto Barrera se crea fuera de los Subprocesos (o Procesos), pero debe estar disponible para la función que ejecuta el subproceso (o proceso). La forma más fácil de manejar esto es pasar la barrera a la función como una de las 378 32 Sincronización entre subprocesos/procesos
parámetros; esto significa que la función se puede utilizar con diferentes objetos de barrera dependiendo del contexto. A continuación se muestra un ejemplo que usa la clase Barrier con un conjunto de Threads: de la importación de subprocesos Barrera, Subproceso desde el tiempo de importación del sueño de randint de importación aleatoria def print_it(mensaje, barrera): imprimir (‘imprimir_it para:’, mensaje) para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) dormir (randint (1, 6)) print(‘Esperar a la barrera con:’, msg) barrera.espera() print(‘Volviendo de print_it:’, mensaje) devolución de llamada def(): imprimir (‘Devolución de llamada en ejecución’) print(‘Principal - Inicio’) b = Barrera (3, devolución de llamada) t1 = Subproceso (objetivo = imprimir_it, args = (‘A’, b)) t2 = Subproceso (objetivo = imprimir_it, args = (‘B’, b)) t3 = Subproceso (objetivo = imprimir_it, args = (‘C’, b)) t1.inicio() t2.inicio() t3.inicio() imprimir(‘Principal - Listo’) La salida de esto es: Principal - Inicio print_it para: A imprimir_it para: B imprimir_it para: C A B C Principal - Listo ABCACBACBABACBCABACBACBBAC Esperar barrera con: B Esperar barrera con: A Esperar barrera con: C Ejecución de devolución de llamada Volviendo de print_it: A Volviendo de print_it: B Volviendo de print_it: C A partir de esto, puede ver que la función print_it() se ejecuta tres veces seguidas. actualmente; las tres invocaciones alcanzan la instrucción barrier.wait() pero en un orden diferente a aquel en que se iniciaron. Una vez que los tres han llegado a este punto en que la función de devolución de llamada se ejecuta antes que la función print_it() las invocaciones pueden proceder. 32.2 usando una barrera 379
La clase Barrera en sí proporciona varios métodos que se utilizan para gestionar o averiguar Información sobre la barrera: Método Descripción esperar (tiempo de espera = ninguno) Espere hasta que todos los subprocesos hayan notificado la barrera (a menos que se agote el tiempo de espera). alcanzado): devuelve el número de subprocesos que pasaron la barrera reiniciar() Devolver la barrera al estado predeterminado abortar() Poner la barrera en un estado roto fiestas Devuelve el número de subprocesos necesarios para pasar la barrera. n_esperando Número de subprocesos actualmente en espera Un objeto Barrera se puede reutilizar cualquier número de veces para el mismo número de Hilos. El ejemplo anterior podría cambiarse fácilmente para que se ejecute usando Proceso alterando la declaración de importación y la creación de un conjunto de Procesos en lugar de Subprocesos: desde multiprocesamiento importación Barrera, Proceso … print(‘Principal - Inicio’) b = Barrera (3, devolución de llamada) t1 = Proceso (objetivo = imprimir_it, args = (‘A’, b)) Tenga en cuenta que solo debe usar subprocesos con un threading.Barrier. Sucesivamente solo debe usar Procesos con una barrera de multiprocesamiento. 32.3 Señalización de eventos Aunque el punto de usar varios subprocesos o procesos es ejecutar por separado operaciones al mismo tiempo, hay ocasiones en las que es importante poder permitir que dos o más subprocesos o procesos para cooperar en el momento de su comportamiento. El El objeto de barrera presentado anteriormente es una forma de nivel relativamente alto para hacer esto; sin embargo, en algunos casos se requiere un control de grano más fino. El threading.Event o multiprocesamiento. Las clases de eventos se pueden usar para este propósito. Un Evento administra una bandera interna que las personas que llaman pueden establecer () o borrar (). Otros subprocesos pueden esperar () a que se establezca la bandera (), bloqueando efectivamente sus propios hilos. progreso hasta que el Evento le permita continuar. La bandera interna se establece inicialmente en Falso, lo que garantiza que si una tarea llega al evento antes de que se configure, debe esperar. De hecho, puede invocar la espera con un tiempo de espera opcional. Si no incluye el tiempo de espera opcional, entonces esperar () esperará para siempre, mientras que esperar (tiempo de espera) esperará hasta el tiempo de espera dado en segundos. Si se alcanza el tiempo de espera, entonces la espera el método devuelve Falso; de lo contrario, espera devuelve True. Como ejemplo, el siguiente diagrama ilustra dos procesos que comparten un evento objeto. El primer proceso ejecuta una función que espera a que se establezca el evento. A su vez el segundo proceso ejecuta una función que establecerá el evento y así liberará la espera proceso. 380 32 Sincronización entre subprocesos/procesos
El siguiente programa implementa el escenario anterior: desde proceso de importación de multiprocesamiento, evento desde el tiempo de importación del sueño def esperar_por_evento(evento): print(‘wait_for_event - Ingresado y esperando’) event_is_set = evento.esperar() print(‘wait_for_event - El evento está configurado: ‘, event_is_set) def set_event(evento): print(‘set_event - Ingresado pero a punto de dormir’) dormir(5) print(‘set_event - Despertar y configurar evento’) evento.set() print(‘set_event - Conjunto de eventos’) imprimir(‘Iniciando’)
Crear el objeto de evento
evento = Evento()
Iniciar un proceso para esperar la notificación del evento
p1 = Proceso (objetivo = esperar_por_evento, args = [evento]) p1.inicio()
Configure un proceso para configurar el evento
p2 = Proceso (objetivo = conjunto_evento, argumentos = [evento]) p2.inicio()
Espere a que se complete el primer proceso
p1.unirse() imprimir(‘Terminado’) 32.3 Señalización de eventos 381
La salida de este programa es: A partir de wait_for_event - Ingresado y esperando set_event - Ingresado pero a punto de dormir set_event - Evento de activación y configuración set_event - Conjunto de eventos wait_for_event - El evento está configurado: Verdadero Hecho Para cambiar esto para usar Threads, simplemente necesitaríamos cambiar la importación y crear dos hilos: de subprocesos de importación Subproceso, Evento … imprimir(‘Iniciando’) evento = Evento() t1 = Subproceso (objetivo = esperar_por_evento, args = [evento]) t1.inicio() t2 = Subproceso (objetivo = conjunto_evento, args = [evento]) t2.inicio() t1.unirse() imprimir(‘Terminado’) 32.4 Sincronización de código concurrente No es raro tener que asegurarse de que las regiones críticas del código estén protegidas contra ejecución concurrente por múltiples subprocesos o procesos. Estos bloques de código típicamente implican la modificación o el acceso a datos compartidos. Por lo tanto es necesario para garantizar que solo un subproceso o proceso actualice un objeto compartido a la vez. tiempo y que los subprocesos o procesos del consumidor están bloqueados mientras se ejecuta esta actualización. ocurriendo Esta situación es más común cuando uno o más subprocesos o procesos son los productores de datos y uno o más subprocesos o procesos son los consumidores de esos datos Esto se ilustra en el siguiente diagrama. 382 32 Sincronización entre subprocesos/procesos
En este diagrama, el Productor se está ejecutando en su propio Hilo (aunque podría también se ejecuta en un proceso separado) y coloca datos en algunos datos compartidos comunes envase. Posteriormente, varios Consumidores independientes pueden consumir ese datos cuando están disponibles y cuando son libres de procesar los datos. Sin embargo, hay No tiene sentido que los consumidores revisen repetidamente el contenedor en busca de datos como ese sería una pérdida de recursos (por ejemplo, en términos de ejecución de código en un procesador y de cambio de contexto entre múltiples subprocesos o procesos). Por lo tanto, necesitamos alguna forma de notificación o sincronización entre el Productor y el Consumidor para manejar esta situación. Python proporciona varias clases en el subproceso (y también en el multi- procesamiento) biblioteca que se puede utilizar para administrar bloques de código críticos. Estos las clases incluyen Bloqueo, Condición y Semáforo. 32.5 Bloqueos de pitón La clase Lock definida (tanto en el threading como en el multiprocesamiento) bibliotecas) proporciona un mecanismo para sincronizar el acceso a un bloque de código. El El objeto de bloqueo puede estar en uno de dos estados bloqueado y desbloqueado (con el estado inicial siendo desbloqueado). El bloqueo otorga acceso a un solo hilo a la vez; otros hilos debe esperar a que el Bloqueo se libere antes de avanzar. La clase Lock proporciona dos métodos básicos para adquirir el bloqueo (acquire()) y liberando (liberar()) la cerradura. • Cuando el estado del objeto Bloquear está desbloqueado, adquirir() cambia el estado a bloqueado y regresa inmediatamente. • Cuando el estado está bloqueado, la adquisición() se bloquea hasta que una llamada a la liberación() en otro hilo lo cambia a desbloqueado, luego la llamada de adquisición () lo restablece a bloqueado y vuelve. • El método release() solo debe llamarse en estado bloqueado; cambia el estado para desbloquear y vuelve inmediatamente. Si se intenta liberar un bloqueo desbloqueado, se generará un RuntimeError. A continuación se muestra un ejemplo del uso de un objeto de bloqueo: 32.4 Sincronización de código concurrente 383
de subprocesamiento importación Subproceso, Bloqueo clase SharedData (objeto): def init(uno mismo): valor propio = 0 self.bloquear = Bloquear() def read_value(self): intentar: print(‘read_value adquiriendo bloqueo’) self.lock.acquire() devolver valor propio finalmente: print(‘read_value liberando Lock’) self.lock.release() def cambio_valor(auto): print(‘cambiar_valor adquiriendo bloqueo’) con autobloqueo: valor propio = valor propio + 1 print(‘bloqueo de cambio_valor liberado’) La clase SharedData presentada anteriormente utiliza bloqueos para controlar el acceso a elementos críticos. bloques de código, específicamente a read_value() y change_value() métodos. El objeto Lock se mantiene internamente en el objeto ShareData y ambos Los métodos intentan adquirir el bloqueo antes de realizar su comportamiento, pero luego deben suelte el bloqueo después de su uso. El método read_value() hace esto explícitamente usando try: finalmente: bloques mientras que el método change_value() usa una declaración with (como el tipo de bloqueo admite el protocolo de administrador de contexto). Ambos enfoques logran el mismo resultado. pero el estilo de declaración with es más conciso. La clase SharedData se usa a continuación con dos funciones simples. En este caso el objeto SharedData se ha definido como una variable global, pero también podría se han pasado a las funciones reader() y updater() como argumento. Las funciones del lector y del actualizador se repiten, intentando llamar a read_value() y los métodos change_value() en el objeto shared_data. Como ambos métodos usan un bloqueo para controlar el acceso a los métodos, solo un hilo puede obtener acceso al área bloqueada a la vez. Esto significa que la función lector() puede comenzar a leer datos antes de que la función updater() haya cambiado los datos (o viceversa). Esto se indica mediante la salida donde el hilo del lector accede al valor ‘0’ dos veces antes de que el actualizador registre el valor ‘1’. Sin embargo, la función updater() se ejecuta una segunda vez antes de que el lector obtenga acceso al bloque de código bloqueado que es por qué se pierde el valor 2. Dependiendo de la aplicación, esto puede o no ser un problema. 384 32 Sincronización entre subprocesos/procesos
datos_compartidos = DatosCompartidos() def lector(): mientras que es cierto: imprimir (datos_compartidos.leer_valor()) def actualizador(): mientras que es cierto: datos_compartidos.cambiar_valor() imprimir(‘Iniciando’) t1 = Hilo (objetivo = lector) t2 = Subproceso (objetivo = actualizador) t1.inicio() t2.inicio() imprimir(‘Terminado’) La salida de esto es: A partir de read_value adquiriendo bloqueo read_value liberando Bloqueo 0 read_value adquiriendo bloqueo read_value liberando Bloqueo 0 Hecho change_value adquisición de bloqueo Bloqueo de cambio de valor liberado 1 change_value adquisición de bloqueo Bloqueo de cambio de valor liberado change_value adquisición de bloqueo Bloqueo de cambio de valor liberado 3 change_value adquisición de bloqueo Bloqueo de cambio de valor liberado 4 Los objetos de bloqueo solo se pueden adquirir una vez; si un subproceso intenta adquirir un bloqueo en el mismo objeto Lock más de una vez, se lanza un RuntimeError. Si es necesario volver a adquirir un bloqueo en un objeto de bloqueo, entonces el enhebrado. Debe usarse la clase RLock. Esta es una cerradura de reentrada y permite la misma Subproceso (o proceso) para adquirir un bloqueo varias veces. Sin embargo, el código debe soltar la cerradura tantas veces como la haya adquirido. 32.5 Bloqueos de pitón 385
32.6 Condiciones de Python Las condiciones se pueden usar para sincronizar la interacción entre dos o más subprocesos o Procesos. Los objetos de condiciones admiten el concepto de un modelo de notificación; ideal para un recurso de datos compartido al que acceden múltiples consumidores y productores. Se puede usar una condición para notificar uno o todos los subprocesos en espera o Procesos que pueden realizar (por ejemplo, para leer datos de un recurso compartido). Los métodos disponibles que soportan esto son: • notificar () notifica un hilo en espera que luego puede continuar • notificar_todos() notifica a todos los subprocesos en espera que pueden continuar • wait() hace que un subproceso espere hasta que se notifique que puede continuar Una condición siempre está asociada con un bloqueo interno que debe ser adquirido y liberado antes de que se puedan llamar los métodos wait() y notificar(). La Condición es compatible con el Protocolo de Administrador de Contexto y, por lo tanto, puede ser se usa a través de una declaración with (que es la forma más típica de usar una Condición) para obtener este candado. Por ejemplo, para obtener la condición de bloqueo y llamar al método de espera podríamos escribir: con condición: condición.esperar() print(‘Ahora podemos continuar’) El objeto de condición se usa en el siguiente ejemplo para ilustrar cómo un productor El subproceso y dos subprocesos de consumo pueden cooperar. Una clase DataResource ha sido definido que contendrá un elemento de datos que se compartirá entre un consumidor y un conjunto de productores. También (internamente) define un atributo de condición. Tenga en cuenta que esto significa que la Condición está completamente internalizada en la clase DataResource; código externo no necesita saber, o preocuparse por, la Condición y su usar. En cambio, el código externo puede simplemente llamar al consumidor () y al productor () funciones en subprocesos separados según sea necesario. El método consumidor () usa una declaración with para obtener el bloqueo (interno) en el objeto Condición antes de esperar a que se le notifique que los datos están disponibles. En a su vez, el método Producer() también usa una declaración with para obtener un bloqueo en el objeto de condición antes de generar el valor del atributo de datos y luego notificar cualquier cosa esperando con la condición de que puedan proceder. Tenga en cuenta que aunque el el método consumidor obtiene un bloqueo en el objeto de condición; si tiene que esperar lo hará liberar el candado y recuperar el candado una vez notificado que puede continuar. Esto es una sutileza que a menudo se pasa por alto. 386 32 Sincronización entre subprocesos/procesos
from threading import Thread, Condition, currentThread desde el tiempo de importación del sueño de randint de importación aleatoria clase de recurso de datos: def init(uno mismo): print(‘DataResource - Inicializando los datos vacíos’) self.data = Ninguno print(‘DataResource - Configuración del objeto Condition’) self.condición = Condición() def consumidor(auto): “““esperar la condición y usar el recurso””” print(‘DataResource - Comenzando el método del consumidor en’, subproceso actual().nombre) con condición propia: self.condition.wait() print(‘DataResource - El recurso está disponible para’, subproceso actual().nombre) print(‘DataResource - Lectura de datos’, subprocesoactual().nombre, ‘:’, self.data) def productor (uno mismo): “““configure el recurso que utilizará el consumidor””” print(‘DataResource - Método productor inicial’) con condición propia: print(‘DataResource - Datos de configuración del productor’) self.datos = randint(1, 100) print(‘DataResource - Productor notificando a todos hilos en espera’) self.condition.notifyAll() print(‘Principal - Inicio’) print(‘Principal - Creando el objeto DataResource’) recurso = DataResource() print(‘Principal - Crear hilos de consumidores’) c1 = Subproceso (objetivo = recurso.consumidor) c1.name = ‘Consumidor1’ c2 = Subproceso (objetivo = recurso.consumidor) c2.name = ‘Consumidor2’ print(‘Principal - Crear el hilo productor’) p = Subproceso (objetivo = recurso. productor) print(‘Principal - Iniciando subprocesos de consumidores’) c1.inicio() c2.inicio() dormir(1) print(‘Principal - Subproceso productor inicial’) p.inicio() imprimir(‘Principal - Listo’) 32.6 Condiciones de Python 387
El resultado de una ejecución de ejemplo de este programa es: Principal - Inicio Main - Creando el objeto DataResource DataResource - Inicializar los datos vacíos DataResource: configuración del objeto Condition Main - Crear los subprocesos de consumo Main - Crear el hilo del productor Principal: inicio de subprocesos de consumo DataResource: iniciando el método del consumidor en Consumer1 DataResource: iniciando el método del consumidor en Consumer2 Principal: subproceso de productor inicial DataResource: método de productor inicial DataResource: datos de configuración del productor Principal - Listo DataResource: el productor notifica todos los subprocesos en espera DataResource: el recurso está disponible para Consumer1 DataResource - Datos leídos en Consumer1: 36 DataResource: el recurso está disponible para Consumer2 DataResource - Datos leídos en Consumer2: 36 32.7 Semáforos de Python La clase Python Semaphore implementa el modelo de semáforo de conteo de Dijkstra. En general, un semáforo es como una variable entera, su valor está destinado a representan una serie de recursos disponibles de algún tipo. Normalmente hay dos operaciones disponibles en un semáforo; estas operaciones son adquirir() y re- lease() (aunque en algunas bibliotecas los nombres originales de Dijkstra de p() y v() se utilizan, estos nombres de operaciones se basan en las frases holandesas originales). • La operación adquirir() resta uno del valor del semáforo, a menos que el valor sea 0, en cuyo caso bloquea el subproceso de llamada hasta que el el valor del semáforo aumenta por encima de 0 de nuevo. • La operación signal() suma uno al valor, indicando una nueva instancia de el recurso se ha agregado al grupo. Tanto el threading.Semaphore como el multiprocessing.Semaphore Las clases también admiten el Protocolo de gestión de contexto. Un parámetro opcional usado con el constructor Semaphore da la inicial valor para el contador interno; por defecto es 1. Si el valor dado es menor que 0, Se genera ValueError. El siguiente ejemplo ilustra 5 subprocesos diferentes, todos ejecutando el mismo función trabajador(). La función trabajador() intenta adquirir un semáforo; si lo hace y luego continúa en el bloque de instrucciones with; si no lo hace, espera hasta que puede adquirirlo. Como el semáforo se inicializa a 2, solo puede haber dos hilos que puede adquirir el Semáforo a la vez. 388 32 Sincronización entre subprocesos/procesos
Sin embargo, el programa de muestra inicia cinco subprocesos, lo que significa que el Los 2 primeros subprocesos en ejecución adquirirán el semáforo y el resto tendrá esperar a adquirir el semáforo. Una vez que los dos primeros suelten el semáforo, otro dos pueden adquirirlo y así sucesivamente. de subprocesos import Thread, Semaphore, currentThread desde el tiempo de importación del sueño def trabajador(semáforo): con semáforo: imprimir (subproceso actual (). obtener nombre () + “- ingresado”) dormir (0.5) print(SubprocesoActual().getName() + " - saliendo”) print(‘Subproceso Principal - Inicio’) semáforo = semáforo(2) para i en el rango (0, 5): hilo = hilo (nombre = ‘T’ + str (i), objetivo = trabajador, args=[semáforo]) hilo.start() print(‘Subproceso Principal - Listo’) El resultado de una ejecución de este programa se muestra a continuación: Subproceso principal: inicio T0 - ingresado T1 - ingresado Subproceso principal - Listo T0 - saliendo T2 - ingresado T1 - saliendo T3 - ingresado T2 - saliendo T4 - ingresado T3 - saliendo T4 - saliendo 32.8 La clase de cola concurrente Como era de esperar, el modelo donde un productor Thread o Process genera los datos que van a ser procesados por uno o más subprocesos o procesos de consumidores es tan común que se proporciona una abstracción de mayor nivel en Python que el uso de Locks, Condiciones o Semáforos; este es el modelo de cola de bloqueo implementado por el Clases threading.Queue o multiprocessing.Queue. 32.7 Semáforos de Python 389
Ambas clases de cola son seguras para subprocesos y procesos. Eso es que trabajan apropiadamente (usando bloqueos internos) para administrar el acceso a datos desde subprocesos concurrentes o Procesos. Un ejemplo del uso de una cola para intercambiar datos entre un proceso de trabajo y el proceso principal se muestra a continuación. El proceso de trabajo ejecuta la función de trabajador () durmiendo, durante 2 s antes poniendo una cadena ‘Hello World’ en la cola. La función principal de la aplicación configura la cola y crea el proceso. La cola se pasa al proceso como uno de sus argumentos A continuación, se inicia el proceso. El proceso principal luego espera hasta que los datos son disponible en la cola a través de los métodos (de bloqueo) get(). Una vez que los datos son disponible se recupera e imprime antes de que finalice el proceso principal. de proceso de importación de multiprocesamiento, cola desde el tiempo de importación del sueño def trabajador(cola): print(‘Trabajador - yendo a dormir’) dormir(2) print(‘Trabajador - despertado y poniendo datos en la cola’) cola.put(‘Hola Mundo’) def principal(): print(‘Principal - Inicio’) cola = cola() p = Proceso (objetivo = trabajador, args = [cola]) print(‘Principal - Iniciando el proceso’) p.inicio() print(‘Principal - esperando datos’) imprimir (cola. obtener ()) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() El resultado de esto se muestra a continuación: Principal - Inicio Principal - Iniciando el proceso Principal - esperar datos Trabajador - ir a dormir Trabajador - despertado y poniendo datos en cola Hola Mundo Principal – Listo Sin embargo, esto no deja tan claro cómo la ejecución de los dos procesos entreteje El siguiente diagrama ilustra esto gráficamente: 390 32 Sincronización entre subprocesos/procesos
En el diagrama anterior, el proceso principal espera que se devuelva un resultado del hacer cola después de la llamada al método get(); como está esperando no está usando ninguna los recursos del sistema. A su vez, el proceso de trabajo duerme durante dos segundos antes de poner algunos datos en la cola (a través de put(‘Hello World’)). Después de enviar este valor a la cola, el valor se devuelve al proceso principal que se despierta (se retira del estado de espera) y puede continuar procesando el resto de la función principal. 32,9 Recursos en línea Consulte los siguientes recursos en línea para obtener la información que se analiza en este capítulo: • https://docs.python.org/3/library/threading.html para información en Hilo a base de barreras, candados, condiciones, semáforos y eventos. • https://docs.python.org/3/library/multiprocessing.html para información en Barreras, bloqueos, condiciones, semáforos y eventos basados en procesos. • https://en.wikipedia.org/wiki/Semaphore_programming Semáforo programa- modelo de ming 32.10 Ejercicios El objetivo de este ejercicio es implementar una versión concurrente de un sistema basado en Stack. contenedor/colección. Debería ser posible agregar datos de forma segura a la pila y extraer datos de la pila utilizando varios hilos. 32.8 La clase de cola concurrente 391
Debería seguir un patrón similar a la clase Queue descrita anteriormente pero admitir el comportamiento First In Last Out (FILO) de una pila y ser utilizable con cualquier número de subprocesos de productor y consumidor (puede ignorar los procesos para este ejercicio). La clave para implementar Stack es recordar que no se pueden leer datos desde la pila hasta que haya algunos datos para acceder; por lo tanto, es necesario esperar a datos para estar disponibles y luego leerlos. Sin embargo, es un hilo de productor que proporcionará esos datos y luego informará a cualquier subproceso en espera que no hay datos disponible. Puede implementar esto de la forma que desee; sin embargo un común La solución es usar una Condición. Para ilustrar esta idea, el siguiente programa de prueba se puede utilizar para verificar la comportamiento de su pila: de pila. Pila de importación de pila desde el tiempo de importación del sueño de subprocesos de importación Subproceso def productor (pila): para i en el rango (0,6): datos = ‘Tarea’ + str(i) print(‘Productor empujando:’, datos) stack.push(datos) dormir(2) def consumidor(etiqueta, pila): mientras que es cierto: imprimir (etiqueta, ‘pila.pop():’, pila.pop()) print(‘Crear pila compartida’) pila = pila () imprimir(‘Pila:’, pila) print(‘Creando e iniciando hilos de consumidores’) consumidor1 = Subproceso (objetivo = consumidor, args = (‘Consumidor1’, pila)) consumidor2 = Subproceso (objetivo = consumidor, args = (‘Consumidor2’, pila)) consumidor3 = Subproceso (objetivo = consumidor, args = (‘Consumidor3’, pila)) consumidor1.inicio() consumidor2.inicio() consumidor3.start() print(‘Creando e iniciando el hilo del productor’) productor = Subproceso (objetivo = productor, args = [pila]) productor.inicio() La salida generada a partir de este programa de ejemplo (que incluye sentencias de impresión from the Stack) se da a continuación: 392 32 Sincronización entre subprocesos/procesos
Crear pila compartida Pila: Pila: [] Creación e inicio de hilos de consumidores. Creando e iniciando el hilo del productor. Productor empujando: Task0 Consumidor1 pila.pop(): Tarea0 Productor empujando: Task1 Consumidor2 pila.pop(): Tarea1 Productor empujando: Task2 Consumidor3 pila.pop(): Tarea2 Productor empujando: Task3 Consumidor1 pila.pop(): Tarea3 Productor empujando: Task4 Consumidor2 pila.pop(): Tarea4 Productor empujando: Task5 Consumidor3 pila.pop(): Tarea5 32.10 Ejercicios 393
capitulo 33 Futuros 33.1 Introducción Un futuro es un hilo (o proceso) que promete devolver un valor en el futuro; una vez el comportamiento asociado se ha completado. Por lo tanto, es un valor futuro. Proporciona una muy forma sencilla de desencadenar un comportamiento que llevará mucho tiempo ejecutar o que pueden retrasarse debido a operaciones costosas como Entrada/Salida y que podría ralentizar la ejecución de otros elementos de un programa. Este capítulo discute futuros en Python. 33.2 La necesidad de un futuro En una invocación normal de método o función, el método o función se ejecuta en línea con el código de invocación (la persona que llama) que tiene que esperar hasta que la función o el método (el destinatario) regresa. Solo después de esto, la persona que llama puede continuar con la siguiente línea de código y ejecutar eso. En muchas (la mayoría) de las situaciones esto es exactamente lo que quieres como la siguiente línea de código puede depender de un resultado devuelto por la línea de código anterior etc. Sin embargo, en algunas situaciones, la siguiente línea de código es independiente de la anterior. línea de código. Por ejemplo, supongamos que estamos completando una interfaz de usuario (IU). La primera línea de código puede leer el nombre del usuario de algunos datos externos fuente (como una base de datos) y luego mostrarlo dentro de un campo en la interfaz de usuario. la siguiente linea de código puede agregar datos de hoy a otro campo en la interfaz de usuario. Estas dos líneas de código son independientes entre sí y pueden ejecutarse simultáneamente/en paralelo con cada uno otro. En esta situación, podríamos usar un hilo o un proceso para ejecutar los dos líneas de código independientemente del llamador, logrando así un nivel de concurrencia y permitiendo que la persona que llama pase a la tercera línea de código, etc. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_33 395
Sin embargo, ni el Subproceso ni el Proceso por defecto proporcionan una simple mecanismo para obtener un resultado de tal operación independiente. esto puede no ser un problema ya que las operaciones pueden ser autónomas; por ejemplo, pueden obtener datos desde la base de datos o desde la fecha de hoy y luego actualizó una interfaz de usuario. Sin embargo, en muchos situaciones, el cálculo devolverá un resultado que debe ser manejado por el código de invocación original (la persona que llama). Esto podría implicar realizar una ejecución prolongada cálculo y luego usar el resultado devuelto para generar otro valor o actualizar otro objeto etc Un futuro es una abstracción que simplifica la definición y ejecución de tales tareas concurrentes. Los futuros están disponibles en muchos idiomas diferentes, incluidos Python, pero también Java, Scala, C++, etc. Cuando se usa un futuro; un objeto invocable (como como una función) se pasa al futuro que ejecuta el comportamiento ya sea como un Subproceso separado o como un Proceso separado y luego puede devolver un resultado una vez que es generado. El resultado puede manejarse mediante una función de devolución de llamada (que se invoca cuando el resultado está disponible) o mediante una operación que esperará a que se obtenga un resultado. proporcionó. 33.3 Futuros en Python La biblioteca concurrent.futures se introdujo en Python en la versión 3.2 (y también está disponible en Python 2.5 en adelante). Los futuros concurrentes biblioteca proporciona la clase Future y una API de alto nivel para trabajar con Futures. La clase concurrent.futures.Future encapsula la clase asíncrona ejecución de un objeto invocable (por ejemplo, una función o método). La clase Future proporciona una variedad de métodos que se pueden usar para obtener información sobre el estado del futuro, recuperar resultados o cancelar el futuro: • cancel() Intento de cancelar el futuro. Si el futuro está siendo actualmente ejecutado y no se puede cancelar, entonces el método devolverá False, de lo contrario la llamada se cancelará y el método devolverá True. • cancelled() Devuelve True si el Future se canceló con éxito. • running() Devuelve True si Future se está ejecutando actualmente y no puede ser cancelado • done() Devuelve True si el futuro se canceló o finalizó con éxito correr. • result(timeout=None) Devuelve el valor devuelto por el Future. Si el El futuro aún no se ha completado, entonces este método esperará hasta los segundos de tiempo de espera. Si la llamada no se ha completado en segundos de tiempo de espera, entonces aparecerá un TimeoutError aumentó. timeout puede ser un int o un float. Si no se especifica el tiempo de espera o Ninguno, no hay límite para el tiempo de espera. Si el futuro se cancela antes de completar entonces se generará CancelledError. Si se genera la llamada, este método plantear la misma excepción. 396 33 Futuros
Sin embargo, debe tenerse en cuenta que las instancias futuras no deben crearse directamente, más bien, deben crearse a través del método de envío de un ejecutor apropiado. 33.3.1 Creación futura Los futuros son creados y ejecutados por ejecutores. Un Ejecutor proporciona dos métodos que se pueden usar para ejecutar un futuro (o futuros) y uno para cerrar el ejecutor En la raíz de la jerarquía de clases de ejecutor se encuentra concurrent.futures. Clase abstracta ejecutor. Tiene dos subclases: • ThreadPoolExecutor y • el ProcessPoolExecutor. El ThreadPoolExecutor usa hilos para ejecutar los futuros mientras que el ProcessPoolExecutor usa procesos separados. Por lo tanto, puede elegir cómo quieres que se ejecute el Future especificando uno u otro de estos ejecutores 33.3.2 Futuro de ejemplo simple Para ilustrar estas ideas, veremos un ejemplo muy simple del uso de un futuro. Para hacer esto, usaremos una función de trabajador simple; similar a la utilizada en el capítulos anteriores: desde el tiempo de importación del sueño
define la función que se usará con el futuro
def trabajador(mensaje): para i en el rango (0, 10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) vuelvo yo La única diferencia con esta versión de trabajador es que también devuelve un resultado que es el número de veces que el trabajador imprimió el mensaje. Por supuesto, podemos invocar este método en línea de la siguiente manera: res = trabajador(‘A’) imprimir (res) 33.3 Futuros en Python 397
Podemos convertir la invocación de este método en un Futuro. Para ello utilizamos un ThreadPoolExecutor importado del módulo concurrent.futures. Luego enviaremos la función de trabajo al grupo para su ejecución. Esto devuelve un referencia a un Futuro que podemos usar para obtener el resultado: desde el tiempo de importación del sueño de concurrent.futures import ThreadPoolExecutor print(‘Configurando ThreadPoolExecutor’) pool = ThreadPoolExecutor(1)
Enviar la función del grupo para que se ejecute
al mismo tiempo - obtener un futuro de la piscina
print(‘Enviando al trabajador al pool’) futuro = pool.submit(trabajador, ‘A’) print(‘Obtuve una referencia al objeto futuro’, futuro)
Obtenga el resultado del futuro - espere si es necesario
print(‘futuro.resultado():’, futuro.resultado()) imprimir(‘Terminado’) La salida de esto es: Configuración de ThreadPoolExecutor Sometiendo al trabajador a la piscina AAOobtuvo una referencia al objeto futuro <Futuro en 0x1086ea8d0 estado=en ejecución> AAAAAAAAfuturo.resultado(): 9 Hecho Observe cómo la salida del programa principal y el trabajador se entrelazan con se imprimen dos ‘A’ antes del mensaje que comienza con ‘Obtuvo un…’. En este caso, se crea un nuevo ThreadPoolExecutor con un hilo en el grupo (por lo general, habría varios subprocesos en el grupo, pero uno se está utilizando aquí con fines ilustrativos). El método de envío () se usa luego para enviar el trabajador de función con el parámetro ‘A’ al ThreadPoolExecutor para que programe la ejecución del función. El método submit() devuelve un objeto Future. El programa principal luego espera a que el objeto futuro devuelva un resultado (llamando el método result() en el futuro). Este método también puede tomar un tiempo de espera. Para cambiar este ejemplo para usar Procesos en lugar de Subprocesos, todo lo que se necesita es para cambiar el ejecutor del grupo a un ProcessPoolExecutor: 398 33 Futuros
El resultado de este programa es muy similar al anterior: Configuración de ThreadPoolExecutor Sometiendo al trabajador a la piscina Obtuvo una referencia al objeto futuro <Futuro en 0x109178630 estado=en ejecución> AAAAAAAAAAfuturo.resultado(): 9 Hecho La única diferencia es que en esta ejecución en particular, el mensaje comienza con ‘Obtenida a..’ se imprime antes de que se imprima cualquiera de las ‘A’s; esto puede deberse al hecho de que un Inicialmente, el proceso tarda más en configurarse que un hilo. 33.4 Ejecución de múltiples futuros Tanto ThreadPoolExecutor como ProcessPoolExecutor pueden ser configurado para admitir múltiples subprocesos/procesos a través del grupo. Cada tarea que es enviado al grupo se ejecutará dentro de un subproceso/proceso separado. si mas tareas se envían que subprocesos/procesos disponibles, entonces la tarea enviada esperará al primer subproceso/proceso disponible y luego se ejecutará. Esto puede actuar como una forma de gestionar la cantidad de trabajo simultáneo que se está realizando. Por ejemplo, en el siguiente ejemplo, la función trabajador() se envía a el grupo cuatro veces, pero el grupo está configurado para usar subprocesos. Así el cuarto trabajador tendrá que esperar hasta que uno de los tres primeros se complete antes de que pueda ejecutarse: de concurrent.futures import ProcessPoolExecutor print(‘Configurando ThreadPoolExecutor’) pool = ProcessPoolExecutor(1) print(‘Enviando al trabajador al pool’) futuro = pool.submit(trabajador, ‘A’) print(‘Obtuve una referencia al objeto futuro’, future1) print(‘futuro.resultado():’, futuro.resultado()) imprimir(‘Terminado’) de concurrent.futures import ThreadPoolExecutor imprimir(‘Iniciando…’) pool = ThreadPoolExecutor(3) futuro1 = pool.submit(trabajador, ‘A’) futuro2 = pool.submit(trabajador, ‘B’) futuro3 = pool.submit(trabajador, ‘C’) future4 = pool.submit(trabajador, ‘D’) print(’\nfuturo4.resultado():’, futuro4.resultado()) imprimir(‘Todo Listo’) 33.3 Futuros en Python 399
Cuando esto se ejecuta, podemos ver que los futuros para A, B y C se ejecutan al mismo tiempo. pero D debe esperar hasta que uno de los otros termine: A partir de… ABCACBCABCBABCACBACABCBACABCBADDDDDDDD futuro4.resultado(): 9 Todo listo El subproceso principal también espera a que future4 finalice, ya que solicita el resultado que es una llamada de bloqueo que solo regresará una vez que el futuro se haya completado y genere una resultado. Nuevamente, para usar Procesos en lugar de Subprocesos, todo lo que tenemos que hacer es reemplazar el ThreadPoolExecutor con ProcessPoolExecutor: de concurrent.futures import ProcessPoolExecutor imprimir(‘Iniciando…’) grupo = ProcessPoolExecutor(3) futuro1 = pool.submit(trabajador, ‘A’) futuro2 = pool.submit(trabajador, ‘B’) futuro3 = pool.submit(trabajador, ‘C’) future4 = pool.submit(trabajador, ‘D’) print(’\nfuturo4.resultado():’, futuro4.resultado()) imprimir(‘Todo Listo’) 33.4.1 Esperando a que se completen todos los futuros Es posible esperar a que se completen todos los futuros antes de avanzar. En el anterior sección se supuso que future4 sería el último futuro en completarse; pero en muchos casos puede que no sea posible saber qué futuro será el último en completarse. En tales situaciones, es muy útil poder esperar a que se completen todos los futuros. Antes de continuar. Esto se puede hacer usando concurrent.futures.wait función. Esta función toma una colección de futuros y, opcionalmente, un tiempo de espera y un indicador return_when. esperar (fs, tiempo de espera = ninguno, retorno_cuando = TODO_COMPLETO) dónde: • el tiempo de espera se puede utilizar para controlar el número máximo de segundos de espera antes de volver. timeout puede ser un int o un float. Si no se especifica el tiempo de espera o Ninguno, no hay límite para el tiempo de espera. • return_when indica cuándo debe regresar esta función. debe ser uno de las siguientes constantes: – FIRST_COMPLETED La función regresará cuando cualquier futuro termine o sea cancelado. 400 33 Futuros
– FIRST_EXCEPTION La función regresará cuando cualquier futuro termine por levantando una excepción. Si ningún futuro plantea una excepción, entonces es equivalente a TODO_COMPLETO. – ALL_COMPLETED La función regresará cuando todos los futuros terminen o estén cancelado. La función wait() devuelve dos conjuntos hecho y no_hecho. el primer conjunto contiene los futuros que completaron (terminaron o fueron cancelados) antes de la espera terminado. El segundo conjunto, not_dones, contiene futuros incompletos. Podemos usar la función esperar () para modificar el ejemplo anterior para que no ya no confíes en future4 terminando en último lugar: de concurrent.futures import ProcessPoolExecutor de concurrent.futures import esperar desde el tiempo de importación del sueño def trabajador(mensaje): para i en el rango (0,10): imprimir (mensaje, final = ‘’, descarga = Verdadero) dormir(1) vuelvo yo print(‘Iniciando… configurando grupo’) grupo = ProcessPoolExecutor(3) futuros = [] print(‘Enviando futuros’) futuro1 = pool.submit(trabajador, ‘A’) futuros.append(futuro1) futuro2 = pool.submit(trabajador, ‘B’) futuros.append(futuro2) futuro3 = pool.submit(trabajador, ‘C’) futuros.append(futuro3) future4 = pool.submit(trabajador, ‘D’) futuros.append(futuro4) print(‘Esperando a que se completen los futuros’) esperar (futuros) imprimir(’\nTodo Listo’) La salida de esto es: Comenzando… configurando la piscina Envío de futuros Esperando a que se completen los futuros ABCABCABCABCABCABCABCACBACBABCADDDDDDDDDD Todo listo Observe cómo se agrega cada futuro a la lista de futuros que luego se pasa al función esperar(). 33.4 Ejecución de múltiples futuros 401
33.4.2 Procesando los resultados como completados ¿Qué pasa si queremos procesar cada uno de los resultados devueltos por nuestra colección de futuros? Podríamos recorrer la lista de futuros en la sección anterior una vez que todos los resultados han sido generados. Sin embargo, esto significa que tendríamos que esperar a todos. completar antes de procesar la lista. En muchas situaciones nos gustaría procesar los resultados tan pronto como sean generado sin importar si ese es el primero, tercero, último o segundo, etc. La función concurrent.futures.as_completed() funciona de maravilla este; servirá cada futuro a su vez tan pronto como se completen; con toda futuros eventualmente siendo devueltos pero sin garantizar el pedido (solo que como tan pronto como un futuro termine de generar un resultado, estará disponible de inmediato). Por ejemplo, en el siguiente ejemplo, la función is_even() duerme durante un número aleatorio de segundos (asegurando que diferentes invocaciones de esta función tomar diferentes duraciones) luego calcula un resultado: El segundo ciclo for recorrerá cada futuro a medida que completan la impresión el resultado de cada uno, como se muestra a continuación: de concurrent.futures import ThreadPoolExecutor, as_completed desde el tiempo de importación del sueño de randint de importación aleatoria def es_par(n): print(‘Comprobando si’, n, ’es par’) dormir (randint (1, 5)) devuelve cadena(n) + ’ ’ + cadena(n % 2 == 0) imprimir(‘Iniciado’) datos = [1, 2, 3, 4, 5, 6] pool = ThreadPoolExecutor(5) futuros = [] para v en datos: futures.append(pool.submit(is_even, v)) para f en as_completed(futuros): imprimir(f.resultado()) imprimir(‘Terminado’) 402 33 Futuros
Como puede ver en esta salida, aunque los seis futuros se iniciaron en secuencia los resultados devueltos están en un orden diferente (siendo el orden devuelto 1, 4, 5, 3, 2 y finalmente 6). 33.5 Procesamiento de resultados futuros mediante una devolución de llamada Una alternativa al enfoque as_complete() es proporcionar una función que ser llamado una vez que se ha generado un resultado. Esto tiene la ventaja de que la principal el programa nunca se detiene; puede continuar haciendo lo que se requiera de él. La función a la que se llama una vez que se genera el resultado generalmente se conoce como devolución de llamada. función; es decir, el futuro vuelve a llamar a esta función cuando el resultado está disponible. Cada futuro puede tener una devolución de llamada separada ya que la función para invocar se establece en el futuro utilizando el método add_done_callback(). Este método toma el nombre de la función a invocar. Por ejemplo, en esta versión modificada del ejemplo anterior, especificamos una llamada Función de retroceso que se utilizará para imprimir el resultado de futuros. Esta función de devolución de llamada es llamado print_future_result(). Toma el futuro que ha completado como su argumento: Comenzó Comprobando si 1 es par Comprobando si 2 es par Comprobar si 3 es par Comprobando si 4 es par Comprobar si 5 es par Comprobar si 6 es par 1 falso 4 cierto 5 falso 3 falso 2 Verdadero 6 Verdadero Hecho 33.4 Ejecución de múltiples futuros 403
Cuando ejecutamos esto, podemos ver que la función de devolución de llamada se llama después de la principal el hilo se ha completado. Nuevamente, el orden no está especificado como la función is_even() todavía duerme por una cantidad aleatoria de tiempo. Comenzó Comprobando si 1 es par Comprobando si 2 es par Comprobar si 3 es par Comprobando si 4 es par Comprobar si 5 es par Hecho En devolución de llamada Resultado futuro: 1 Falso Comprobar si 6 es par En devolución de llamada Resultado futuro: 5 Falso En devolución de llamada Resultado futuro: 4 Verdadero En devolución de llamada Resultado futuro: 3 Falso En devolución de llamada Resultado futuro: 2 Verdadero En devolución de llamada Resultado futuro: 6 Verdadero de concurrent.futures import ThreadPoolExecutor desde el tiempo de importación del sueño de randint de importación aleatoria def es_par(n): print(‘Comprobando si’, n, ’es par’) dormir (randint (1, 5)) devuelve cadena(n) + ’ ’ + cadena(n % 2 == 0) def print_future_result(futuro): print(‘En devolución de llamada Resultado futuro: ‘, futuro.resultado()) imprimir(‘Iniciado’) datos = [1, 2, 3, 4, 5, 6] pool = ThreadPoolExecutor(5) para v en datos: futuro = pool.submit(is_even, v) futuro.add_done_callback(print_future_result) imprimir(‘Terminado’) 404 33 Futuros
33.6 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre futuros: • https://docs.python.org/3/library/concurrent.futures.html El estándar Python Documentación de la biblioteca sobre Futuros. • https://pymotw.com/3/concurrent.futures La página Módulo Python de la semana en Futuros. • https://www.blog.pythonlibrary.org/2016/08/03/python-3-concurrency-the- concurrent-futures-module un tutorial alternativo sobre Python Futures. 33.7 Ejercicios En matemáticas, el factorial de un entero positivo n, denotado por n!, es el pro- conducto de todos los enteros positivos menores o iguales a n. Por ejemplo, 5! = 5 � 4 � 3 � 2 � 1 = 120 Tenga en cuenta que el valor de 0! es 1 Escribe un Futuro que calcule el factorial de cualquier número con el resultado imprimiéndose a través de una función de devolución de llamada. Hay varias formas en las que el valor factorial se puede calcular usando un bucle for o una función recursiva. En cualquier caso, duerma durante un milisegundo entre cada cálculo. Comience múltiples Futuros para diferentes valores factoriales y vea cuál regresa primero 33.6 Recursos en línea 405
capitulo 34 Concurrencia con AsyncIO 34.1 Introducción Las instalaciones de Async IO en Python son adiciones relativamente recientes que originalmente introdujeron producido en Python 3.4 y evolucionando hasta Python 3.7 inclusive. Ellos son compuesto (a partir de Python 3.7) de dos nuevas palabras clave async y await (introducidas en Python 3.7) y el paquete Async IO Python. En este capítulo, primero analizamos E/S asíncrona antes de presentar la asíncrona. y esperar palabras clave. Luego presentamos Async IO Tasks, cómo se crean y se utilizan y gestionado. 34.2 E/S asíncrona La E/S asíncrona (o E/S asíncrona) es una programación concurrente independiente del lenguaje. modelo (o paradigma) que ha sido implementado en varias programaciones diferentes (como C# y Scala) así como en Python. La E/S asíncrona es otra forma en la que puede crear aplicaciones simultáneas en Python. Es en muchos sentidos una alternativa a las instalaciones proporcionadas por el Biblioteca de subprocesos en Python. Sin embargo, donde la biblioteca Threading es más susceptible a problemas asociados con el GIL (The Global Interpreter Lock) que puede afectar el rendimiento, las instalaciones de Async IO están mejor aisladas de este problema. La forma en que opera Async IO también es más liviana que las instalaciones. proporcionar día la biblioteca de multiprocesamiento ya que las tareas asíncronas en La E/S asíncrona se ejecuta dentro de un solo proceso en lugar de requerir procesos separados para ser generada en el hardware subyacente. Asíncrono IO es, por lo tanto, otra forma alternativa de implementar concurrente soluciones a problemas. Cabe señalar que no se basa en Threading o Multiprocesamiento; en cambio, Async IO se basa en la idea de cooperativa © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_34 407
multitarea Estas tareas de cooperación operan de forma asíncrona; con esto queremos decir que las tareas: • son capaces de operar por separado de otras tareas, • son capaces de esperar a que otra tarea devuelva un resultado cuando sea necesario, • y, por lo tanto, pueden permitir que se ejecuten otras tareas mientras esperan. El aspecto IO (Entrada/Salida) del nombre Async IO se debe a que esta forma de El programa concurrente se adapta mejor a las tareas enlazadas de E/S. En una tarea enlazada de E/S, un programa pasa la mayor parte de su tiempo enviando datos a, o leer datos de algún tipo de dispositivo externo (por ejemplo, una base de datos o un conjunto de archivos, etc.). Esta comunicación requiere mucho tiempo y significa que el programa pasa la mayor parte de su tiempo esperando una respuesta del dispositivo externo. Una forma en que tales aplicaciones vinculadas a E/S pueden (aparentemente) acelerarse es superponer la ejecución de diferentes tareas; por lo tanto, mientras una tarea está esperando una base de datos para responder con algunos datos, otra tarea puede ser escribir datos en un archivo de registro etc. 34.3 Bucle de eventos de E/S asíncrono Cuando está desarrollando código usando las instalaciones Async IO, no necesita preocuparse por cómo funcionan los componentes internos de la biblioteca Async IO; sin embargo, al menos en el nivel conceptual es útil entender un concepto clave; el de Async IO bucle de eventos; Este bucle controla cómo y cuándo se ejecuta cada tarea. Para los fines de esta discusión, una tarea representa algún trabajo que se puede ejecutar independientemente de otras piezas de trabajo. El bucle de eventos sabe sobre cada tarea que se ejecutará y cuál es el estado de la tarea. actualmente es (por ejemplo, si está esperando que algo suceda/se complete). Él selecciona una tarea que está lista para ejecutarse de la lista de tareas disponibles y la ejecuta. Esta tarea tiene el control total de la CPU hasta que complete su trabajo o devuelve el control al bucle de eventos (por ejemplo, porque ahora debe esperar algunos datos se suministrarán desde una base de datos). El Event Loop ahora verifica para ver si cualquiera de las tareas en espera está lista para continuar ejecutándose y toma nota de su estado. Event Loop luego selecciona otra tarea que está lista para ejecutarse y la inicia. tarea desactivada. Este ciclo continúa hasta que todas las tareas han terminado. esto se ilustra abajo: 408 34 Concurrencia con AsyncIO
Un punto importante a tener en cuenta en la descripción anterior es que una tarea no da encienda el procesador a menos que decida hacerlo, por ejemplo, al tener que esperar algo demás. Nunca los interrumpen en medio de una operación; esto evita la problema que pueden tener dos subprocesos cuando un planificador separado divide el tiempo ya que ambos pueden estar compartiendo el mismo recurso. Esto puede simplificar enormemente su código. 34.4 Las palabras clave Async y Await La palabra clave async, introducida en Python 3.7, se usa para marcar una función como algo que usa la palabra clave await (volveremos a esto más adelante ya que es otro uso de la palabra clave async). Una función que usa la palabra clave await se puede ejecutar como una tarea separada y puede ceder el control del procesador cuando llama esperar contra otra función asíncrona y debe esperar a que esa función se com- completo La función asíncrona invocada puede ejecutarse como una tarea separada, etc. Para invocar una función asíncrona, es necesario iniciar el bucle de eventos de E/S asíncrona y para que esa función sea tratada como una tarea por el bucle de eventos. Esto se hace por llamando al método asyncio.run() y pasando la función asincrónica raíz. La función asyncio.run() se introdujo en Python 3.7 (versiones anteriores de Python, como Python 3.6, requería que obtuvieras explícitamente una referencia al Evento Loop y para ejecutar la función asincrónica raíz a través de eso). Un punto a tener en cuenta sobre esto función es que se ha marcado como provisional en Python 3.7. Esto significa que las futuras versiones de Python pueden o no admitir la función o pueden modificar la función de alguna manera. Por lo tanto, debe comprobar la documentación de la versión de Python que está utilizando para ver si el método de ejecución ha sido alterado o no. 34.4.1 Uso de Async y Await Examinaremos un programa Async IO muy simple de arriba hacia abajo. El principal() La función para el programa se da a continuación: def principal() : print(‘Principal - Inicio’) asyncio.run(hacer_algo()) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() 34.3 Bucle de eventos de E/S asíncrono 409
La función main() es el punto de entrada para el programa y llama: asyncio.run(hacer_algo()) Esto inicia la ejecución del bucle de eventos Async IO y da como resultado el do_some- La función thing () se envuelve en una tarea que es administrada por el ciclo. Nota que no cree explícitamente una tarea en Async IO; siempre son creados por alguna función, sin embargo, es útil estar al tanto de las tareas, ya que puede interactuar con para comprobar su estado o para recuperar un resultado. La función do_something() está marcada con la palabra clave async: definición asíncrona hacer_algo(): print(‘hacer_algo - esperará al trabajador’) resultado = esperar trabajador() print(‘hacer_algo - resultado:’, resultado) Como se mencionó anteriormente, esto indica que se puede ejecutar como una tarea separada y que puede utilizar la palabra clave await para esperar a que se produzca alguna otra función o comportamiento. completo. En este caso, la función asíncrona do_something() debe esperar la función del trabajador () para completar. La palabra clave await hace más que simplemente indicar que el do_something () La función debe esperar a que se complete el trabajador. Activa otra tarea para ser creado que ejecutará la función de trabajador () y libera el procesador permitiendo que Event Loop seleccione la siguiente tarea a ejecutar (que puede o no ser la tarea que ejecuta la función trabajador()). El estado del do_something la tarea ahora está esperando mientras el estado de la tarea del trabajador () está listo (para ejecutarse). El código para la tarea del trabajador se proporciona a continuación: trabajador asíncrono de definición (): print(’trabajador - tomará algún tiempo’) tiempo.dormir(3) print(’trabajador - Listo’) volver 42 La palabra clave async nuevamente indica que esta función se puede ejecutar como una tarea. Sin embargo, esta vez el cuerpo de la función no usa la palabra clave await. Esto se debe a que se trata de un caso especial conocido como función corrutina Async IO. Este es una función que devuelve un valor de una tarea (está relacionado con la idea de un estándar Python coroutine que es un consumidor de datos). Tristemente, Computer Science tiene muchos ejemplos donde el mismo término ha sido utilizado para diferentes cosas, así como ejemplos en los que se han utilizado diferentes términos por lo mismo En este caso, para evitar confusiones, solo quédese con Async IO coroutines son funciones marcadas con async que se pueden ejecutar como una tarea separada y puede llamar esperar. 410 34 Concurrencia con AsyncIO
La lista completa del programa se muestra a continuación: importar asyncio tiempo de importación trabajador asíncrono de definición (): print(’trabajador - tomará algún tiempo’) tiempo.dormir(3) print(’trabajador - hecho’) volver 42 definición asíncrona hacer_algo(): print(‘hacer_algo - esperará al trabajador’) resultado = esperar trabajador() print(‘hacer_algo - resultado:’, resultado) def principal(): print(‘Principal - Inicio’) asyncio.run(hacer_algo()) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() Cuando se ejecuta este programa, la salida es: Principal - Inicio do_something - esperará al trabajador trabajador - tomará algún tiempo trabajador - hecho hacer_algo – resultado: 42 Principal – Listo Cuando se ejecuta, hay una pausa entre las dos impresiones del trabajador mientras duerme. Aunque no es del todo obvio aquí, la función do_something() se ejecutó como una tarea, esta tarea luego esperó cuando llegó a la función trabajador () que se ejecutó como otra tarea. Una vez que la tarea del trabajador completó el do_some- cosa podría continuar y completar su operación. Una vez que esto sucedió, el El bucle de eventos de E/S asíncrono podría finalizar ya que no había más tareas disponibles. 34.5 Tareas de E/S asíncronas Las tareas se utilizan para ejecutar funciones marcadas con la palabra clave async al mismo tiempo. Las tareas nunca se crean directamente, sino que se crean implícitamente a través de la palabra clave await o mediante funciones como asyncio.run descritas anteriormente o 34.4 Las palabras clave Async y Await 411
asyncio.create_task(), asincio.reunir() y asincio.as_- terminado(). Estas funciones adicionales de creación de tareas se describen a continuación: • asyncio.create_task() Esta función toma una función marcada con async y lo envuelve dentro de una Tarea y lo programa para que lo ejecute el Bucle de eventos de E/S asíncrono. Esta función se agregó en Python 3.7. • asyncio.gather(*aws) Esta función ejecuta todas las funciones asíncronas pasadas como tareas separadas. Reúne los resultados de cada tarea por separado y los devuelve como una lista. El orden de los resultados corresponde al orden de los funciones asíncronas en la lista de aws. • asyncio.as_completed(aws) Ejecuta cada una de las funciones asíncronas pasadas lo. Un objeto Task admite varios métodos útiles • cancel() cancela una tarea en ejecución. Llamar a este método hará que la Tarea lanzar una excepción CancelledError. • cancelled() devuelve True si la tarea ha sido cancelada. • done() devuelve True si la tarea se completó, generó una excepción o fue cancelado. • result() devuelve el resultado de la Tarea si se ha realizado. Si el resultado de Tareas no es aún disponible, entonces el método genera la excepción InvalidStateError. •Exception() devuelve una excepción si la Tarea generó una. si la tarea fue cancelado y genera la excepción CancelledError. Si la tarea no es aún hecho, genera una excepción InvalidStateError. También es posible agregar una función de devolución de llamada para invocar una vez que la tarea se haya completado. completado (o para eliminar dicha función si se ha agregado): • add_done_callback(callback) Agrega una devolución de llamada que se ejecutará cuando el La tarea está hecha. • remove_done_callback(devolución de llamada) Elimina la devolución de llamada de la llamada- lista de espaldas. Tenga en cuenta que el método se llama ‘agregar’ en lugar de ’establecer’, lo que implica que puede haber múltiples funciones llamadas cuando la tarea se ha completado (si es necesario). El siguiente ejemplo ilustra algunos de los anteriores: trabajador asíncrono de definición (): print(’trabajador - tomará algún tiempo’) espera asyncio.sleep(1) print(’trabajador - Listo’) volver 42 def print_it(tarea): imprimir(‘imprimir_resultado:’, tarea.resultado()) importar asyncio 412 34 Concurrencia con AsyncIO
En este ejemplo, la función trabajador() está envuelta dentro de un objeto de tarea que es devuelto de la llamada asyncio.create_task(worker()). Una función (print_it()) se registra como una devolución de llamada en la tarea usando el función asyncio.create_task(trabajador()). Tenga en cuenta que el trabajador se pasa la tarea que ha completado como un parámetro. Esto le permite obtener información de la tarea como cualquier resultado generado. En este ejemplo, la función asíncrona do_something() espera explícitamente en el tarea a completar. Una vez que esto sucede, se utilizan varios métodos diferentes para obtener información sobre la tarea (como si se canceló o no). Otro punto a tener en cuenta sobre este listado es que en la función trabajador() haber agregado una espera usando la función asyncio.sleep(1); esto permite que trabajador a dormir y esperar a que se complete la tarea desencadenada; es un IO asíncrono alternativa a time.sleep(1). La salida de este programa es: Principal - Inicio do_something: crea una tarea para el trabajador do_something - agregar una devolución de llamada trabajador - tomará algún tiempo trabajador - Hecho imprimir_it resultado: 42 hacer_algo - tarea.cancelado(): Falso hacer_algo - tarea.hecho(): Verdadero hacer_algo - tarea.resultado(): 42 hacer_algo - tarea.excepción(): Ninguno hacer_algo - terminado Principal - Listo definición asíncrona hacer_algo(): print(‘hacer_algo - crear tarea para el trabajador’) tarea = asyncio.create_task(trabajador()) print(‘hacer_algo - agregar una devolución de llamada’) tarea.add_done_callback(print_it) esperar tarea
Información sobre la tarea
print(‘hacer_algo - tarea.cancelado():’, tarea.cancelado()) print(‘hacer_algo - tarea.hecho():’, tarea.hecho()) print(‘hacer_algo - tarea.resultado():’, tarea.resultado()) print(‘hacer_algo - tarea.excepción():’, tarea.excepción()) print(‘hacer_algo - terminado’) def principal() : print(‘Principal - Inicio’) asyncio.run(hacer_algo()) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() 34.5 Tareas de E/S asíncronas 413
34.6 Ejecución de múltiples tareas En muchos casos es útil poder ejecutar varias tareas al mismo tiempo. Hay dos opciones proporcionadas para esto, asyncio.gather() y asyncio. función as_completed(); veremos ambos en esta sección. 34.6.1 Recopilación de resultados de varias tareas A menudo es útil recopilar todos los resultados de un conjunto de tareas y continuar sólo una vez obtenidos todos los resultados. Al usar subprocesos o procesos, este se puede lograr iniciando varios subprocesos o procesos y luego usando algunos otro objeto como una Barrera para esperar a que todos los resultados estén disponibles antes continuo. Dentro de la biblioteca Async IO todo lo que se requiere es usar el asyn- función cio.gather() con una lista de funciones asíncronas para ejecutar, por ejemplo: importar asyncio importar al azar trabajador asíncrono de definición (): print(‘Trabajador - tomará algún tiempo’) espera asyncio.sleep(1) resultado = aleatorio.randint(1,10) print(‘Trabajador - Listo’) resultado devuelto definición asíncrona hacer_algo(): print(‘hacer_algo - esperará al trabajador’)
Ejecute tres llamadas al trabajador al mismo tiempo y recopile
resultados resultados = esperar asyncio.gather(trabajador(), trabajador(), obrero()) print(‘resultados de llamadas:’, resultados) def principal() : print(‘Principal - Inicio’) asyncio.run(hacer_algo()) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() En este programa, la función do_something() usa resultados = esperar asyncio.gather(trabajador(), trabajador(), trabajador()) para ejecutar tres invocaciones de la función worker() en tres Tareas separadas y para esperar a que los resultados de los tres estén disponibles antes de que se devuelvan como una lista de valores y almacenados en la variable de resultados. 414 34 Concurrencia con AsyncIO
Esto hace que sea muy fácil trabajar con múltiples tareas simultáneas y cotejar sus resultados Tenga en cuenta que en este ejemplo de código, la función asincrónica del trabajador devuelve un resultado aleatorio número entre 1 y 10. La salida de este programa es: do_something - esperará al trabajador Trabajador - tomará algún tiempo Trabajador - tomará algún tiempo Trabajador - tomará algún tiempo Trabajador - Hecho Trabajador - Hecho Trabajador - Hecho resultados de llamadas: [5, 3, 4] Principal – Listo Principal - Inicio Como puede ver en esto, las tres invocaciones de trabajadores se inician pero luego suelte el procesador mientras duermen. Después de esto las tres tareas se despiertan y completo antes de que los resultados se recopilen e impriman. 34.6.2 Manejo de resultados de tareas a medida que están disponibles Otra opción cuando se ejecutan varias tareas es manejar los resultados a medida que se vuelven disponibles, en lugar de esperar a que se proporcionen todos los resultados antes de continuar. Este La opción es compatible con la función asyncio.as_completed(). Esta función devuelve un iterador de funciones asíncronas que se servirán tan pronto como tengan completaron su trabajo. La construcción for-loop se puede usar con el iterador devuelto por la función; sin embargo, dentro del bucle for, el código debe llamar await en las funciones asíncronas devuelto para que se pueda obtener el resultado de la tarea. Por ejemplo: definición asíncrona hacer_algo(): print(‘hacer_algo - esperará al trabajador’)
Ejecute tres llamadas al trabajador al mismo tiempo y recopile
resultados para async_func en asyncio.as_completed((trabajador(‘A’), trabajador(‘B’), trabajador(‘C’))): resultado = esperar async_func print(‘hacer_algo - resultado:’, resultado) Tenga en cuenta que la función asyncio.as_completed() toma un contenedor como una tupla de funciones asíncronas. 34.6 Ejecución de múltiples tareas 415
También hemos modificado ligeramente la función del trabajador para que se agregue una etiqueta a la número aleatorio generado para que quede claro qué invocación de la función del trabajador devolver qué resultado: trabajador asíncrono def (etiqueta): print(‘Trabajador - tomará algún tiempo’) espera asyncio.sleep(1) resultado = aleatorio.randint(1,10) print(‘Trabajador - Listo’) etiqueta de retorno + str(resultado) Cuando ejecutamos este programa def principal() : print(‘Principal - Inicio’) asyncio.run(hacer_algo()) imprimir(‘Principal - Listo’) la salida es Principal - Inicio do_something - esperará al trabajador Trabajador - tomará algún tiempo Trabajador - tomará algún tiempo Trabajador - tomará algún tiempo Trabajador - Hecho Trabajador - Hecho Trabajador - Hecho hacer_algo - resultado: C2 hacer_algo - resultado: A1 hacer_algo - resultado: B10 Principal – Listo Como puede ver en esto, los resultados no se devuelven en el orden en que las tareas se crean, la tarea ‘C’ se completa primero, seguida de ‘A’ y ‘B’. Esto ilustra la comportamiento de la función asyncio.as_completed(). 34.7 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre futuros: • https://docs.python.org/3/library/asyncio-task.html La biblioteca estándar de Python documentación sobre AsyncIO. • https://pymotw.com/3/asyncio La página Módulo Python de la semana en E/S asíncrono. • https://pythonprogramming.net/asyncio-basics-intermediate-python-tutorial Un Tutorial de AsyncIO. 416 34 Concurrencia con AsyncIO
34.8 Ejercicios Este ejercicio utilizará las instalaciones de la biblioteca AsyncIO para calcular un conjunto de números factoriales. El factorial de un entero positivo es el producto de todos los enteros positivos menores que o igual a n. Por ejemplo, 5! = 5x4x3x2x1 = 120 Tenga en cuenta que el valor de 0! es 1, Cree una aplicación que utilizará las palabras clave async y await para calcular los factoriales de un conjunto de números. La función factorial debe esperar por 0.1 de un segundo (usando asyncio.sleep(0.1)) cada vez que se redondea el ciclo usado para calcular Calcular el factorial de un número. Puede usar con asyncio.as_completed() o asyncio.gather() para recoger los resultados. También puede usar una lista de comprensión para crear la lista de llamadas al factorial función. La función principal podría verse así: def principal(): print(‘Principal - Inicio’) asyncio.run(calcular_factoriales([5, 7, 3, 6])) imprimir(‘Principal - Listo’) si nombre == ‘principal’: principal() 34.8 Ejercicios 417
Parte VIII Programación reactiva
capitulo 35 Introducción a la programación reactiva 35.1 Introducción En este capítulo introduciremos el concepto de Programación Reactiva. Reactivo la programación es una forma de escribir programas que permiten que el sistema reaccione a los datos siendo publicado en él. Veremos la biblioteca RxPy que proporciona un Python implementación del enfoque ReactiveX para la Programación Reactiva. 35.2 ¿Qué es una aplicación reactiva? Una aplicación reactiva es aquella que debe reaccionar a los datos; normalmente ya sea a la presencia de nuevos datos, o a cambios en los datos existentes. El Manifiesto Reactivo presenta las características clave de Reactive Systems como: • Sensible. Esto significa que dichos sistemas responden de manera oportuna. aquí de el curso oportuno diferirá según la aplicación y el dominio; en uno situación un segundo puede ser oportuno en otro puede ser demasiado lento. • Resiliente. Dichos sistemas se mantienen receptivos frente a fallas. Los sistemas deben por lo tanto, estar diseñado para manejar fallas con gracia y continuar trabajando apropiadamente después de la falla. • Elástico. A medida que aumenta la carga de trabajo, el sistema debería seguir respondiendo. • Impulsado por mensajes. La información se intercambia entre elementos de un reactivo. sistema mediante mensajes. Esto asegura acoplamiento flojo, aislamiento y ubicación. transparencia entre estos componentes. Como ejemplo, considere una aplicación que enumera un conjunto de Equity Stock Trade valores basados en los últimos datos de precios fijos del mercado. Esta aplicación podría presentar la valor actual de cada comercio dentro de una tabla. Cuando se obtienen nuevos datos sobre el precio de las acciones en el mercado © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_35 421
publicado, entonces la aplicación debe actualizar el valor de la operación dentro de la tabla. Tal aplicación puede describirse como reactiva. La programación reactiva es un estilo de programación (típicamente compatible con bibliotecas) que permite escribir código que sigue las ideas de los sistemas reactivos. Por supuesto el hecho de que parte de una aplicación utilice una biblioteca de programación reactiva no hacer que toda la aplicación sea reactiva; de hecho, puede que sólo sea necesario para una parte de un aplicación para exhibir un comportamiento reactivo. 35.3 El proyecto ReactiveX ReactiveX es la implementación más conocida de la Programación Reactiva paradigma. ReactiveX se basa en el patrón de diseño Observer-Observable. Sin embargo es un extensión a este patrón de diseño, ya que extiende el patrón de tal manera que el enfoque admite secuencias de datos y/o eventos y agrega operadores que permiten a los desarrolladores para componer secuencias juntas de forma declarativa mientras se abstrae de las preocupaciones asociado con subprocesos de bajo nivel, sincronización, estructuras de datos concurrentes y E/S sin bloqueo. El proyecto ReactiveX tiene implementaciones para muchos idiomas, incluidos RxJava, RxScala y RxPy; esta última es la versión que estamos viendo ya que es para el Lenguaje pitón. RxPy se describe como: Una biblioteca para componer programas asincrónicos y basados en eventos usando la columna Observable. lecciones y funciones de operador de consulta en Python 35.4 El patrón del observador El patrón Observer es uno del conjunto de patrones de diseño de Gang of Four. La pandilla de cuatro patrones (como se describe originalmente en Gamma et al. 1995) se denominan así porque este libro sobre patrones de diseño fue escrito por cuatro autores muy famosos a saber; Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides. El patrón de observador proporciona una forma de garantizar que un conjunto de objetos sea notificado cada vez que cambia el estado de otro objeto. Ha sido ampliamente utilizado en un número de lenguajes (como Smalltalk y Java) y también se puede usar con Python. La intención del patrón observador es administrar una relación de uno a muchos entre un objeto y aquellos objetos interesados en el estado, y en particular el estado cambios, de ese objeto. Así, cuando el estado de los objetos cambia, el interesado (de- pendiente) los objetos son notificados de ese cambio y pueden tomar cualquier acción adecuado. 422 35 Introducción a la programación reactiva
Hay dos roles clave dentro del Patrón del Observador, estos son el Observable y los roles de observador. • Observables. Este es el objeto que es responsable de notificar a otros objetos que se ha producido un cambio en su estado • Observador. Un observador es un objeto que será notificado del cambio de estado de el Observable y puede tomar la acción apropiada (como desencadenar un cambio en su propio estado o realizando alguna acción). Además, el estado normalmente se representa explícitamente: • Estado. Este papel puede ser desempeñado por un objeto que se utiliza para compartir información sobre el cambio de estado que ha ocurrido dentro del Observable. Esto podría ser tan simple como una Cadena que indica el nuevo estado del Observable o podría ser un objeto orientado a datos que proporciona información más detallada. Estos roles se ilustran en la siguiente figura. En la figura anterior, el objeto Observable publica datos en un flujo de datos. El los datos en el flujo de datos se envían a cada uno de los observadores registrados con el Observable. De esta forma, los datos se transmiten a todos los Observadores de un Observable. Es común que un Observable solo publique datos una vez que hay un Observador disponibles para procesar esos datos. El proceso de registro con un Observable es denominado suscripción. Por lo tanto, un Observable tendrá cero o más suscriptores. (Observadores). Si el Observable publica datos a un ritmo más rápido que el que puede procesar el Observer, los datos se ponen en cola a través del flujo de datos. Esto permite que el observador procesar los datos recibidos uno a la vez a su propio ritmo; sin ninguna preocupación por los datos pérdida (siempre que haya suficiente memoria disponible para el flujo de datos). 35.5 Observables fríos y calientes Otro concepto que es útil entender es el de Observables Fríos y Calientes. • Los Observables fríos son Observables perezosos. Es decir, un Observable frío solo publicar datos si al menos un observador está suscrito. 35.4 El patrón del observador 423
• Hot Observables, por el contrario, publican datos si hay un observador sub- escrito o no. 35.5.1 Observables fríos Un Cold Observable no publicará ningún dato a menos que haya al menos un observador suscrito para procesar esos datos. Además, un Observable frío solo proporciona datos para un observador cuando ese observador esté listo para procesar los datos; esto es porque el La relación Observable-Observador es más una relación de atracción. Por ejemplo, dado un Observable que generará un conjunto de valores basados en un rango, entonces eso Observable generará cada resultado de forma perezosa cuando lo solicite un observador. Si el Observador tarda un tiempo en procesar los datos emitidos por el Observable, entonces el Observable esperará hasta que el Observador esté listo para procesar los datos antes emitiendo otro valor. 35.5.2 Observables calientes Hot Observables, por el contrario, publica datos si hay un observador suscrito O no. Cuando un Observador se registra en el Observable, comenzará a recibir datos en ese momento, a medida que el Observable publique nuevos datos. Si el Observable tiene elementos de datos anteriores ya publicados, estos se habrán perdido y el El observador no recibirá esos datos. La situación más común en la que se crea un Hot Observable es cuando el el productor fuente representa datos que pueden ser irrelevantes si no se procesan inmediatamente o puede ser reemplazado por datos posteriores. Por ejemplo, los datos publicados por un Stock La fuente de datos de precios de mercado entraría en esta categoría. Cuando un Observable se envuelve alrededor de esta fuente de datos, puede publicar esos datos ya sea que un observador sea o no suscrito 35.5.3 Implicaciones de observables fríos y calientes Es importante saber si tienes un Observable frío o caliente porque esto puede impacto en lo que puede suponer sobre los datos suministrados a los Observadores y, por lo tanto, cómo necesita diseñar su aplicación. Si es importante que no se pierdan datos, entonces se necesita cuidado para garantizar que los suscriptores estén en su lugar antes de un Hot Observable comienza a publicar datos (cuando esto no es una preocupación para un Observable frío). 424 35 Introducción a la programación reactiva
35.6 Diferencias entre la programación dirigida por eventos y Programación Reactiva En la programación dirigida por eventos, se genera un evento en respuesta a algo sucediendo; el evento luego representa esto con cualquier dato asociado. Por ejemplo, si el usuario hace clic con el mouse, entonces un MouseClickEvent asociado podría ser generado. Este objeto generalmente contendrá información sobre las coordenadas x e y del mouse junto con qué botón se hizo clic, etc. Entonces es posible asociar algún comportamiento (como una función o un método) con este evento de modo que si ocurre el evento, luego se invoca la operación asociada y el objeto del evento es proporcionado como un parámetro. Este es ciertamente el enfoque utilizado en wxPython biblioteca presentada anteriormente en este libro: Del diagrama anterior, cuando se genera un MoveEvent, on_move() se llama al método y el evento se pasa al método. En el enfoque de programación reactiva, un observador está asociado con un Observable. Todos los datos generados por el Observable serán recibidos y tratados por el observador. Esto es cierto sean cuales sean los datos, ya que el Observador es un manejador de datos. generado por el Observable en lugar de un controlador de un tipo específico de datos (como con el enfoque basado en eventos). Ambos enfoques podrían usarse en muchas situaciones. Por ejemplo, podríamos tener un escenario en el que se procesarán algunos datos cada vez que cambie el precio de una acción. Esto podría implementarse utilizando un StockPriceChangeEvent asociado con un StockPriceEventHandler. También podría implementarse a través de Stock PriceChangeObserverable y StockPriceChangeObserver. En en cualquier caso, un elemento maneja los datos generados por otro elemento. Sin embargo, la biblioteca RxPy simplifica este proceso y permite que Observer se ejecute en el mismo subproceso como, o un subproceso separado del Observable con solo un pequeño cambio en el código. 35.7 Ventajas de la programación reactiva Hay varias ventajas en el uso de una biblioteca de Programación Reactiva, estas incluir: • Evita múltiples métodos de devolución de llamada. Los problemas asociados con el uso de las devoluciones de llamada a veces se denominan infierno de devolución de llamada. Esto puede ocurrir cuando hay son múltiples devoluciones de llamada, todas definidas para ejecutarse en respuesta a algunos datos que se generan. erated o alguna operación completando. Puede ser difícil de entender, mantener y depurar dichos sistemas. 35.6 Diferencias entre la programación dirigida por eventos y la programación reactiva 425
• Ejecución asíncrona y multiproceso más simple. El enfoque adoptado por RxPy hace que sea muy fácil ejecutar operaciones/comportamiento dentro de un multi entorno de subprocesos con funciones asíncronas independientes. • Operadores Disponibles. La biblioteca RxPy viene preconstruida con numerosas funciones adores que hacen mucho más fácil el procesamiento de los datos producidos por un Observable. • Composición de datos. Es sencillo componer nuevos flujos de datos (Observables) a partir de datos suministrados por dos o más otros Observables para asíncrono procesamiento cronológico. 35.8 Desventajas de la programación reactiva Es fácil complicar demasiado las cosas cuando comienza a encadenar operadores. Si usted utilizar demasiados operadores, o un conjunto de funciones demasiado complejo con los operadores, puede volverse difícil de entender lo que está pasando. Muchos desarrolladores piensan que la programación Reactiva es inherentemente de subprocesos múltiples; Este no es necesariamente el caso; de hecho RxPy (la biblioteca explorada en los siguientes dos capítulos) tiene un solo subproceso de forma predeterminada. Si una aplicación necesita el comportamiento para ejecutar de forma asíncrona, entonces es necesario indicarlo explícitamente. Otro problema para algunos marcos de programación reactivos es que puede volverse memoria intensiva para almacenar flujos de datos para que los observadores puedan procesar esos datos cuando estén listos. 35,9 El Marco de Programación Reactiva RxPy La biblioteca RxPy es parte del proyecto ReactiveX más grande y proporciona una implementación de ReactiveX para Python. Se basa en los conceptos de Observables, Observadores, Sujetos y Operadores. En este libro usamos RxPy versión 3. En el próximo capítulo discutiremos Observables, Observadores, Sujetos y sub- cripciones utilizando la biblioteca RxPy. El siguiente capítulo explorará varios RxPy operadores. 35.10 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre la programación reactiva: • https://www.reactivemanifesto.org/ El Manifiesto Reactivo. • http://reactivex.io/ La página de inicio de ReactiveX. • https://en.wikipedia.org/wiki/Design_Patterns Wikipedia página en Diseño Libro de patrones. 426 35 Introducción a la programación reactiva
35.11 Referencia Para obtener más información sobre el patrón de diseño Observer Observable, consulte “Patrones” libro de la banda de los cuatro • E. Gamma, R. Helm, R. Johnson, J. Vlissades, Patrones de diseño: elementos de software orientado a objetos reutilizable, Addison-Wesley (1995). 35.11 Referencia 427
capitulo 36 RxPy Observables, Observadores y Sujetos 36.1 Introducción En este capítulo discutiremos Observables, Observadores y Sujetos. Nosotros también considere cómo los observadores pueden o no correr simultáneamente. En el resto de este capítulo, veremos la versión 3 de RxPy, que es una de las principales actualización de RxPy versión 1 (por lo tanto, deberá tener cuidado si está buscando en la web ejemplos ya que algunos aspectos han cambiado; más notablemente el forma en que se encadenan los operadores). 36.2 Observables en RxPy Un Observable es una clase de Python que publica datos para que puedan ser procesados por uno o más observadores (potencialmente ejecutándose en subprocesos separados). Se puede crear un Observable para publicar datos a partir de datos estáticos o de fuentes dinámicas. Los observables se pueden encadenar más para controlar cómo y cuándo se publican los datos, para transformar los datos antes de que se publiquen y para restringir qué datos se efectivamente publicado. Por ejemplo, para crear un Observable a partir de una lista de valores podemos usar el función rx.from_list(). Esta función (también conocida como operador RxPy) es utilizado para crear el nuevo objeto Observable: importar rx Observable = rx.from_list([2, 3, 5, 7]) © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_36 429
36.3 Observadores en RxPy Podemos agregar un Observador a un Observable usando el método subcribe(). Este método se puede suministrar con una función lambda, una función con nombre o un objeto cuya clase implementa el protocolo Observer. Por ejemplo, la forma más sencilla de crear un observador es usar una función lambda:
Suscribir una función lambda
observable.subscribe(valor lambda: print(‘Lambda recibido’, valor)) Cuando el Observable publique datos, se invocará la función lambda. Cada dato publicado se suministrará de forma independiente a la función. La salida de la suscripción anterior para el Observable anterior es: Lambda Recibido 2 Lambda Recibido 3 Lambda Recibido 5 Lambda Recibido 7 También podemos haber usado una función estándar o con nombre como observador: def prime_number_reporter(valor): print(‘Función Recibida’, valor)
Suscribir una función con nombre
observable.subscribe(principal_numero_reportero) Tenga en cuenta que es solo el nombre de la función que se usa con subscribe() método (ya que esto efectivamente pasa una referencia a la función en el método). Si ahora ejecutamos este código usando el Observable anterior, obtenemos: Función recibida 2 Función Recibida 3 Función Recibida 5 Función recibida 7 De hecho, el método subscribe() toma cuatro parámetros opcionales. estos son: • on_next Acción a invocar para cada elemento de datos generado por el Observable. • on_error Acción para invocar ante la terminación excepcional del Observable secuencia. • on_completed Acción para invocar tras la finalización correcta del Observable secuencia. • Observer El objeto que va a recibir notificaciones. Puedes suscribirte usando un observador o devoluciones de llamada, no ambos. 430 36 RxPy Observables, Observadores y Sujetos
Cada uno de los anteriores se puede utilizar como parámetros posicionales o como argumentos de palabras clave. mentos, por ejemplo:
Use lambdas para configurar las tres funciones
observable.subscribe( en_siguiente = valor lambda: print(‘Recibido en_siguiente’, valor), on_error = lambda exp: print(‘Ocurrió un error’, exp), on_completed = lambda: print(‘Recibido completado notificación’) ) El código anterior define tres funciones lambda que se llamarán dependiendo dependiendo de si los datos son suministrados por el Observable, si ocurre un error o cuando el el flujo de datos se termina. La salida de esto es: Recibido el_próximo 2 Recibido el_próximo 3 Recibido el_próximo 5 Recibido el_próximo 7 Notificación completada recibida Tenga en cuenta que la función on_error no se ejecuta ya que no se generó ningún error en este ejemplo. El último parámetro opcional del método subscribe() es un Observer objeto. Un objeto Observer puede implementar el protocolo Observer que tiene la siguientes métodos on_next(), on_completed() y on_error(), para ejemplo: clase PrimeNumberObserver: def on_next(self, valor): print(‘Objeto recibido’, valor) def on_completed(self): print(‘Flujo de datos completado’) def on_error(self, error): imprimir(‘Ocurrió un error’, error) Las instancias de esta clase ahora se pueden usar como un observador a través de subscribe() método: observable.subscribe(PrimeNumberObserver())
Suscribir un objeto Observer
El resultado de este ejemplo usando el Observable anterior es: Objeto recibido 2 Objeto recibido 3 Objeto recibido 5 Objeto recibido 7 Flujo de datos completado Tenga en cuenta que también se llama al método on_completed(); sin embargo, el El método on_errror() no se llama porque no se generaron excepciones. 36.3 Observadores en RxPy 431
La clase Observer debe asegurarse de que los métodos implementados se adhieran a los Protocolo de observador (es decir, que las firmas de on_next(), on_completed () y on_error() son correctos). 36.4 Múltiples Suscriptores/Observadores Un Observable puede tener varios Observadores suscritos. En este caso cada uno de los Se envía a Observadores todos los datos publicados por el Observable. Múltiples observadores se puede registrar con un Observable llamando al método de suscripción múltiple veces. Por ejemplo, el siguiente programa tiene cuatro suscriptores además de función on_error y on_completed registradas:
Crea un observable usando datos en una lista
observable = rx.from_list([2, 3, 5, 7]) clase PrimeNumberObserver: "”” Una clase de observador "””
def on_next(self, valor):
print('Objeto recibido', valor)
def on_completed(self): print(‘Flujo de datos completado’) def on_error(self, error): imprimir(‘Ocurrió un error’, error) def prime_number_reporter(valor): print(‘Función Recibida’, valor) print(‘Configurar Observadores / Suscriptores’)
Suscribir una función lambda
observable.subscribe(valor lambda: print(‘Lambda recibido’, valor))
Suscribir una función con nombre
observable.subscribe(principal_numero_reportero)
Suscribir un objeto Observer
observable.subscribe(PrimeNumberObserver())
Use lambdas para configurar las tres funciones
observable.subscribe( on_next=valor lambda: print(‘Recibido el_siguiente’, valor), on_error=lambda exp: print(‘Ocurrió un error’, exp), on_completed=lambda: print(‘Recibido completado notificación’) ) 432 36 RxPy Observables, Observadores y Sujetos
La salida de este programa es: Crear el objeto Observable Configurar observadores/suscriptores Lambda Recibido 2 Lambda Recibido 3 Lambda Recibido 5 Lambda Recibido 7 Función recibida 2 Función Recibida 3 Función Recibida 5 Función recibida 7 Objeto recibido 2 Objeto recibido 3 Objeto recibido 5 Objeto recibido 7 Flujo de datos completado Recibido el_próximo 2 Recibido el_próximo 3 Recibido el_próximo 5 Recibido el_próximo 7 Notificación completada recibida Observe cómo a cada uno de los suscriptores se le envían todos los datos antes que al próximo suscriptor se envían sus datos (este es el comportamiento predeterminado de RxPy de subproceso único). 36.5 Materias en RxPy Un sujeto es a la vez un observador y un observable. Esto permite que un sujeto reciba un elemento de datos y luego volver a publicar esos datos o los datos derivados de ellos. Por ejemplo, imagina un sujeto que recibe datos de cotizaciones bursátiles publicados por una fuente externa (a la organización que recibe los datos). Este tema podría agregue una marca de tiempo y una ubicación de origen a los datos antes de volver a publicarlos en otros observadores internos. Sin embargo, hay una diferencia sutil que debe tenerse en cuenta entre un Sujeto y un simple Observable. Una suscripción a un Observable causará un independiente ejecución del Observable cuando se publican los datos. Fíjate cómo en el anterior sección todos los mensajes fueron enviados a un observador específico antes del siguiente observador se envió ningún dato en absoluto. Sin embargo, un Sujeto comparte la acción de publicación con todos los suscriptores y por lo tanto, todos recibirán el mismo elemento de datos en una cadena antes del siguiente dato artículo. En la jerarquía de clases, la clase Sujeto es una subclase directa del Observador. clase. 36.4 Múltiples Suscriptores/Observadores 433
El siguiente ejemplo crea un Asunto que enriquece los datos que recibe al agregar una marca de tiempo a cada elemento de datos. A continuación, vuelve a publicar el elemento de datos en cualquier Los observadores que se han suscrito a ella. importar rx de rx.subjects asunto de importación desde fechahora fechahora de importación fuente = rx.from_list([2, 3, 5, 7]) clase TimeStampSubject (Asunto): def on_next(self, valor): print(‘Asunto recibido’, valor) super().on_next((valor, fechahora.ahora())) def on_completed(self): print(‘Flujo de datos completado’) super().on_completed() def on_error(self, error): print(‘En Asunto - Ocurrió un error’, error) super().on_error(error) def prime_number_reporter(valor): print(‘Función Recibida’, valor) Configuración de impresión’)
Crear el Asunto
asunto = TimeStampSubject()
Configure múltiples suscriptores para el tema
asunto.subscribe(principal_numero_reportero) asunto.subscribe(valor lambda: print(‘Lambda recibido’, valor)) asunto.subscribe( en_siguiente = valor lambda: print(‘Recibido en_siguiente’, valor), on_error = lambda exp: print(‘Ocurrió un error’, exp), on_completed = lambda: print(‘Recibido completado notificación’) )
Suscribir el Sujeto a la fuente Observable
source.subscribe(asunto) imprimir(‘Terminado’) Tenga en cuenta que en el programa anterior, los Observadores se agregan al Sujeto antes el Asunto se agrega a la fuente Observable. Esto asegura que los observadores están suscritos antes de que el Sujeto comience a recibir datos publicados por el 434 36 RxPy Observables, Observadores y Sujetos
Observable. Si el Sujeto estaba suscrito al Observable antes del Los observadores estaban suscritos al Sujeto, entonces todos los datos podrían haber sido publicado antes de que los Observadores se registraran en el Sujeto. La salida de este programa es: Configuración Asunto Recibido 2 Función recibida (2, datetime.datetime(2019, 5, 21, 17, 0, 2, 196372)) Lambda recibido (2, fecha y hora. fecha y hora (2019, 5, 21, 17, 0, 2, 196372)) Recibido el_siguiente (2, fechahora.fechahora(2019, 5, 21, 17, 0, 2, 196372)) Asunto Recibido 3 Función recibida (3, datetime.datetime(2019, 5, 21, 17, 0, 2, 196439)) Lambda recibido (3, fecha y hora. fecha y hora (2019, 5, 21, 17, 0, 2, 196439)) Recibido el_siguiente (3, fechahora.fechahora(2019, 5, 21, 17, 0, 2, 196439)) Asunto Recibido 5 Función recibida (5, datetime.datetime(2019, 5, 21, 17, 0, 2, 196494)) Lambda recibido (5, fecha y hora. fecha y hora (2019, 5, 21, 17, 0, 2, 196494)) Recibido el_siguiente (5, fechahora.fechahora(2019, 5, 21, 17, 0, 2, 196494)) Asunto Recibido 7 Función recibida (7, datetime.datetime(2019, 5, 21, 17, 0, 2, 196548)) Lambda recibido (7, datetime.datetime(2019, 5, 21, 17, 0, 2, 196548)) Recibido el_siguiente (7, datetime.datetime(2019, 5, 21, 17, 0, 2, 196548)) Flujo de datos completado Notificación completada recibida Hecho Como puede verse en esta salida, los números 2, 3, 5 y 7 son recibidos por todos los Observadores una vez que el Sujeto haya agregado la marca de tiempo. 36.6 Concurrencia de observadores Por defecto, RxPy usa un modelo de un solo subproceso; eso es Observables y Observadores ejecutar en el mismo hilo de ejecución. Sin embargo, esto es solo el valor predeterminado, ya que es el enfoque más simple. Es posible indicar que cuando un Observador se suscribe a un Observable que debe ejecutarse en un subproceso separado usando el parámetro de palabra clave del planificador en el 36.5 Materias en RxPy 435
método de suscripción(). A esta palabra clave se le asigna un programador apropiado, como el rx.concurrency.NewThreadScheduler. Este programador asegurará que el Observer ejecuta en un subproceso separado. Para ver la diferencia, observe los siguientes dos programas. La principal diferencia entre los programas es el uso de planificadores específicos: importar rx Observable = rx.from_list([2, 3, 5]) observable.subscribe(lambda v: print(‘Lambda1 Recibido’, v)) observable.subscribe(lambda v: print(‘Lambda2 Recibido’, v)) observable.subscribe(lambda v: print(‘Lambda3 Recibido’, v)) El resultado de esta primera versión se muestra a continuación: Lambda1 Recibido 2 Lambda1 Recibido 3 Lambda1 Recibido 5 Lambda2 Recibido 2 Lambda2 Recibido 3 Lambda2 Recibido 5 Lambda3 Recibido 2 Lambda3 Recibido 3 Lambda3 Recibido 5 El método subscribe() toma un parámetro de palabra clave opcional llamado planificador que permite proporcionar un objeto planificador. Ahora, si especificamos algunos programadores diferentes, veremos que el efecto es ejecutar los observadores simultáneamente con la salida resultante entretejida: importar rx desde rx.concurrency import NewThreadScheduler, ThreadPoolScheduler, ImmediateScheduler Observable = rx.from_list([2, 3, 5]) observable.subscribe(lambda v: print(‘Lambda1 Recibido’, v), planificador=ThreadPoolScheduler(3)) observable.subscribe(lambda v: print(‘Lambda2 Recibido’, v), planificador=ProgramadorInmediato()) observable.subscribe(lambda v: print(‘Lambda3 Recibido’, v), planificador = NewThreadScheduler())
Como el Observable se ejecuta en un subproceso separado, es necesario
asegurar que el hilo principal no termine
input(‘Presiona enter para terminar’) 436 36 RxPy Observables, Observadores y Sujetos
Tenga en cuenta que debemos asegurarnos de que el hilo principal que ejecuta el programa no terminar (ya que todos los Observables ahora se están ejecutando en sus propios hilos) esperando para la entrada del usuario. La salida de esta versión es: Lambda2 Recibido 2 Lambda1 Recibido 2 Lambda2 Recibido 3 Lambda2 Recibido 5 Lambda1 Recibido 3 Lambda1 Recibido 5 Presiona enter para terminar Lambda3 Recibido 2 Lambda3 Recibido 3 Lambda3 Recibido 5 De forma predeterminada, la palabra clave del programador en el método subscribe() tiene como valor predeterminado Ninguno que indica que el subproceso actual se utilizará para la suscripción al Observable. 36.6.1 Programadores disponibles Para admitir diferentes estrategias de programación, la biblioteca RxPy proporciona dos módulos que suministran diferentes planificadores; la concurrencia rx.y rx. moneda.mainloopscheduler. Los módulos contienen una variedad de programas ulers, incluidos los que se enumeran a continuación. Los siguientes programadores están disponibles en el módulo rx.concurrency: • ImmediateScheduler Esto programa una acción para su ejecución inmediata. • CurrentThreadScheduler Esto programa la actividad para el subproceso actual. • TimeoutScheduler Este programador funciona a través de una devolución de llamada cronometrada. • NewThreadScheduler crea un programador para cada unidad de trabajo en un hilo de arata. • ThreadPoolScheduler. Este es un planificador que utiliza un grupo de subprocesos para ejecutar trabajo. Este programador puede actuar como una forma de limitar la cantidad de trabajo llevado a cabo concurrentemente. El módulo rx.concurrency.mainloopschduler también define los siguientes bajando programadores: • IOLoopScheduler Un programador que programa el trabajo a través de Tornado I/O bucle de evento principal. • PyGameScheduler Un programador que programa trabajos para PyGame. • WxScheduler Un programador para un bucle de eventos de wxPython. 36.6 Concurrencia de observadores 437
36.7 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre RxPy: • https://github.com/ReactiveX/RxPY El repositorio del concentrador RxPy Git. • https://rxpy.readthedocs.io/en/latest/ Documentación para la biblioteca RxPy. • https://rxpy.readthedocs.io/en/latest/operators.html Listas de RxPy disponibles operadores. 36.8 Ejercicios Dado el siguiente conjunto de tuplas que representan los precios de acciones/acciones: acciones = ((‘APPL’, 12.45), (‘IBM’, 15.55), (‘MSFT’, 5.66), (‘APLICACIÓN’, 13.33)) Escriba un programa que cree un Observable basado en los datos de las acciones. A continuación, suscriba tres observadores diferentes al Observable. El primero debe imprimir el precio de la acción, el segundo debe imprimir el nombre de la acción y el tercero debe imprimir toda la tupla. 438 36 RxPy Observables, Observadores y Sujetos
capitulo 37 Operadores RxPy 37.1 Introducción En este capítulo veremos los tipos de operadores provistos por RxPy que pueden ser aplicada a los datos emitidos por un Observable. 37.2 Operadores de programación reactiva Detrás de la interacción entre un Observable y un Observer hay un flujo de datos. Es decir, el Observable suministra un flujo de datos a un Observador que consume/ procesos que fluyen. Es posible aplicar un operador a este flujo de datos que puede ser usado para filtrar, transformar y generalmente refinar cómo y cuándo los datos son entregado al observador. Los operadores se definen principalmente en el módulo rx.operators, por ejemplo rx.operadores.promedio(). Sin embargo, es común usar un alias para esto. tal que el módulo de operadores se llame op, como de operadores de importación rx como op Esto permite que se use una forma abreviada cuando se hace referencia a un operador, como como op.average(). Muchos de los operadores RxPy ejecutan una función que se aplica a cada uno de los elementos de datos producidos por un Observable. Otros se pueden utilizar para crear una inicial Observable (de hecho, ya ha visto estos operadores en la forma de operador from_list()). Se puede usar otro conjunto de operadores para generar un resultado. basado en datos producidos por el Observable (como el operador sum()). © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_37 439
De hecho, RxPy proporciona una amplia variedad de operadores y estos operadores pueden ser categorizados de la siguiente manera: • Creacional, • Transformacional, • combinatoria, • Filtros, • Controladores de errores, • Operadores condicionales y booleanos, • Matemáticas, • Conectable. En el resto de esta sección se presentan ejemplos de algunas de estas categorías. 37.3 Operadores de tuberías Para aplicar un operador que no sea un operador creacional a un Observable, es necesario Essary para crear una tubería. Una tubería es esencialmente una serie de una o más operaciones que se puede aplicar al flujo de datos generado por el Observable. El resultado de aplicando la tubería es que se genera un nuevo flujo de datos que representa los resultados producido siguiendo la aplicación de cada operador a su vez. esto se ilustra abajo: Para crear una tubería se utiliza el método Observable.pipe(). Este método toma una lista delimitada por comas de uno o más operadores y devuelve un flujo de datos. Los observadores pueden entonces suscribirse al flujo de datos de la tubería. Esto se puede ver en el ejemplos dados en el resto de este capítulo para transformaciones, filtros, matemática operadores etc 440 37 Operadores RxPy
37.4 Operadores creacionales Ya ha visto un ejemplo de un operador creacional en los ejemplos anteriores. enviado anteriormente en este capítulo. Esto se debe a que el operador rx.from_list() es un ejemplo de un operador creacional. Se utiliza para crear un nuevo Observable basado en datos contenidos en una lista como estructura. Una versión más genérica de from_list() es el operador from_(). Este el operador toma un iterable y genera un Observable basado en los datos provistos por el iterable. Se puede usar cualquier objeto que implemente el protocolo iterable incluidos los tipos definidos por el usuario. También hay un operador from_iterable(). Todo tres operadores hacen lo mismo y puede elegir cuál usar en función de qué proporciona el significado más semántico en su contexto. Las tres declaraciones siguientes tienen el mismo efecto: fuente = rx.from_([2, 3, 5, 7]) fuente = rx.from_iterable([2, 3, 5, 7]) fuente = rx.from_list([2, 3, 5, 7]) Esto se ilustra pictóricamente a continuación: Otro operador creacional es el operador rx.range(). Este operador genera Calcula un observable para un rango de números enteros. El rango se puede especificar con nuestro sin un valor inicial y con o dentro de un incremento. Sin embargo, el maxi- Siempre se debe proporcionar el valor mínimo en el rango, por ejemplo: obs1 = rx.rango(10) obs2 = rx.rango(0, 10) obs3 = rx.rango(0, 10, 1) 37.5 Operadores transformacionales Hay varios operadores transformacionales definidos en rx.operators módulo que incluye rx.operators.map() y rx.operators.flat_map(). El operador rx.operators.map() aplica una función a cada elemento de datos generado por un Observable. 37.4 Operadores creacionales 441
El operador rx.operators.flat_map() también aplica una función a cada elemento de datos, pero luego aplica una operación de aplanamiento al resultado. Por ejemplo, si el resultado es una lista de listas, entonces flat_map aplanará esto en una sola lista. En esta sección nos se centrará en el operador rx.operators.map(). El operador rx.operators.map() permite aplicar una función a todos elementos de datos generados por un Observable. Luego se devuelve el resultado de esta función. como resultado de los operadores map() Observable. La función se suele utilizar para realizar alguna forma de transformación de los datos que se le suministran. Esto podría ser sumando uno a todos los valores enteros, convirtiendo el formato de los datos de XML a JSON, enriqueciendo los datos con información adicional como la hora en que se adquirido y quién proporcionó los datos, etc. En el ejemplo dado a continuación, estamos transformando el conjunto de valores enteros manipulado por el Observable original en cadenas. En el diagrama estas cadenas incluya comillas alrededor de ellos para resaltar que, de hecho, son una cadena: Esto es típico del uso de un operador de transformación; eso es cambiar los datos de un formato a otro o para añadir información a los datos. El código utilizado para implementar este escenario se proporciona a continuación. Tenga en cuenta el uso de la pipe() para aplicar el operador al flujo de datos generado por el Observable:
Aplicar una transformación a una fuente de datos para convertir
números enteros en cadenas
importar rx de operadores de importación rx como op
Configure una fuente con una función de mapa
fuente = rx.from_list([2, 3, 5, 7]).pipe( op.map(valor lambda: “’” + str(valor) + “’”) )
Suscribir una función lambda
source.subscribe(valor lambda: print(‘Lambda recibido’, valor, ’ es una cadena ‘, es instancia (valor, str))) 442 37 Operadores RxPy
La salida de este programa es: Lambda Recibido ‘2’ es una cadena Verdadero Lambda Recibido ‘3’ es una cadena Verdadero Lambda Recibido ‘5’ es una cadena Verdadero Lambda Recibido ‘7’ es una cadena Verdadero 37.6 Operadores combinatorios Los operadores combinatorios combinan varios elementos de datos de alguna manera. Uno ejemplo de un operador combinatorio es el operador rx.merge(). este operador fusiona los datos producidos por dos Observables en un solo flujo de datos Observable. Por ejemplo: En el diagrama anterior, dos Observables están representados por la secuencia 2, 3, 5, 7 y la secuencia 11, 13, 16, 19. Estos Observables se suministran a la combinación operador que genera un único Observable que suministrará los datos generados a partir de ambos de los Observables originales. Este es un ejemplo de un operador que no toma una función pero en su lugar toma dos Observables. El código que representa el escenario anterior se proporciona a continuación:
Un ejemplo que ilustra cómo fusionar dos fuentes de datos
importar rx
Configurar dos fuentes
fuente1 = rx.from_list([2, 3, 5, 7]) fuente2 = rx.from_list([10, 11, 12])
Combinar dos fuentes en una
rx.merge(fuente1, fuente2)
.subscribe(lambda v: print(v, end=’,’))
Fíjate que en este caso nos hemos suscrito directamente al Observable devuelto
por el operador merge() y no han almacenado esto en una variable intermedia (esto
fue una decisión de diseño y cualquier enfoque es aceptable).
37.5
Operadores transformacionales
443
El resultado de este programa se presenta a continuación: 2,3,5,7,10,11,12, Observe en la salida la forma en que los datos se mantienen en los Observables originales está entrelazado en la salida del Observable generado por el operador merge(). 37.7 Operadores de filtrado Hay varios operadores en esta categoría, incluidos rx.operators.filter (), rx.operators.first(), rx.operators.last() y rx.opera- tors.distinct(). El operador filter() solo permite que esos elementos de datos pasen a través de ese pase alguna expresión de prueba definida por la función pasada al filtro. Esta función debe devolver Verdadero o Falso. Cualquier elemento de datos que haga que la función devuelva True se deja pasar a través del filtro. Por ejemplo, supongamos que la función que se pasa a filter() está diseñada para permitir solo el paso de números pares. Si el flujo de datos contiene los números 2, 3, 5, 7, 4, 9 y 8, entonces el filtro() solo emitirá los números 2, 4 y 8. Esto es ilustrado a continuación: El siguiente código implementa el escenario anterior:
Fuente de filtro para números pares
importar rx de operadores de importación rx como op
Configure una fuente con un filtro
fuente = rx.from_list([2, 3, 5, 7, 4, 9, 8]).pipe( op.filter(valor lambda: valor % 2 == 0) )
Suscribir una función lambda
source.subscribe(valor lambda: print(‘Lambda recibido’, valor)) En el código anterior, el operador rx.operators.filter() toma una lambda función que verificará si el valor actual es par o no (tenga en cuenta que esto podría haber sido una función con nombre o un método en un objeto, etc.). Se aplica al flujo de datos. generado por el Observable usando el método pipe(). La salida generada por este ejemplo es: 444 37 Operadores RxPy
Lambda Recibido 2 Lambda Recibido 4 Lambda Recibido 8 Los operadores first() y last() emiten solo el primer y último elemento de datos publicado por el Observable. El operador distint() suprime los elementos duplicados que publica el Observable. Por ejemplo, en la siguiente lista utilizada como datos para el Observable, los números 2 y 3 están duplicados:
Usar distinto para suprimir duplicados
fuente = rx.from_list([2, 3, 5, 2, 4, 3, 2]).pipe( op.distinto() )
Suscribir una función lambda
source.subscribe(valor lambda: print(‘Recibido’, valor)) Sin embargo, cuando el programa genera la salida, todos los duplicados se han eliminado. suprimido: Recibido 2 Recibido 3 Recibido 5 Recibido 4 37.8 Operadores matemáticos Los operadores matemáticos y agregados realizan cálculos en el flujo de datos proporcionada por un Observable. Por ejemplo, rx.operators.average() El operador se puede utilizar para calcular el promedio de un conjunto de números publicados por un Observable. Del mismo modo, rx.operators.max() puede seleccionar el valor máximo, rx.operators.min() el valor mínimo y rx.operators.sum() sumar todos los números publicados etc. Se da un ejemplo usando el operador rx.operators.sum() golpe:
Ejemplo de sumar todos los valores en un flujo de datos
importar rx de operadores de importación rx como op
Configure una fuente y aplique la suma
rx.from_list([2, 3, 5, 7]).pipe( op.suma() ).subscribe(lambda v: imprimir(v)) 37.7 Operadores de filtrado 445
La salida del operador rx.operators.sum() es el total de los datos artículos publicados por el Observable (en este caso el total de 2, 3, 5 y 7). El Función de observador que está suscrita a los operadores rx.operators.sum() Observable imprimirá este valor: Sin embargo, en algunos casos puede ser útil ser notificado del intermediario total acumulado, así como el valor final para que otros operadores en la cadena puedan reaccionar a estos subtotales. Esto se puede lograr usando rx.operators.scan() operador. El operador rx.operators.scan() es en realidad un operador transformacional pero puede usarse en este caso para proporcionar una operación matemática. El El operador scan() aplica una función a cada elemento de datos publicado por un Observable y genera su propio elemento de datos para cada valor recibido. Cada valor generado es pasó a la siguiente invocación de la función scan() y se publicó en el flujo de datos observable de los operadores scan(). Por lo tanto, el total acumulado puede ser generado a partir del subtotal anterior y el nuevo valor obtenido. Esto es mostrado abajo: importar rx de operadores de importación rx como op
Suma móvil o incremental
rx.from_([2, 3, 5, 7]).pipe( op.scan(subtotal lambda, i: subtotal+i) ).subscribe(lambda v: imprimir(v)) La salida de este ejemplo es: 2 5 10 17 Esto significa que cada subtotal se publica así como el total final. 37,9 Operadores de encadenamiento Un aspecto interesante del enfoque RxPy para el procesamiento de flujo de datos es que es Es posible aplicar múltiples operadores al flujo de datos producido por un Observable. Los operadores discutidos anteriormente en realidad devuelven otro Observable. este nuevo Observable puede suministrar su propio flujo de datos basado en el flujo de datos original y el resultado de aplicar el operador. Esto permite aplicar otro operador en secuencia a los datos producidos por el nuevo Observable. Esto permite a los operadores encadenarse para proporcionar un procesamiento sofisticado de los datos publicados por el Observables originales. 446 37 Operadores RxPy
Por ejemplo, primero podríamos comenzar filtrando la salida de un Observable tal que solo se publican ciertos elementos de datos. Entonces podríamos aplicar una transformación mación en forma de un operador map() a esos datos, como se muestra a continuación: Tenga en cuenta el orden en que hemos aplicado los operadores; primero filtramos los datos que no es de interés y luego aplicar la transformación. Esto es más eficiente que aplicar los operadores al revés como en el ejemplo anterior no necesitamos transformar los valores impares. Por lo tanto, es común intentar empujar a los operadores de filtro tan arriba en la cadena como sea posible. El código utilizado para generar el conjunto encadenado de operadores se proporciona a continuación. En esto caso hemos usado funciones lambda para definir la función filter() y el mapa () función. Los operadores se aplican al Observable obtenido de la lista suministrado. El flujo de datos generado por el Observable es procesado por cada uno de los operadores definidos en la tubería. Como ahora hay dos operadores, la tubería contiene ambos operadores y actúa como un conducto por el que fluyen los datos. La lista utilizada como fuente inicial de los datos de Observables contiene una secuencia de evento y números impares. La función filter() selecciona solo números pares y el La función map() transforma los valores enteros en cadenas. Entonces suscribimos un Función de observador al Observable producido por el mapa transformacional () operador.
Ejemplo de encadenamiento de operadores
importar rx de operadores de importación rx como op
Configure una fuente con un filtro
fuente = rx.from_list([2, 3, 5, 7, 4, 9, 8]) tubería = fuente.tubería( op.filter(valor lambda: valor % 2 == 0), op.map(valor lambda: “’” + str(valor) + “’”) )
Suscribir una función lambda
pipe.subscribe(valor lambda: print(‘Recibido’, valor)) 37,9 Operadores de encadenamiento 447
El resultado de esta aplicación se muestra a continuación: Recibido ‘2’ Recibió ‘4’ Recibido ‘8’ Esto deja en claro que solo se permiten los tres números pares (2, 4 y 8) hasta la función map(). 37.10 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre RxPy: • https://rxpy.readthedocs.io/en/latest/ Documentación para la biblioteca RxPy. • https://rxpy.readthedocs.io/en/latest/operators.html Listas de RxPy disponibles operadores. 37.11 Ejercicios Dado el siguiente conjunto de tuplas que representan los precios de acciones/acciones: acciones = ((‘APPL’, 12.45), (‘IBM’, 15.55), (‘MSFT’, 5.66), (‘APLICACIÓN’, 13.33)) Dar solución a lo siguiente: • Seleccione todas las acciones ‘APPL’ • Seleccione todas las acciones con un precio superior a 15,00 • Encuentre el precio promedio de todas las acciones de ‘APPL’. Ahora use el segundo conjunto de tuplas y combínelos con el primer conjunto de acciones precios: existencias2 = ((‘GOOG’, 8,95), (‘APLICAR’, 7,65), (‘APLICAR’, 12,45), (‘MSFT’, 5.66), (‘GOOG’, 7.56), (‘IBM’, 12.76)) Convierta cada tupla en una lista y calcule cuánto participa 25 en esa acción sería, imprima esto como el resultado). • Encuentre las acciones de mayor valor. • Encuentre las acciones de menor valor. • Publicar solo tiempos de datos únicos (es decir, suprimir duplicados). 448 37 Operadores RxPy
Parte IX Programación de red
capitulo 38 Introducción a Sockets y Web Servicios 38.1 Introducción En los siguientes dos capítulos exploraremos los servicios web y basados en socket. aproximaciones a las comunicaciones entre procesos. Estos procesos pueden estar ejecutándose en la misma computadora o diferentes computadoras en la misma red de área local o pueden ser distantes geográficamente. En todos los casos, la información es enviada por un programa que se ejecuta en un proceso a otro programa que se ejecuta en un proceso separado a través de tomas de Internet. Este capítulo presenta los conceptos básicos involucrados en la programación de redes. 38.2 Enchufes Los sockets, o más bien los sockets de Protocolo de Internet (IP), proporcionan una interfaz de programación para la pila de protocolos de red que es administrada por el sistema operativo subyacente. El uso de una API de este tipo significa que el programador se abstrae de la baja detalles de nivel de cómo se intercambian datos entre procesos en (potencialmente) diferentes ordenadores y, en cambio, pueden centrarse en los aspectos de nivel superior de su solución. Hay varios tipos diferentes de enchufe IP disponibles, sin embargo, el enfoque en este libro es sobre Stream Sockets. Un enchufe de corriente utiliza el control de transmisión Protocolo (TCP) para enviar mensajes. Este tipo de socket a menudo se denomina TCP/IP. enchufe. TCP proporciona una transmisión ordenada y confiable de datos a través de la conexión entre dos dispositivos (o hosts). Esto puede ser importante ya que TCP garantiza que para cada mensaje enviado; que cada mensaje no solo llegará al host receptor, sino que los mensajes lleguen en el orden correcto. Una alternativa común al TCP es el Protocolo de datagramas de usuario (o UDP). UDP no proporciona ninguna garantía de entrega (es decir, los mensajes pueden perderse o pueden llegar fuera de servicio). Sin embargo, UDP es un protocolo más simple y puede ser particularmente útil para © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_38 451
sistemas de transmisión, donde múltiples clientes pueden necesitar recibir los datos publicados por un host de servidor (particularmente si la pérdida de datos no es un problema). 38.3 Servicios web Un servicio web es un servicio ofrecido por una computadora host que puede ser invocado por un cliente remoto mediante el Protocolo de transferencia de hipertexto (HTTP). HTTP se puede ejecutar cualquier protocolo de transporte de flujo fiable, aunque normalmente se utiliza sobre TCP/IP. Él fue diseñado originalmente para permitir la transferencia de datos entre un servidor HTTP y un navegador web para que los datos puedan presentarse en una forma legible por humanos a un usuario. Sin embargo, cuando se usa con un servicio web, se usa para respaldar el programa para programar la comunicación entre un cliente y un servidor utilizando datos legibles por máquina formatos. Actualmente, este formato suele ser JSON (notación de objetos de scripts de Java) aunque en el pasado se usaba mucho XML (eXtensible Markup Language). 38.4 Servicios de direccionamiento Cada dispositivo (host) conectado a Internet tiene una identidad única (estamos ignorando redes privadas aquí). Esta identidad única se representa como una dirección IP. Usando una dirección IP, podemos conectar un socket a un host específico en cualquier lugar de Internet. Él Por lo tanto, es posible conectarse a una amplia gama de tipos de dispositivos de esta manera desde impresoras, cajas registradoras, frigoríficos, así como servidores, mainframes y PC, etc. Las direcciones IP tienen un formato común, como 144.124.16.237. Una versión IP 4 dirección es siempre un conjunto de cuatro números separados por puntos. Cada número puede estar en el rango 0–255, por lo que el rango completo de direcciones IP es de 0.0.0.0 a 255.255.255.255. Una dirección IP se puede dividir en dos partes; la parte que indica la red en el que está conectado el host y la identificación del host, por ejemplo: De este modo: • Los elementos de ID de red de la dirección IP identifican la red específica en que el host se encuentra actualmente. • El Host ID es la parte de la dirección IP que especifica un dispositivo específico en la red (como su computadora). 452 38 Introducción a los sockets y los servicios web
En cualquier red dada, puede haber múltiples hosts, cada uno con su propia ID de host. pero con una ID de red compartida. Por ejemplo, en una red doméstica privada puede haber ser: • 192.168.1.1 La computadora portátil de Jasmine. • 192.168.1.2 PC de Adán • 192.168.1.3 Impresora doméstica • 192.168.1.4 Smart TV En muchos sentidos, los elementos de identificación de red y de host de una dirección IP son como el dirección postal de una casa en una calle. La calle puede tener un nombre, por ejemplo Coleridge Avenue y puede haber varias casas en la calle. Cada casa tiene un número único; por lo tanto, 10 Coleridge Avenue se diferencia de manera única de 20 Coleridge Avenue por el número de la casa. En este punto puede que te estés preguntando dónde están las URL que ves en tu web. navegador entran en juego (como www.bbc.co.uk). Estos son nombres textuales que en realidad mapear a una dirección IP. El mapeo es realizado por algo llamado Servidor del sistema de nombres de dominio (o DNS). Un servidor DNS actúa como un servicio de búsqueda para proporcione la dirección IP real para un nombre de URL textual en particular. la presencia de un versión textual en inglés de una dirección de host es porque los humanos son mejores para recordar siendo (con suerte) un nombre significativo en lugar de lo que podría parecer ser un nombre aleatorio secuencia de números. Hay varios sitios web que se pueden usar para ver estas asignaciones (y uno es dado al final de este capítulo). Algunos ejemplos de cómo el nombre textual inglés mapas a una dirección IP se dan a continuación: • www.aber.ac.uk mapas a 144.124.16.237 • www.uwe.ac.uk mapas a 164.11.132.96 • www.bbc.net.uk mapas a 212.58.249.213 • www.gov.uk se asigna a 151.101.188.144 Tenga en cuenta que estas asignaciones eran correctas en el momento de escribir este artículo; pueden cambiar como se pueden proporcionar nuevas entradas a los servidores DNS, lo que hace que se asignar a un host físico diferente. 38.5 servidor local Hay una dirección IP especial que normalmente está disponible en una computadora host y es muy útil para desarrolladores y probadores. Esta es la dirección IP: También se conoce como localhost, que suele ser más fácil de recordar. 127.0.0.1 38.4 Servicios de direccionamiento 453
Localhost (y 127.0.0.1) se usa para referirse a la computadora en la que está actualmente cuando se ejecuta un programa; es decir, es su computadora host local (de ahí el nombre servidor local). Por ejemplo, si inicia un servidor de socket en su computadora local y desea un programa de socket del cliente, ejecutándose en la misma computadora, para conectarse al servidor programa; puede indicarle que lo haga haciendo que se conecte a localhost. Esto es particularmente útil cuando no conoce la dirección IP de su computadora local o porque el código puede ejecutarse en varias computadoras diferentes cada uno de los cuales tendrá su propia dirección IP. Esto es particularmente común si usted están escribiendo código de prueba que usarán los desarrolladores cuando ejecuten sus propias pruebas en diferentes máquinas de desarrollador (host). Usaremos localhost en los próximos dos capítulos como una forma de especificar dónde para buscar un programa de servidor. 38.6 Números de puerto Cada dispositivo/host de Internet normalmente puede admitir múltiples procesos. Por lo tanto es necesarios para asegurar que cada proceso tenga su propio canal de comunicación. A haga esto, cada host tiene disponibles múltiples puertos a los que un programa también puede conectarse. Por ejemplo, el puerto 80 a menudo se reserva para servidores web HTTP, mientras que el puerto 25 es reservado para servidores SMTP. Esto significa que si un cliente quiere conectarse a un HTTP servidor en una computadora en particular, entonces debe especificar el puerto 80, no el puerto 25 en ese host. Se escribe un número de puerto después de la dirección IP del host y se separa del dirección por dos puntos, por ejemplo: • www.aber.ac.uk:80 indica el puerto 80 en la máquina host que normalmente estar ejecutando un servidor HTTP, en este caso para la Universidad de Aberystwyth. • localhost:143 esto indica que desea conectarse al puerto 143 que es normalmente reservado para un servidor IMAP (Protocolo de acceso a mensajes de Internet) en su máquina local. • www.uwe.ac.uk:25 esto indica el puerto 25 en un host que se ejecuta en la Universidad de el oeste de Inglaterra, Bristol. El puerto 25 suele estar reservado para SMTP (Simple Mail Protocolo de transferencia). Los números de puerto en el sistema IP son números de 16 bits en el rango 0–65 536. Generalmente, los números de puerto por debajo de 1024 están reservados para servicios predefinidos (que significa que debe evitar usarlos a menos que desee comunicarse con uno de esos servicios como telnet, correo SMTP, ftp, etc.). Por lo tanto, es típico que elija un número de puerto superior a 1024 al configurar sus servicios ganados. 454 38 Introducción a los sockets y los servicios web
38.7 IPv4 frente a IPv6 Lo que hemos descrito en este capítulo en términos de direcciones IP se basa de hecho en el Protocolo de Internet versión 4 (también conocido como IPv4). Esta versión del Protocolo de Internet fue desarrollado durante la década de 1970 y publicado por el IETF (Internet Engineering Task Force) en septiembre de 1981 (reemplazando una definición anterior publicada en enero 1980). Esta versión del estándar utiliza 32 bits binarios para cada elemento del host dirección (de ahí el rango de 0 a 255 para cada una de las partes de la dirección). Este proporciona un total de 4290 millones de posibles direcciones únicas. Esto parecía un enorme cantidad en 1981 y ciertamente suficiente para lo que se imaginó en ese momento para el Internet. Desde 1981, Internet se ha convertido en la columna vertebral no solo del mundo Web en sí, sino también al concepto de Internet de las Cosas (en el que todos los posibles el dispositivo puede estar conectado a Internet desde su refrigerador, a su calefacción central sistema a su tostadora). Esta explosión potencial en dispositivos direccionables de Internet/ anfitriones llevaron a mediados de 1990 a preocupaciones sobre la posible falta de Internet direcciones usando IPv4. Por lo tanto, el IETF diseñó una nueva versión de Internet Protocolo; Protocolo de Internet versión 6 (o IPv6). Esto fue ratificado como un Internet Estándar en julio de 2017. IPv6 usa una dirección de 128 bits para cada elemento en una dirección de host. También utiliza ocho grupos de números (en lugar de 4) que están separados por dos puntos. Cada grupo de números tiene cuatro dígitos hexadecimales. A continuación se ilustra el aspecto de una dirección IPv6: La aceptación del protocolo IPv6 ha sido más lenta de lo que se esperaba originalmente, esto es en parte porque IPv4 e IPv6 no han sido diseñados para ser interoperables, pero también porque la utilización de las direcciones IPv4 no ha sido tan rápida como muchos temido originalmente (en parte debido al uso de redes privadas). Sin embargo, con el tiempo Es probable que esto cambie a medida que más organizaciones pasen a utilizar IPv6. 38.8 Sockets y servicios web en Python Los próximos dos capítulos discuten cómo se pueden implementar sockets y servicios web en Pitón. El primer capítulo trata sobre los sockets generales y los sockets del servidor HTTP. El segundo capítulo analiza cómo se puede usar la biblioteca Flask para crear aplicaciones web. servicios que se ejecutan sobre HTTP usando sockets TCP/IP. 2001:0DB8:AC10:FE01:EF69:B5ED:DD57:2CLE 38.7 IPv4 frente a IPv6 455
38,9 Recursos en línea Consulte los siguientes recursos en línea para obtener información • https://en.wikipedia.org/wiki/Network_socket Página de Wikipedia sobre sockets. • https://en.wikipedia.org/wiki/Web_service Página de Wikipedia sobre servicios web. • https://codebeautify.org/website-to-ip-address Proporciona asignaciones de URL a Direcciones IP. • https://en.wikipedia.org/wiki/IPv4 Página de Wikipedia sobre IPv4. • https://en.wikipedia.org/wiki/IPv6 Página de Wikipedia sobre IPv6. • https://www.techopedia.com/definition/28503/dns-server Para una introducción a DNS. 456 38 Introducción a los sockets y los servicios web
capitulo 39 Enchufes en Python 39.1 Introducción Un Socket es un punto final en un enlace de comunicación entre procesos separados. En Los sockets de Python son objetos que proporcionan una forma de intercambiar información. entre dos procesos de forma directa e independiente de la plataforma. En este capítulo introduciremos la idea básica de las comunicaciones por socket y luego presenta un servidor de socket simple y una aplicación de cliente. 39.2 Comunicación de zócalo a zócalo Cuando dos procesos a nivel de sistema operativo desean comunicarse, pueden hacerlo a través de enchufes. Cada proceso tiene un socket que está conectado al otro socket. Uno El proceso puede escribir información en el socket, mientras que el segundo proceso puede leer información desde el socket. Asociados con cada conector hay dos flujos, uno para entrada y otro para salida. Así, para pasar información de un proceso a otro, escribes esa información a la secuencia de salida de un objeto de socket y leerlo desde la secuencia de entrada de otro objeto socket (asumiendo que los dos sockets están conectados). Hay varios tipos diferentes de enchufes disponibles, sin embargo, en este capítulo enfóquese en los sockets TCP/IP. Tal socket es un socket orientado a la conexión que proporcionar una garantía de entrega de datos (o notificación de la falta de entrega de los datos). TCP/IP, o el Protocolo de control de transmisión/Protocolo de Internet, es un conjunto de protocolos de comunicación utilizados para interconectar dispositivos de red en Internet o en una intranet privada. TCP/IP en realidad especifica cómo se intercambian los datos entre programas a través de Internet proporcionando comunicaciones de extremo a extremo que identifican cómo se deben dividir los datos en paquetes, direccionar, transmitir, enrutar y recibido en destino. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_39 457
39.3 Configurar una conexión Para configurar la conexión, un proceso debe estar ejecutando un programa que está esperando una conexión mientras que el otro debe intentar conectarse al primer programa. el primero es denominado socket de servidor, mientras que el segundo simplemente como socket. Para que el segundo proceso se conecte al primero (el socket del servidor) debe saber en qué máquina se está ejecutando el primero y a qué puerto está conectado. Por ejemplo, en el diagrama anterior, el socket del servidor se conecta al puerto 8084. En gire el socket del cliente se conecta a la máquina en la que se está ejecutando el servidor y al número de puerto 8084 en esa máquina. No sucede nada hasta que el socket del servidor acepta la conexión. En ese punto el los sockets están conectados y los flujos de socket están vinculados entre sí. Esto significa que el flujo de salida del servidor está conectado al flujo de entrada del socket del Cliente y viceversa. 39.4 Un ejemplo de aplicación de servidor de cliente 39.4.1 La estructura del sistema El diagrama anterior ilustra la estructura básica del sistema que estamos tratando de construir. Habrá un objeto de servidor ejecutándose en una máquina y un objeto de cliente corriendo en otro. El cliente se conectará al servidor usando sockets para para obtener información La aplicación real que se está implementando en este ejemplo es una libreta de direcciones busque la aplicación. Las direcciones de los empleados de una empresa se mantienen en un diccionario. Este diccionario está configurado en el programa del servidor, pero igualmente podría mantenerse en una base de datos, etc. Cuando un cliente se conecta al servidor puede obtener un dirección de la oficina de los empleados. 458 39 Enchufes en Python
39.4.2 Implementación de la aplicación de servidor Primero describiremos la aplicación del servidor. Esta es la aplicación Python pro- gram que atenderá las solicitudes de las aplicaciones cliente. Para ello debe proporcionar una socket del servidor para que los clientes se conecten. Esto se hace vinculando primero un socket de servidor a un puerto en la máquina del servidor. El programa del servidor debe entonces escuchar los mensajes entrantes. conexiones La lista presenta el código fuente del programa Servidor. enchufe de importación def principal():
Configurar nombres y oficinas
direcciones = {‘JUAN’: ‘C45’, ‘DENISE’: ‘C44’, ‘PHOEBE’: ‘D52’, ‘ADÁN’: ‘B23’} imprimir(‘Servidor Iniciando’) print(‘Crear el socket’) calcetín = socket.socket(socket.AF_INET, enchufe.SOCK_STREAM) print(‘Vincular el socket al puerto’) dirección_servidor = (socket.gethostname(), 8084) print(‘Iniciando el’, dirección_servidor) sock.bind(dirección_del_servidor)
especifica el número de conexiones permitidas
print(‘Escuchar conexiones entrantes’) calcetín.escuchar(1) mientras que es cierto: print(‘Esperando conexión’) conexión, dirección_cliente = calcetín.aceptar() 39.4 Un ejemplo de aplicación de servidor de cliente 459
El servidor de la lista anterior configura las direcciones para que contengan un diccionario. de los nombres y direcciones. Luego espera a que un cliente se conecte a él. Esto se hace creando un socket y vinculándolo a un puerto específico (en este caso, el puerto 8084) usando: print(‘Crear el socket’) calcetín = socket.socket(socket.AF_INET, enchufe.SOCK_STREAM) print(‘Vincular el socket al puerto’) dirección_servidor = (socket.gethostname(), 8084) La construcción del objeto socket se analiza con más detalle en el siguiente sección. A continuación, el servidor escucha una conexión de un cliente. Tenga en cuenta que el calcetín. El método listen () toma el valor 1, lo que indica que manejará una conexión a la vez A continuación, se configura un bucle infinito para ejecutar el servidor. Cuando se realiza una conexión de un cliente, tanto la conexión como la dirección del cliente están disponibles. Mientras que hay Si hay datos disponibles del cliente, se leen usando la función recv. Tenga en cuenta que los datos recibido del cliente se supone que es una cadena. Esto se usa luego como una clave para buscar la dirección en el diccionario de direcciones. intentar: print(‘Conexión desde’, dirección_cliente) mientras que es cierto: datos = conexión.recv(1024).decode() imprimir(‘Recibido: ‘, datos) si datos: clave = str(datos).superior() respuesta = direcciones[clave] print(’enviando datos de vuelta al cliente: ‘, respuesta) conexión.sendall( respuesta.codificar()) demás: print(‘No hay más datos de’, dirección_cliente) romper finalmente: conexión.cerrar() si nombre == ‘principal’: principal() 460 39 Enchufes en Python
Una vez que se obtiene la dirección, se puede enviar de vuelta al cliente. En Python 3 es necesario para decodificar () y codificar () el formato de cadena a los datos sin procesar transmitido a través de los flujos de socket. Tenga en cuenta que siempre debe cerrar un socket cuando haya terminado con él. 39.5 Tipos de socket y dominios Cuando creamos la clase de socket anterior, pasamos dos argumentos al socket constructor: zócalo(zócalo.AF_INET, zócalo.SOCK_STREAM) Para entender los dos valores pasados al constructor socket() es necesario entender que los Sockets se caracterizan según dos propiedades; su dominio y su tipo. El dominio de un socket esencialmente define los protocolos de comunicaciones que son Se utiliza para transferir los datos de un proceso a otro. También incorpora cómo se nombran los sockets (para que se pueda hacer referencia a ellos al establecer el comunicación). Hay dos dominios estándar disponibles en los sistemas Unix; estos son AF_UNIX que representa las comunicaciones dentro del sistema, donde los datos se mueven desde el proceso para procesar a través de los búferes de memoria del kernel. AF_INET representa comunicación utilizando el conjunto de protocolos TCP/IP; en qué procesos pueden estar en la misma máquina o en diferentes máquinas. • El tipo de socket indica cómo se transfieren los datos a través del socket. Allá son esencialmente dos opciones aquí: • Datagrama cuyos sockets admiten un modelo basado en mensajes donde no hay conexión está involucrado, y no se garantiza que la comunicación sea confiable. • Conectores de flujo que admiten un modelo de circuito virtual, donde los datos se intercambian como un flujo de bytes y la conexión es confiable. Según el dominio, pueden estar disponibles más tipos de socket, como los que admiten el paso de mensajes en una conexión confiable. 39.6 Implementación de la aplicación cliente La aplicación cliente es esencialmente un programa muy simple que crea un enlace a la aplicación de servidor. Para hacer esto, crea un objeto de socket que se conecta a los servidores. host, y en nuestro caso este socket está conectado al puerto 8084. Una vez que se ha realizado una conexión, el cliente puede enviar el mensaje codificado. cadena al servidor. El servidor luego devolverá una respuesta que el cliente debe descodificar. Luego cierra la conexión. 39.4 Un ejemplo de aplicación de servidor de cliente 461
La implementación del cliente se muestra a continuación: def principal(): imprimir(‘Cliente Iniciando’) print(‘Crear un socket TCP/IP’) calcetín = socket.socket(socket.AF_INET, enchufe.SOCK_STREAM) print(‘Conectar el socket al puerto del servidor’) dirección_servidor = (socket.gethostname(), 8084) print(‘Conectando a: ‘, dirección_servidor) sock.connect(dirección_del_servidor) imprimir(‘Conectado al servidor’) intentar: imprimir(‘Enviar datos’) mensaje = ‘Juan’ print(‘Enviando: ‘, mensaje) calcetín.send(mensaje.codificar()) datos = calcetín.recv(1024).decode() print(‘Recibido del servidor: ‘, datos) finalmente: print(‘Zócalo de cierre’) calcetín.cerrar() si nombre == ‘principal’: principal() enchufe de importación El resultado de los dos programas debe considerarse en conjunto. 462 39 Enchufes en Python
Como puede ver en este diagrama, el servidor espera una conexión desde el cliente. Cuando el cliente se conecta al servidor; el servidor espera recibir datos de el cliente. En este punto, el cliente debe esperar a que se le envíen datos desde el servidor. Luego, el servidor configura los datos de respuesta y los envía de vuelta al cliente. El cliente recibe esto y lo imprime y cierra la conexión. Mientras tanto, el servidor ha estado esperando a ver si hay más datos del cliente; como el cliente cierra la conexión el servidor sabe que el cliente ha terminado y vuelve a esperar para la próxima conexión. 39.7 El módulo de servidor de sockets En el ejemplo anterior, el código del servidor es más complejo que el del cliente; y esto es para un servidor de un solo subproceso; la vida puede volverse mucho más complicada si el servidor está Se espera que sea un servidor de subprocesos múltiples (es decir, un servidor que puede manejar múltiples solicitudes de diferentes clientes al mismo tiempo). Sin embargo, el socket de servidor módulo proporciona a más conveniente, enfoque orientado a objetos para crear un servidor. Se necesita gran parte del código de placa de caldera en tales aplicaciones se define en clases, y el desarrollador solo tiene que proporcionar sus propias clases o reemplazan los métodos para definir la funcionalidad específica requerida. Hay cinco clases de servidor diferentes definidas en el módulo socketserver. • BaseServer es la raíz de la jerarquía de clases del Servidor; no es realmente la intención para ser instanciado y utilizado directamente. En su lugar, se extiende por TCPServer y otras clases. • TCPServer usa sockets TCP/IP para comunicarse y es probablemente el más tipo de servidor de socket de uso común. • UDPServer proporciona acceso a sockets de datagramas. • UnixStreamServer y UnixDatagramServer usar dominio Unix sockets y solo están disponibles en plataformas Unix. La responsabilidad de procesar una solicitud se divide entre una clase de servidor y un clase de controlador de solicitudes. El servidor se ocupa de los problemas de comunicación (escuchar en un socket y un puerto, aceptando conexiones, etc.) y el controlador de solicitudes se ocupa de los problemas de la solicitud (interpretar los datos entrantes, procesarlos, enviar datos de vuelta a el cliente). Esta división de responsabilidades significa que, en muchos casos, simplemente puede usar una de las clases de servidor existentes sin modificaciones y proporciona un solicite la clase de controlador para que funcione. El siguiente ejemplo define un controlador de solicitudes que se conecta al TCPServer cuando se construye. El manejador de solicitudes define un manejo de métodos. dle() que se espera que maneje el procesamiento de la solicitud. 39.6 Implementación de la aplicación cliente 463
Tenga en cuenta que la aplicación cliente anterior no necesita cambiar en absoluto; el servidor los cambios están ocultos para el cliente. Sin embargo, este sigue siendo un servidor de un solo subproceso. Podemos muy simplemente convertirlo en un servidor de subprocesos múltiples (uno que puede manejar múltiples solicitudes al mismo tiempo) por mezclando socketserver.ThreadingMixIn en TCPServer. Esto puede hacerse definiendo una nueva clase que no es más que una clase que se extiende tanto importar servidor de sockets clase MiTCPHandler(socketserver.BaseRequestHandler): """ La clase RequestHandler para el servidor. """ def init(self, solicitud, dirección_cliente, servidor): print(‘Configurar nombres y oficinas’) auto.direcciones = {‘JOHN’: ‘C45’, ‘DENISE’: ‘C44’, ‘PHOEBE’: ‘D52’, ‘ADÁN’: ‘B23’} super().init(solicitud, dirección_cliente, servidor) manejar def (auto): imprimir (‘En el identificador’)
self.request es el socket TCP conectado
al cliente
datos = self.request.recv(1024).decode() print(‘datos recibidos:’, datos) clave = str(datos).superior() respuesta = self.direcciones[clave] imprimir(‘respuesta:’, respuesta)
Enviar el resultado de vuelta al cliente
self.request.sendall(response.encode()) def principal(): imprimir(‘Servidor de inicio’) dirección_servidor = (‘host local’, 8084) print(‘Creando servidor’) servidor = socketserver.TCPServer(dirección_servidor, Mi TCPHhandler) print(‘Activando servidor’) servidor.serve_forever() si nombre == ‘principal’: principal() 464 39 Enchufes en Python
ThreadingMixIn y TCPServer y creando una instancia de esta nueva clase en lugar del TCPServer directamente. Por ejemplo: clase ThreadedEchoServer( socketserver.ThreadingMixIn, socketserver.TCPServer): aprobar def principal(): imprimir(‘Iniciando’) dirección = (’localhost’, 8084) servidor = ThreadedEchoServer(dirección, Mi TCPHhandler) print(‘Activando servidor’) servidor.serve_forever() De hecho, ni siquiera necesita crear su propia clase (como la ThreadedEchoServer) como socketserver.ThreadingTCPServer tiene estado proporcionó como a por defecto mezclando de el Servidor TCP y el Clases ThreadingMixIn. Por lo tanto, podríamos simplemente escribir: def principal(): imprimir(‘Iniciando’) dirección = (’localhost’, 8084) servidor = socketserver.ThreadedEchoServer(dirección, Mi TCPHhandler) print(‘Activando servidor’) servidor.serve_forever() 39.8 Servidor HTTP Además del TCPServer, también tiene disponible un servidor http. Servidor HTTP; esto se puede usar de manera similar al TCPServer, pero es se utiliza para crear servidores que responden al protocolo HTTP utilizado por los navegadores web. En En otras palabras, se puede usar para crear un servidor web muy simple (aunque debería ser señaló que en realidad solo es adecuado para crear servidores web de prueba, ya que solo implementa controles de seguridad muy básicos). Probablemente valga la pena un breve aparte para ilustrar cómo un servidor web y un servidor web el navegador interactúa. El siguiente diagrama ilustra las interacciones básicas: 39.7 El módulo de servidor de sockets 465
En el diagrama anterior, el usuario está usando un navegador (como Chrome, IE o Safari) para acceder a un servidor web. El navegador se está ejecutando en su máquina local (que podría sea una PC, una Mac, una caja de Linux, un iPad, un teléfono inteligente, etc.). Para acceder al servidor web ingresan una dirección URL (Universal Resource Locator) en su navegador. En el ejemplo, esta es la URL www.foo.com. También indica que quieren conectarse al puerto 8080 (en lugar del puerto predeterminado 80 utilizado para conexiones HTTP). La máquina remota (que es la que indica la dirección www.foo.com) recibe esta solicitud y determina qué hacer con ella. Si no hay el puerto de monitoreo del programa 8080 rechazará la solicitud. En nuestro caso tenemos un Programa Python (que en realidad es el programa del servidor web) escuchando ese puerto y se pasa la petición. Luego manejará esta solicitud y generará una respuesta. mensaje que se enviará de vuelta al navegador en la máquina local de los usuarios. El respuesta indicará qué versión del protocolo HTTP admite, si todo salió bien o no (este es el código 200 en el diagrama anterior; es posible que tenga visto el código 404 que indica que no se encontró una página web, etc.). El navegador en la máquina local luego representa los datos como una página web o maneja los datos como apropiado etc Para crear un servidor web Python simple, http.server.HTTPServer puede usarse directamente o puede subclasificarse junto con el servidor de sockets. ThreadingMixIn para crear un servidor web de subprocesos múltiples, por ejemplo: clase ThreadingHTTPServer(ThreadingMixIn, HTTPServer): “““Servidor HTTP multiproceso simple "”” aprobar Desde Python 3.7, el módulo http.server ahora proporciona exactamente esta clase como una instalación incorporada y, por lo tanto, ya no es necesario definirla usted mismo (ver http. servidor.ThreadingHTTPServer). Para manejar solicitudes HTTP, debe implementar una de las solicitudes HTTP métodos como do_GET() o do_POST(). Cada uno de estos mapas a un tipo de Solicitud HTTP, por ejemplo: • do_GET() se asigna a una solicitud HTTP Get que se genera si escribe una web dirección en la barra de URL de un navegador web o • do_POST() se asigna a una solicitud HTTP Post que se usa, por ejemplo, cuando un formulario en una página web se utiliza para enviar datos a un servidor web. El método do_GET(self) o do_POST(self) debe manejar cualquier entrada suministrada con la solicitud y generar las respuestas apropiadas de nuevo a la navegador. Esto significa que debe seguir el protocolo HTTP. 466 39 Enchufes en Python
El siguiente programa corto crea un servidor web simple que generará un mensaje de bienvenida y la hora actual como respuesta a una solicitud GET. hace esto usando el módulo de fecha y hora para crear una marca de tiempo de la fecha y la hora usando la función hoy(). Esto se convierte en una matriz de bytes usando UTF-8 codificación de caracteres (UTF-8 es la forma más utilizada para representar texto dentro de páginas web). Necesitamos una matriz de bytes, ya que eso es lo que ejecutará write() método más adelante. Una vez hecho esto, hay varios elementos de metadatos que deben configurarse para que que el navegador sabe qué datos está a punto de recibir. Estos metadatos se conocen como datos de encabezado y puede incluir el tipo de contenido que se envía y la cantidad de datos (contenido) que se transmite. En nuestro caso muy simple necesitamos decirle que estamos enviándolo en texto sin formato (en lugar del HTML utilizado para describir una página web típica) a través de la información del encabezado ‘Tipo de contenido’. También necesitamos decirle cuántos datos somos. envío utilizando la longitud del contenido. Entonces podemos indicar que hemos terminado definiendo la información del encabezado y ahora están enviando los datos reales. El datos sí mismo es enviado a través de el wfile atributo heredado de el BaseHTTPRequestHandler. De hecho, hay dos atributos relacionados rfile y wfile: • rfile este es un flujo de entrada que le permite leer datos de entrada (que no es siendo utilizado en este ejemplo). • wfile contiene el flujo de salida que se puede usar para escribir (enviar) datos al navegador. Este objeto proporciona un método write() que toma un objeto similar a un byte que se escribe (eventualmente) en el navegador. Se utiliza un método main() para configurar el servidor HTTP que sigue el patrón utilizado para el servidor TCP; sin embargo, el cliente de este servidor será un navegador web. desde http.server import BaseHTTPRequestHandler, ThreadingHTTPServer desde fechahora fechahora de importación clase MyHttpRequestHandler(BaseHTTPRequestHandler): “““Manejador de solicitudes muy simple. Solo admite GET”.”” def do_GET(auto): print(“do_GET() comenzando a procesar solicitud”) welcome_msg = ‘Hola desde el servidor en ’ + str(fechahora.hoy()) byte_msg = bytes(welcome_msg, ‘utf-8’) auto.enviar_respuesta(200) self.send_header(“Content-type”, ’text/plain; charset- utf-8’) 39.8 Servidor HTTP 467
Una vez que el servidor está en funcionamiento, es posible conectarse al servidor mediante un navegador e ingresando una dirección web apropiada en el campo URL de los navegadores. Esto significa que en su navegador (asumiendo que se está ejecutando en la misma máquina que el programa anterior) solo necesita escribir en la barra de URL http://local- host:8080 (esto indica que desea utilizar el protocolo http para conectarse al máquina local en el puerto 8080). Cuando haga esto, debería ver el mensaje de bienvenida con la fecha actual y tiempo: self.end_headers() print(‘do_GET() respondiendo con mensaje’) self.wfile.write(byte_msg) def principal(): imprimir(‘Configurando servidor’) dirección_servidor = (‘host local’, 8080) httpd = ThreadingHTTPServer(dirección_servidor, MiHttpRequestHandler) print(‘Activando servidor HTTP’) httpd.serve_forever() si nombre == ‘principal’: self.send_header(‘Content-length’, str(len(byte_msg))) principal() 468 39 Enchufes en Python
39.9 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • https://docs.python.org/3/howto/sockets.html tutorial sobre programación de sockets en Python. • https://pymotw.com/3/socket/tcp.html el módulo Python de la semana TCP página. • https://pymotw.com/3/socketserver/index.html El módulo Python de la semana página en SocketServer. • https://docs.python.org/3/library/http.server.html HTTP Servidores Pitón documentación. • https://pymotw.com/3/http.server/index.html El módulo Python de la semana página en el módulo http.server. • https://www.redbooks.ibm.com/pubs/pdfs/redbooks/gg243376.pdf un tutorial en PDF- manual de IBM sobre TCP/IP. • http:// ask.pocoo.org/ para obtener más información, el micro marco Flask para web desarrollo. • https://www.djangoproject.com/ proporciona información sobre el marco Django para la creación de aplicaciones web. 39.10 Ejercicios El objetivo de este ejercicio es explorar el trabajo con sockets TCP/IP. Debe crear un servidor TCP que recibirá una cadena de un cliente. A continuación, se debe realizar una comprobación para ver qué información indica la cadena. Las entradas requeridas y admitidas son: • ‘Fecha’ que debería dar como resultado que se devuelva la fecha actual. • ‘Hora’, que debería dar como resultado que se devuelva la hora actual. • ‘Fecha y hora’, que debería dar como resultado la fecha y la hora actuales. devuelto • Cualquier otra cosa debería resultar en que la cadena de entrada se devuelva al cliente en mayúsculas con el mensaje ‘OPCIÓN DESCONOCIDA’: precediendo a la cadena. A continuación, el resultado se envía de vuelta al cliente. A continuación, debe crear un programa cliente para llamar al servidor. El programa cliente puede solicitar la entrada del usuario en forma de cadena y enviar esa cadena al servidor. El resultado devuelto por el servidor debe mostrarse en el cliente antes solicitando al usuario la siguiente entrada. Si el usuario ingresa -1 como entrada, entonces el programa debe terminar. 39.9 Recursos en línea 469
A continuación se proporciona un ejemplo del tipo de salida que el cliente podría generar para ilustrar el objetivo general del ejercicio: Cliente inicial Proporcione una entrada (fecha, hora, fecha y hora o -1 a salida): Fecha Conectado al servidor Enviando datos Recibido del servidor: 2019-02-19 Zócalo de cierre Proporcione una entrada (fecha, hora, fecha y hora o -1 a salida): Hora Conectado al servidor Enviando datos Recibido del servidor: 15:50:40 Zócalo de cierre Proporcione una entrada (fecha, hora, fecha y hora o -1 a salida): fecha y hora Conectado al servidor Enviando datos Recibido del servidor: 2019-02-19 15:50:44.720747 Zócalo de cierre Proporcione una entrada (fecha, hora, fecha y hora o -1 a salida): -1 470 39 Enchufes en Python
capitulo 40 Servicios web en Python 40.1 Introducción Este capítulo analiza los servicios web RESTful implementados con Flask estructura. 40.2 Servicios RESTful REST significa Transferencia de estado representacional y fue un término acuñado por Roy Fielding en su Ph.D. para describir la arquitectura ligera y orientada a los recursos estilo que sustenta la web. Fielding, uno de los principales autores de HTTP, fue buscando una manera de generalizar el funcionamiento de HTTP y la web. el gen- eliminó el suministro de páginas web como una forma de suministro de datos a pedido de un cliente donde el cliente tiene el estado actual de un intercambio. Con base en esta información estatal mación, el cliente solicita el siguiente elemento de datos relevantes y envía toda la información necesarios para identificar la información a suministrar con la solicitud. Por lo tanto, la las solicitudes son independientes y no forman parte de una conversación con estado en curso (por lo tanto transferencia estatal). Cabe señalar que, aunque Fielding pretendía crear una forma de describiendo el patrón de comportamiento dentro de la web, también tenía el ojo puesto en producir servicios basados en la web más ligeros (que los que utilizan Enterprise Frameworks de integración o servicios basados en SOAP). Estos más ligeros basados en HTTP Los servicios web se han vuelto muy populares y ahora se usan ampliamente en muchas áreas. Los sistemas que siguen estos principios se denominan servicios RESTful. Un aspecto clave de un servicio RESTful es que todas las interacciones entre un cliente (ya sea JavaScript ejecutándose en un navegador o en una aplicación independiente) son hecho usando operaciones simples basadas en HTTP. HTTP admite cuatro operaciones, estas son HTTP Get, HTTP Post, HTTP Put y HTTP Delete. Estos se pueden utilizar como © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_40 471
verbos para indicar el tipo de acción que se solicita. Por lo general, estos se utilizan como sigue: • recuperar información (HTTP Get), • crear información (publicación HTTP), • información de actualización (HTTP Put), • borrar información (HTTP Delete). Cabe señalar que REST no es un estándar en la forma en que HTML es un estándar. Más bien es un patrón de diseño que se puede utilizar para crear aplicaciones web. que se pueden invocar sobre HTTP y que dan sentido al uso de Get, Post, Put y Eliminar operaciones HTTP con respecto a un recurso específico (o tipo de datos). La ventaja de utilizar los servicios RESTful como tecnología, en comparación con algunos otros enfoques (como los servicios basados en SOAP que también se pueden invocar a través de HTTP) es que • las implementaciones tienden a ser más simples, • el mantenimiento más fácil, • se ejecutan sobre protocolos HTTP y HTTPS estándar y • no requieren costosas infraestructuras y licencias de uso. Esto significa que hay costos más bajos del lado del servidor y del servidor. Hay poco vendedor o dependencia tecnológica y los clientes no necesitan saber nada sobre el detalles de implementación o tecnologías que se utilizan para crear los servicios. 40.3 Una API RESTful
- Una API RESTful es aquella en la que primero debe determinar los conceptos clave o recursos representados o gestionados.
- Pueden ser libros, productos en una tienda, reservas de habitaciones en hoteles, etc. ejemplo, un servicio relacionado con una librería podría proporcionar información sobre recursos como libros, CDs, DVDs, etc. Dentro de este servicio los libros son solo un tipo de recurso. Ignoraremos los otros recursos, como DVD y CD, etc.
- Basándonos en la idea de un libro como recurso, identificaremos las URL adecuadas para
estos servicios RESTful. Tenga en cuenta que aunque las direcciones URL se utilizan con frecuencia para
describir una página web; ese es solo un tipo de recurso. Por ejemplo, podríamos
desarrollar un recurso como
/bookservice/libro
a partir de esto podríamos desarrollar una API basada en URL, como
/bookservice/libro/
Donde ISBN (el Número Internacional Estándar de Libros) indica un único número que se usará para identificar un libro específico cuyos detalles se devolverán usando esta URL. 472 40 Servicios web en Python
También necesitamos diseñar la representación o formatos que el servicio puede suministrar.
Estos podrían incluir texto sin formato, JSON, XML, etc. Estándares JSON para JavaScript
Notación de objetos y es una forma concisa de describir los datos que se van a transferir de un
servicio que se ejecuta en un servidor a un cliente que se ejecuta en un navegador. Este es el formato que
utilizará en la siguiente sección. Como parte de esto podríamos identificar una serie de operaciones
a ser proporcionado por nuestros servicios en función del tipo de método HTTP utilizado para invocar
nuestro servicio y el contenido de la URL proporcionada. Por ejemplo, para un simple
BookService esto podría ser:
• GET /book/
ser invocado a través de HTTP; sin embargo, esto se ha ampliado para proporcionar más REST como instalaciones. En este capítulo nos centraremos en Flask ya que es uno de los más utilizados. frameworks para servicios RESTful ligeros en Python. 40.5 Matraz Flask es un marco de desarrollo web para Python. Se describe a sí mismo como un micro marco para Python que es algo confuso; hasta el punto en que hay un página dedicada a esto en su sitio web que explica lo que significa y cuál es el Las implicaciones son de esto para Flask. Según Flask, el micro en su descripción se relaciona con su objetivo principal de mantener el núcleo de Flask simple pero extensible. A diferencia de Django no incluye instalaciones destinadas a ayudarlo a integrar su aplicación con una base de datos por ejemplo. En cambio, Flask se enfoca en la funcionalidad principal Requerido de un marco de servicio web y permite que se use la extensión, como y cuando requerido, para funcionalidad adicional. Flask es también una convención sobre el marco de configuración; eso es si sigues el convenciones estándar, entonces no necesitará lidiar con mucha configuración adicional. información de duración (aunque si desea seguir un conjunto diferente de convenciones luego puede proporcionar información de configuración para cambiar los valores predeterminados). Como la mayoria las personas (al menos inicialmente) seguirán estas convenciones, hace que sea muy fácil obtener algo en funcionamiento muy rápidamente. 40.6 Hola mundo en matraz Como es tradicional en todos los lenguajes de programación, comenzaremos con un simple ‘Hola Aplicación de estilo mundial. Esta aplicación nos permitirá crear una web muy sencilla servicio que asigna una URL particular a una función que devolverá datos en formato JSON. Usaremos el formato de datos JSON, ya que se usa mucho en la web. servicios. 40.6.1 Usando JSON estándares JSON para la notación de objetos de JavaScript; es un intercambio de datos ligero formato que también es fácil de leer y escribir para los humanos. Aunque se deriva de un subconjunto del lenguaje de programación JavaScript; de hecho es completamente lenguaje Independiente y muchos lenguajes y marcos ahora son compatibles automáticamente. procesamiento de sus propios formatos hacia y desde JSON. Esto lo hace ideal para Servicios web RESTful. 474 40 Servicios web en Python
JSON en realidad se basa en algunas estructuras básicas: • Una colección de pares de nombre/valor en la que el nombre y el valor están separados comprar dos puntos ‘:’ y cada par puede estar separado por una coma ‘,’. • Una lista ordenada de valores que están entre corchetes (’[]’). Esto hace que sea muy fácil construir estructuras que representen cualquier conjunto de datos, por ejemplo un libro con un ISBN, un título, autor y precio podría estar representado por: { “autor”: “Phoebe Cooke”, “isbn”: 2, “precio”: 12.99, “título”: “Java” } A su vez, una lista de libros se puede representar mediante un conjunto de libros separados por comas. entre corchetes. Por ejemplo: [ {“autor”: “Gryff Smith”,“isbn”: 1, “precio”: 10,99, “título”: “XML”}, {“autor”: “Phoebe Cooke”, “isbn”: 2, “precio”: 12,99, “título”: “Java”} {“autor”: “Jason Procter”, “isbn”: 3, “precio”: 11,55, “título”: “C#”}] 40.6.2 Implementación de un servicio web Flask Hay varios pasos involucrados en la creación de un servicio web Flask, estos son:
- Matraz de importación.
- Inicialice la aplicación Flask.
- Implemente una o más funciones (o métodos) para respaldar los servicios que desea a publicar.
- Proporcionar información de enrutamiento para enrutar desde la URL a una función (o método).
- Inicie la ejecución del servicio web. Veremos estos pasos en el resto de este capítulo. 40.6.3 Un servicio sencillo Ahora crearemos nuestro servicio web hello world. Para hacer esto, primero debemos importar el módulo matraz. En este ejemplo usaremos la clase Flask y jsonify() elementos funcionales del módulo. 40.6 Hola mundo en matraz 475
Luego necesitamos crear el objeto de la aplicación principal, que es una instancia del Clase de matraz: de matraz importar Flask, jsonify aplicación = Frasco (nombre) El argumento pasado al constructor Flask() es el nombre del módulo o paquete de la aplicación. Como este es un ejemplo simple, usaremos el name atributo del módulo que en este caso será ‘main’. En aplicaciones más grandes y complejas, con múltiples paquetes y módulos, entonces usted Es posible que deba elegir un nombre de paquete apropiado. El objeto de la aplicación Flask implementa el WSGI (Web Server Gateway Interfaz) estándar para Python. Esto se especificó originalmente en PEP-333 en 2003 y se actualizó para Python 3 en PEP-3333 publicado en 2010. Proporciona un sencillo convención sobre cómo los servidores web deben manejar las solicitudes a las aplicaciones. el frasco objeto de aplicación es el elemento que puede enrutar una solicitud de una URL a un Python función. 40.6.4 Proporcionar información de enrutamiento Ahora podemos definir la información de enrutamiento para el objeto de la aplicación Flask. Este la información asignará una URL a una función. Cuando esa URL es, por ejemplo, ingresada en el campo URL de un navegador web, el objeto de la aplicación Flask recibirá ese solicitar e invocar la función adecuada. Para proporcionar información de mapeo de rutas, usamos el decorador @app.route en un función o método. Por ejemplo, en el siguiente código, el decorador @app.route mapea la URL /hola a la función bienvenido() para solicitudes HTTP Get: @app.route(’/hola’, métodos=[‘GET’]) definitivamente bienvenido(): volver jsonify({‘msg’: ‘Hola Flask World’}) Hay dos cosas a tener en cuenta acerca de esta definición de función: • El decorador @app.route se usa para especificar declarativamente el enrutamiento información para la función. Esto significa que la URL ‘/hola’ será mapeado a la función bienvenido(). El decorador también especifica el HTTP método que es compatible; en este caso, se admiten solicitudes GET (que es en realidad es el valor predeterminado, por lo que no es necesario incluirlo aquí, pero es útil desde un punto de vista punto de vista de la documentación). 476 40 Servicios web en Python
• Lo segundo es que vamos a devolver nuestros datos en formato JSON; por lo tanto, usamos la función jsonify() y le pasamos un Diccionario de Python estructura con un único par clave/valor. En este caso la clave es ‘msg’ y los datos asociado con esa clave es ‘Hello Flask World’. La función jsonify() convertirá esta estructura de datos de Python en una estructura JSON equivalente. 40.6.5 Ejecutando el Servicio Ahora estamos listos para ejecutar nuestra aplicación. Para ello invocamos el método run() del objeto de la aplicación Flask: aplicación.ejecutar(depurar=Verdadero) Opcionalmente, este método tiene una depuración de parámetros de palabra clave que se puede establecer en Verdadero; si esto se hace, cuando la aplicación se ejecuta alguna información de depuración se genera que le permite ver lo que está sucediendo. Esto puede ser útil en desarrollo, pero normalmente no se usaría en producción. El programa completo se presenta a continuación: de matraz importar Flask, jsonify aplicación = Frasco (nombre) @app.route(’/hola’, métodos=[‘GET’]) definitivamente bienvenido(): volver jsonify({‘msg’: ‘Hola Flask World’}) aplicación.ejecutar(depurar=Verdadero) Cuando se ejecuta este programa, la salida inicial generada es como se muestra a continuación:
- Aplicación Flask para servir “hello_flask_world” (carga diferida)
- Medio ambiente: producción ADVERTENCIA: Este es un servidor de desarrollo. No lo use en un despliegue de producción. Utilice un servidor WSGI de producción en su lugar.
- Modo de depuración: activado
- Ejecutándose en http://127.0.0.1:5000/ (Presione CTRL+C para salir)
- Reiniciar con stat
- ¡El depurador está activo!
- PIN del depurador: 274-630-732 Por supuesto, todavía no vemos ningún resultado de nuestro propio programa. Esto es porque no hemos invocado la función de bienvenida() a través de la URL /hola. 40.6 Hola mundo en matraz 477
40.6.6 Invocación del servicio Utilizaremos un navegador web para acceder al servicio web. Para ello debemos introducir el lleno URL que enrutará la solicitud a nuestra aplicación en ejecución y a la bienvenida () función. La URL en realidad se compone de dos elementos, la primera parte es la máquina en que se está ejecutando la aplicación y el puerto que está utilizando para escuchar las solicitudes. En realidad, esto se incluye en el resultado anterior: mire la línea que comienza con ‘En ejecución’. Esto significa que la URL debe comenzar con http://127.0.0.1:5000. Este indica que la aplicación se está ejecutando en la computadora con la dirección IP 127.0.0.1 y escuchando en el puerto 5000. Por supuesto, también podríamos usar localhost en lugar de 127.0.0.1. El resto de la URL debe proporcionar la información que permitirá Frasco para enrutar desde la computadora y el puerto a las funciones que queremos ejecutar. Por lo tanto, la URL completa es http://127.0.0.1:5000/hello y, por lo tanto, se usa en el navegador web que se muestra a continuación: Como puede ver, el resultado devuelto es el texto que proporcionamos a jsonify() pero ahora en formato JSON simple y se muestra en el navegador web. También debería poder ver en la salida de la consola que se recibió una solicitud por el marco Flask para la solicitud GET asignada a la URL /hello: 127.0.0.1 - - [23/may/2019 11:09:40] “GET /hola HTTP/1.1” 200
Una característica útil de este enfoque es que si realiza un cambio en su programa entonces el marco Flask notará este cambio cuando se ejecute en desarrollo modo y puede reiniciar el servicio web con los cambios de código implementados. Si haces esto verá que la salida le notifica el cambio:
- Cambio detectado en ‘hello_flask_world.py’, recargando
- Reiniciar con stat Esto permite realizar cambios sobre la marcha y su efecto puede ser inmediatamente visto. 478 40 Servicios web en Python
40.6.7 La solución definitiva Podemos arreglar un poco este ejemplo definiendo una función que se puede usar para crear el objeto de la aplicación Flask y asegurándonos de que solo ejecutamos la aplicación si el código se está ejecutando como el módulo principal: desde matraz importar Flask, jsonify, url_for def crear_servicio(): aplicación = Frasco (nombre) @app.route(’/hola’, métodos=[‘GET’]) definitivamente bienvenido(): volver jsonify({‘msg’: ‘Hola Flask World’}) con aplicación.test_request_context(): imprimir (url_para (‘bienvenido’)) devolver la aplicación si nombre == ‘principal’: aplicación = crear_servicio() aplicación.ejecutar(depurar=Verdadero) Una característica que hemos agregado a este programa es el uso de test_re- búsqueda_contexto(). El objeto de contexto de solicitud de prueba devuelto implementa el protocolo de administrador de contexto y, por lo tanto, se puede usar a través de una declaración with; esto es útil para fines de depuración. Se puede usar para verificar la URL utilizada para cualquier función. con la información de enrutamiento especificada. En este caso, la salida de la declaración de impresión es ‘/hola’ ya que esta es la URL definida por el decorador @app.route. 40.7 Recursos en línea Consulte los siguientes recursos en línea para obtener información sobre los temas de este capítulo: • http://www.ics.uci.edu/*fielding/pubs/dissertation/top.htm Roy Fielding’ Doctor. Tesis; si está interesado en el fondo para DESCANSAR, lea esto. • https://wiki.python.org/moin/WebFrameworks para una lista muy extensa de web marcos para Python. • https://www.djangoproject.com/ para obtener información sobre Django. • http://www.web2py.com/ Documentación del marco web de Web2py. • https://cherrypy.org/ Para obtener documentación sobre el marco web CherryPy. • http:// ask.pocoo.org/ Para obtener información y ejemplos sobre el desarrollo web de Flask. marco de opciones. 40.6 Hola mundo en matraz 479
• http:// ask.pocoo.org/docs/1.0/foreword/#what-does-micro-mean Flasks expla- nación de lo que significa micro. • https://www.json.org/ Información sobre JSON. • https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface WSGI Web Estándar de interfaz de puerta de enlace del servidor. • https://curl.haxx.se/ Información sobre la herramienta de línea de comando curl. • https://developer.mozilla.org/en-US/docs/Web/HTTP/Status HTTP Respuesta Códigos de estado. 480 40 Servicios web en Python
capitulo 41 Servicio web de librería 41.1 Creación de un servicio de librería Flask El capítulo anterior ilustró la estructura básica de un servicio web muy simple solicitud. Ahora estamos en condiciones de explorar la creación de un conjunto de servicios web por algo un poco más realista; la aplicación de servicio web de la librería. En este capítulo implementaremos el conjunto de servicios web descritos anteriormente en el capítulo anterior para una librería muy sencilla. Esto significa que definiremos servicios para manejar no solo las solicitudes GET sino también PUT, POST y DELETE solicitudes para la API de la librería RESTful. 41.2 El diseño Antes de ver la implementación de la API RESTful de Bookshop, vamos a considerar qué elementos tenemos para los servicios de servicios. Una pregunta que a menudo causa cierta confusión es cómo se relacionan los servicios web con enfoques de diseño tradicionales como el diseño orientado a objetos. El enfoque adoptado aquí es que la API del servicio web proporciona una forma de implementar una interfaz para funciones, objetos y métodos apropiados utilizados para implementar la aplicación/ modelo de dominio Esto significa que todavía tendremos un conjunto de clases que representarán el Librería y los Libros que se encuentran dentro de la librería. A su vez las funciones implementan Mencionando los servicios de la web accederá a la librería para recuperar, modificar, actualizar y eliminar los libros en poder de la librería. © Springer Nature Suiza AG 2019 J. Hunt, Guía avanzada para la programación de Python 3, Temas de Pregrado en Ciencias de la Computación, https://doi.org/10.1007/978-3-030-25943-3_41 481
El diseño general se muestra a continuación:
Esto muestra que un objeto Libro tendrá un isbn, un título, un autor y un precio
atributo.
A su vez, el objeto Bookshop tendrá un atributo de libros que contendrá cero o
más libros. El atributo de libros en realidad contendrá una Lista como la lista de libros
necesita cambiar dinámicamente a medida que se agregan libros nuevos o libros antiguos
eliminado
La Librería también definirá tres métodos que
• permitir la obtención de un libro a través de su isbn,
• permitir que se agregue un libro a la lista de libros y
• habilitar la eliminación de un libro (según su isbn).
Se proporcionará información de enrutamiento para un conjunto de funciones que invocarán
métodos apropiados en el objeto Bookshop. Las funciones a decorar
@app.route y las asignaciones que se utilizarán se enumeran a continuación:
• get_books() que se asigna a la URL /libro/lista utilizando HTTP Get
solicitud de método.
• get_book(isbn) que se asigna a la URL /libro/
La clase Libro es una clase de tipo Valor simple (es decir, está orientada a datos sin comportamiento propio): Libro de clase: def init(self, isbn, título, autor, precio): self.isbn = isbn self.titulo = titulo self.autor = autor self.precio = precio def str(uno mismo): return self.título + ’ por ’ + self.autor + ’ @ ’ + str(auto.precio) La clase Bookshop contiene una lista de libros y proporciona un conjunto de métodos para acceder a libros, actualizar libros y eliminar libros: Librería de clase: def init(uno mismo, libros): self.books = libros def get(self, isbn): if int(isbn) > len(self.books): abortar(404) volver lista(filtro(lambda b: b.isbn == isbn, self.books))[0] def add_book(self, libro): self.books.append(libro) def delete_book(self, isbn): self.books = list(filter(lambda b: b.isbn != isbn, auto.libros)) En el código anterior, el atributo de libros contiene la lista de libros actualmente disponible. El método get() devuelve un libro dado un ISBN específico. El método add_book() agrega un objeto de libro a la lista de libros. El método delete_book() elimina un libro según su ISBN. La variable global bookshop contiene el objeto Bookshop inicializado con un conjunto de libros predeterminado: 41.3 El modelo de dominio 483
41.4 Codificación de libros en JSON Un problema que tenemos es que, aunque la función jsonify() sabe cómo convertir tipos integrados como cadenas, enteros, listas, diccionarios, etc. en un formato apropiado formato JSON; no sabe cómo hacer esto para tipos personalizados como un Libro. Por lo tanto, necesitamos definir alguna forma de convertir un Libro en un libro apropiado. formato JSON. Una forma en que podríamos hacer esto sería definiendo un método que pueda ser llamado para convertir una instancia de la clase Libro en un formato JSON. Podríamos llamar a esto método to_json(). Por ejemplo: Libro de clase: "”" Representa un libro en la librería""" def init(self, isbn, título, autor, precio): self.isbn = isbn self.titulo = titulo self.autor = autor self.precio = precio def str(uno mismo): return self.título + ’ por ’ + self.autor + ’ @ ’ + str(auto.precio) def to_json(uno mismo): devolver { ‘isbn’: self.isbn, ’título’: auto.título, ‘autor’: auto.autor, ‘precio’: self.price } Ahora podríamos usar esto con la función jsonify() para convertir un libro en el Formato JSON: jsonify({’libro’: libro.to_json()}) librería = librería( [Libro (1, ‘XML’, ‘Gryff Smith’, 10.99), Libro (2, ‘Java’, ‘Phoebe Cooke’, 12.99), Libro (3, ‘Scala’, ‘Adam Davies’, 11.99), Libro (4, ‘Python’, ‘Jasmine Byrne’, 15.99)]) 484 41 Servicio web de librería
Este enfoque sin duda funciona y proporciona una forma muy ligera de convertir un libro en JSON. Sin embargo, el enfoque presentado anteriormente significa que cada vez que queremos jsonify un libro debemos recordar llamar al método to_json(). En algunos casos esto significa que también tendremos que escribir un código ligeramente complicado. Para ejemplo, si deseamos devolver una lista de libros de la librería como una lista JSON, podría escribir: jsonify({’libros’: [b.to_json() para b en librería.libros]}) Aquí hemos utilizado una lista de comprensión para generar una lista que contiene el JSON versiones de los libros que se encuentran en la librería. Esto empieza a parecer demasiado complejo, fácil de olvidar y probablemente un error. propenso. Flask mismo usa codificadores para codificar tipos en JSON. El matraz proporciona una forma de creando sus propios codificadores que se pueden usar para convertir un tipo personalizado, como el Clase de libro, en JSON. Tal codificador puede ser utilizado automáticamente por el jso- función nificar(). Para ello debemos implementar una clase codificadora; la clase extenderá el matraz. superclase json.JSONEncoder. La clase debe definir un método predeterminado (self, obj). Este método toma un objeto y devuelve la representación JSON de ese objeto. Por lo tanto, podemos escribir un codificador para la clase Libro de la siguiente manera: El método predeterminado () en esta clase verifica que el objeto que se le pasa sea un instancia de la clase Libro y, si lo es, creará una versión JSON de la Libro. Esta estructura JSON se basa en los atributos isbn, title, author y price. Si no es una instancia de la clase Libro, luego pasa el objeto al padre clase. Ahora podemos registrar este codificador con el objeto de la aplicación Flask para que se utilizará siempre que un Libro deba convertirse a JSON. Esto se hace asignando el codificador personalizado al objeto de la aplicación Flask a través de app.json_encoder atributo: clase LibroJSONEncoder(JSONEncoder): def por defecto(self, obj): si es una instancia (obj, Libro): devolver { ‘isbn’: obj.isbn, ’título’: obj.título, ‘autor’: obj.autor, ‘precio’: obj.precio } demás: devuelve super(BookJSONEncoder, self).default(obj) 41.4 Codificación de libros en JSON 485
Ahora, si deseamos codificar un solo libro o una lista de libros, el codificador anterior
ser utilizado automáticamente y por lo tanto no necesitamos hacer nada más. Así nuestro anterior
los ejemplos se pueden escribir simplemente haciendo referencia al libro o bookshop.books
atributo:
jsonify({’libro’: libro})
jsonify({’libros’: librería.libros})
41.5
Configuración de los servicios GET
Ahora podemos configurar los dos servicios que admitirán solicitudes GET, estos son los
•
Servicios /book/list y /book
En el ejemplo anterior también hemos indicado (opcionalmente) el tipo de parámetro. Por defecto, el tipo será una cadena; sin embargo, sabemos que el ISBN es de hecho, un número entero, por lo que hemos indicado que anteponiendo el nombre del parámetro con el tipo int (y separó la información del tipo del nombre del parámetro por un colon ‘:’). En realidad, hay varias opciones disponibles, incluyendo • cadena (el valor predeterminado), • int (como se usa arriba), • float para valores positivos de punto flotante, • uuid para cadenas uuid y • ruta a la que no le gustan las cadenas pero acepta barras. Podemos volver a usar un navegador para ver los resultados de llamar a estos servicios; esta vez las URL serán • http://127.0.0.1:5000/book/list y • http://127.0.0.1:5000/libro/1 Por ejemplo: Como puede ver en esto, la información del libro se devuelve como un conjunto de clave/valor pares en formato JSON. 41.5 Configuración de los servicios GET 487
41.6 Eliminación de un libro El servicio web eliminar un libro es muy similar al servicio obtener un libro en el sentido de que toma un isbn como parámetro de ruta de URL. Sin embargo, en este caso simplemente devuelve un confirmación de que el libro se eliminó con éxito: @app.route(’/libro/int:isbn’, métodos=[‘ELIMINAR’]) def borrar_libro(isbn): librería.delete_book(isbn) devolver jsonify({‘resultado’: Verdadero}) Sin embargo, ya no podemos probar esto simplemente usando un navegador web. Esto es porque el navegador web utiliza el método de solicitud HTTP Get para todas las URL ingresadas en el campo URL. Sin embargo, el servicio web de eliminación está asociado con HTTP Delete método de solicitud. Para invocar la función delete_book(), por lo tanto, debemos asegurarnos de que el solicitud que se envía utiliza el método de solicitud DELETE. Esto se puede hacer desde un cliente que puede indicar el tipo de método de solicitud que se está utilizando. Los ejemplos podrían incluir otro programa de Python, un sitio web de JavaScript, etc. Sin embargo, para fines de prueba, utilizaremos el programa curl. este programa es disponible en la mayoría de los sistemas Linux y Mac y se puede instalar fácilmente, si no es ya disponible, en otros sistemas operativos. curl es una herramienta de línea de comandos y una biblioteca que se puede usar para enviar y Recibir datos a través de Internet. Soporta una amplia gama de protocolos y estándares. y, en particular, admite los protocolos HTTP y HTTPS y se puede utilizar para enviar y recibir datos a través de HTTP/S utilizando diferentes métodos de solicitud. Por ejemplo, para invocar la función delete_book() usando /book/2 URL y el método HTTP Delete podemos usar curl de la siguiente manera: curl http://localhost:5000/book/2 -X ELIMINAR Esto indica que queremos invocar la URL (http://localhost:5000/book/2) y que deseamos utilizar un método de solicitud personalizado (es decir, no el GET predeterminado) que es en el caso DELETE (como lo indica la opción −X). El resultado devuelto por el El comando se proporciona a continuación para indicar que el libro se eliminó con éxito. { “resultado”: verdadero } Podemos verificar esto comprobando el resultado de la URL /libro/lista en el navegador web: 488 41 Servicio web de librería
Esto confirma que el libro 2 ha sido eliminado. 41.7 Agregar un nuevo libro También queremos apoyar la adición de un nuevo libro a la Librería. Los detalles de un nuevo el libro podría simplemente agregarse a la URL como parámetros de ruta de URL; sin embargo como el la cantidad de datos que se agregará crece, esto sería cada vez más difícil de mantener y verificar. De hecho, aunque históricamente había un límite de 2083 car- actores en Internet Explore (IE) de Microsoft, que en teoría se ha eliminado desde IE8, en la práctica, normalmente todavía hay límites en el tamaño de la URL. La mayoría de la web los servidores tienen un límite de 8 KB (o 8192 bytes), aunque esto suele ser configurable. También puede haber límites del lado del cliente (como los impuestos por IE o Safari de Apple (que suelen tener un límite de 2 KB). Si se supera el límite en un navegador o en el servidor, entonces la mayoría de los sistemas simplemente truncarán los caracteres fuera del límite (en algunos casos sin previo aviso). Normalmente, estos datos se envían en el cuerpo de la solicitud HTTP como parte de un Solicitud de publicación HTTP. Este límite en el mismo cuerpo del mensaje de una solicitud de publicación es mucho superior (normalmente hasta 2 GB). Esto significa que es mucho más fiable y seguro. manera de transferir datos a un servicio web. Sin embargo, cabe señalar que esto no significa que los datos sean más seguro que si es parte de la URL; solo que se envía de otra forma. Desde el punto de vista de las funciones de Python que se invocan como resultado de una Solicitud del método HTTP Post significa que los datos no están disponibles como parámetro para 41.6 Eliminación de un libro 489
la URL y por lo tanto a la función. En cambio, dentro de la función es necesario obtener el objeto de la solicitud y luego usarlo para obtener la información contenida dentro el cuerpo de la solicitud. Un atributo clave en el objeto de solicitud, disponible cuando una solicitud HTTP contiene Datos JSON, es el atributo request.json. Este atributo contiene un diccionario como estructura que contiene los valores asociados con las claves en los datos JSON estructura. Esto se muestra a continuación para la función create_book(). desde solicitud de importación de matraz, abortar @app.route(’/libro’, métodos=[‘POST’]) def crear_libro(): imprimir(‘crear libro’) si no es request.json o no ‘isbn’ en request.json: abortar(400) libro = Libro(solicitud.json[‘isbn’], solicitud.json[’título’], request.json.get(‘autor’, “”), float(solicitud.json[‘precio’])) librería.add_book(libro) volver jsonify({’libro’: libro}), 201 La función anterior accede al objeto flask.request que representa el solicitud HTTP actual. La función primero verifica que contenga datos JSON y que el ISBN del libro a agregar, es parte de esa estructura JSON. Si es el ISBN no entonces se llama a la función flask.abort() pasando un HTTP adecuado código de estado de respuesta. En este caso, el código de error indica que se trata de un mal Solicitud (Código de error HTTP 400). Sin embargo, si los datos JSON están presentes y contienen un número ISBN, entonces el Se obtienen valores para las claves isbn, título, autor y precio. Recordar que JSON es una estructura similar a un diccionario de claves y valores, tratándolo de esta manera way facilita la extracción de los datos que contiene una estructura JSON. También significa que podemos usar estilos de acceso orientados tanto a métodos como a claves. Esto se muestra arriba donde usamos el método get() junto con un valor predeterminado para usar, si un autor no es especificado. Finalmente, como queremos tratar el precio como un número de punto flotante, debemos usar el función float() para convertir el formato de cadena proporcionado por JSON en un float. Usando los datos extraídos podemos instanciar una nueva instancia de Libro que puede ser añadido a la librería. Como es común en los servicios web, estamos devolviendo los nuevos objeto de libro creado como resultado de crear el libro junto con la respuesta HTTP código de estado 201, que indica la creación exitosa de un recurso. 490 41 Servicio web de librería
Ahora podemos probar este servicio usando el programa de línea de comando curl: curl -H “Tipo de contenido: aplicación/json” -X POST -d ‘{“title”:“Leer un libro”, “author”:“Bob”,“isbn”:“5”, “precio”:“3.44”}’ http://localhost:5000/libro Las opciones utilizadas con este comando indican el tipo de datos que se envían en el cuerpo de la solicitud (-H) junto con los datos a incluir en el cuerpo de la solicitud (- d). El resultado de ejecutar este comando es: { “libro”: { “autor”: “Bob”, “isbn”: “5”, “precio”: 3.44, “title”: “Leer un libro” } } Ilustrando que se ha añadido el nuevo libro de Bob. 41.8 Actualización de un libro Actualizar un libro que ya tiene el objeto librería es muy similar a agregar un libro, excepto que se utiliza el método de solicitud HTTP Put. Nuevamente, la función que implementa el comportamiento requerido debe usar el matraz. objeto de solicitud para acceder a los datos enviados junto con la solicitud PUT. Sin embargo, en este caso, el número ISBN especificado se utiliza para encontrar el libro actualizado, en lugar de especificar un libro completamente nuevo. La función update_book() se proporciona a continuación: @app.route(’/libro’, métodos=[‘PUT’]) def actualizar_libro(): si no es request.json o no ‘isbn’ en request.json: abortar(400) isbn = solicitud.json[‘isbn’] libro = librería.get(isbn) libro.título = solicitud.json[’título’] libro.autor = solicitud.json[‘autor’] libro.precio = solicitud.json[‘precio’] volver jsonify({’libro’: libro}), 201 41.7 Agregar un nuevo libro 491
Esta función restablece el título, el autor y el precio del libro recuperado de la librería. Devuelve de nuevo el libro actualizado como resultado de ejecutar el función. El programa curl se puede usar nuevamente para invocar esta función, aunque esta vez se debe especificar el método HTTP Put: curl -H “Tipo de contenido: aplicación/json” -X PUT -d ‘{“title”:“Leer un libro de Python”, “author”:“Bob Jones”,“isbn”:“5”, “precio”:“3.44”}’ http://localhost:5000/libro La salida de este comando es: { “libro”: { “autor”: “Bob Jones”, “isbn”: “5”, “precio”: “3.44”, “title”: “Leer un libro de Python” } } Esto muestra que el libro 5 ha sido actualizado con la nueva información. 41,9 ¿Qué pasa si nos equivocamos? El código presentado para los servicios web de la librería no es especialmente defensivo, ya que es posible intentar agregar un nuevo libro con el mismo ISBN que uno existente. Sin embargo, comprueba que se haya proporcionado un número ISBN tanto con el funciones create_book() y update_book(). Sin embargo, ¿qué sucede si no se proporciona un número ISBN? En ambas funciones llamamos a la función flask.abort(). Por defecto si esto sucede un mensaje de error será devuelto al cliente. Por ejemplo, en el siguiente comando nos hemos olvidado de incluir el ISBN número: curl -H “Tipo de contenido: aplicación/json” -X POST -d ‘{“título”:“Leer un libro”, “autor”:“Tom Andrews”, “precio”:“13.24”}’ http://localhost:5000/libro 492 41 Servicio web de librería
Esto genera la siguiente salida de error:
Solicitud incorrecta
El navegador (o proxy) envió una solicitud para que este servidor pudiera no entiendo.
Lo extraño aquí es que la salida de error está en formato HTML, que no es lo que podríamos haber esperado ya que estamos creando un servicio web y trabajando con JSON. El problema es que Flask tiene por defecto generar una página web HTML de error que espera ser renderizado en un navegador web. Podemos superar esto definiendo nuestra propia función personalizada de manejo de errores. Este es una función que está decorada con un decorador @app.errorhandler() que proporciona el código de estado de respuesta que maneja. Por ejemplo: @app.controlador de errores(400) def not_found(error): devuelve make_response(jsonify({'libro': 'No encontrado'}), 400) Ahora, cuando se genera un código 400 a través de la función flask.abort(), el Se invocará la función not_found() y se generará una respuesta JSON con la información proporcionada por la función flask.make_response(). Para ejemplo: curl -H "Tipo de contenido: aplicación/json" -X POST -d '{"título":"Leer un libro", "autor":"Tom Andrews", "precio":"13.24"}' http://localhost:5000/libro La salida de este comando es: { "libro": "No encontrado" } 41,9 ¿Qué pasa si nos equivocamos? 49341.10 Listado de servicios de librería La lista completa de la aplicación de servicios web de la librería se proporciona a continuación: desde matraz importar Flask, jsonify, solicitud, abortar, make_response desde matraz.json importar JSONEncoder Libro de clase: def init(self, isbn, título, autor, precio): self.isbn = isbn self.titulo = titulo self.autor = autor self.precio = precio def str(uno mismo): return self.título + ’ por ’ + self.autor + ’ @ ’ + str(auto.precio) clase LibroJSONEncoder(JSONEncoder): def por defecto(self, obj): si es una instancia (obj, Libro): devolver { ‘isbn’: obj.isbn, ’título’: obj.título, ‘autor’: obj.autor, ‘precio’: obj.precio } demás: devuelve super(BookJSONEncoder, self).default(obj) 494 41 Servicio web de librería
Librería de clase: def init(uno mismo, libros): self.books = libros def get(self, isbn): if int(isbn) > len(self.books): abortar(404) volver lista(filtro(lambda b: b.isbn == isbn, self.books))[0] def add_book(self, libro): self.books.append(libro) def delete_book(self, isbn): self.books = list(filter(lambda b: b.isbn != isbn, auto.libros)) librería = Librería([Libro(1, ‘XML’, ‘Gryff Smith’, 10.99), Libro (2, ‘Java’, ‘Phoebe Cooke’, 12.99), Libro (3, ‘Scala’, ‘Adam Davies’, 11.99), Libro (4, ‘Python’, ‘Jasmine Byrne’, 15.99)]) def create_bookshop_service(): aplicación = Frasco (nombre) app.json_encoder=BookJSONEncoder @app.route(’/libro/lista’, métodos=[‘GET’]) def obtener_libros(): volver jsonify({’libros’: librería.libros}) @app.route(’/libro/int:isbn’, métodos=[‘GET’]) def get_book(isbn): libro = librería.get(isbn) volver jsonify({’libro’: libro}) 41.10 Listado de servicios de librería 495
@app.route(’/libro’, métodos=[‘POST’]) def crear_libro(): imprimir(‘crear libro’) si no es request.json o no ‘isbn’ en request.json: abortar(400) libro = Libro(solicitud.json[‘isbn’], solicitud.json[’título’], request.json.get(‘autor’, “”), float(solicitud.json[‘precio’])) librería.add_book(libro) volver jsonify({’libro’: libro}), 201 @app.route(’/libro’, métodos=[‘PUT’]) def actualizar_libro(): si no es request.json o no ‘isbn’ en request.json: abortar(400) isbn = solicitud.json[‘isbn’] libro = librería.get(isbn) libro.título = solicitud.json[’título’] libro.autor = solicitud.json[‘autor’] libro.precio = solicitud.json[‘precio’] volver jsonify({’libro’: libro}), 201 @app.route(’/libro/int:isbn’, métodos=[‘ELIMINAR’]) def borrar_libro(isbn): librería.delete_book(isbn) devolver jsonify({‘resultado’: Verdadero}) @app.controlador de errores(400) def not_found(error): devuelve make_response(jsonify({’libro’: ‘No encontrado’}), 400) devolver la aplicación si nombre == ‘principal’: aplicación = create_bookshop_service() aplicación.ejecutar(depurar=Verdadero) 496 41 Servicio web de librería
41.11
Ejercicios
Los ejercicios de este capítulo involucran la creación de un servicio web que proporcionará
información sobre los precios del mercado de valores.
Los servicios a implementar son:
Obtener método:
• /stock/list esto devolverá una lista de las acciones que se pueden consultar por su
precio.
• /stock/ticker devolverá el precio actual de la acción indicado por
teletipo, por ejemplo/stock/APPL o/stock/MSFT.
Método POST:
• /stock con el cuerpo de solicitud que contiene JSON para un nuevo tablero de cotizaciones y
precio, por ejemplo {‘IBM’: 12,55}.
Método PUT:
• /stock con el cuerpo de la solicitud que contiene JSON para un tablero de cotizaciones existente y
precio.
método ELIMINAR
• /stock/
popular post
Temas de Pregrado en Ciencias de la Computación Guía avanzada a Python 3 Programación Juan caza
Read MoreTemas de Pregrado en Ciencias de la Computación Guía avanzada a Python 3 Programación Juan caza
Read More• https://docs.python.org/3/ El sitio principal de documentación de Python 3. Contiene tutoriales, referencias de bibliotecas, guías de configuración e instalación, así como Python cómo-tos • https://docs.
Read More