0% encontró este documento útil (0 votos)
76 vistas187 páginas

Python Proyect Libro

Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
76 vistas187 páginas

Python Proyect Libro

Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd

Machine Translated by Google

libros

Python 3 para ciencia y


Aplicaciones de ingeniería
Aprenda a usar Python de manera productiva en escenarios
de la vida real en el trabajo y en la vida cotidiana.

Félix Bittmann
Machine Translated by
Google

Python 3 para ciencia y


Aplicaciones de ingeniería


Félix Bittmann

una publicación de Elektor

APRENDE DISEÑO COMPARTE


Machine Translated by
Google

• Esta es una publicación de Elektor. Elektor es la marca mediática de


Elektor International Media BV

Calle York 78

Londres W1H 1DP, Reino Unido

Teléfono: (+44) (0)20 7692 8344

© Elektor International Media BV 2020

Publicado por primera vez en el Reino Unido en 2020.

• Todos los derechos reservados. Ninguna parte de este libro puede reproducirse en ninguna forma material, incluyendo

fotocopiar, o almacenar en cualquier soporte por medios electrónicos y ya sea de forma transitoria o incidental

a algún otro uso de esta publicación, sin el permiso por escrito del titular de los derechos de autor, excepto en

de conformidad con las disposiciones de la Ley de derechos de autor, diseños y patentes de 1988 o según los términos de un

licencia emitida por Copyright Licensing Agency Ltd, 90 Tottenham Court Road, Londres, Inglaterra W1P 9HE.

Las solicitudes de permiso por escrito del titular de los derechos de autor para reproducir cualquier parte de esta publicación deben ser

dirigido a los editores. Los editores han hecho todo lo posible para garantizar la exactitud de la

información contenida en este libro. No asumen, y por la presente renuncian, cualquier responsabilidad ante ninguna de las partes por

cualquier pérdida o daño causado por errores u omisiones en este libro, ya sea que dichos errores u omisiones resulten de

negligencia, accidente o cualquier otra causa.

• Catalogación de la Biblioteca Británica en datos de publicaciones

El registro de catálogo de este libro está disponible en la Biblioteca Británica.

• ISBN 9783895763991

• LIBRO ELECTRÓNICO 9783895764004

• EPUB 9783895764011

Producción de preimpresión: DMC ¦ [Link]

Impreso en los Países Bajos por Wilco

Elektor es parte de EIM, la fuente líder mundial de información técnica esencial y productos electrónicos para ingenieros profesionales,
diseñadores electrónicos y las empresas que buscan involucrarlos. Cada día, nuestro equipo internacional desarrolla y ofrece contenido
de alta calidad, a través de una variedad de canales de medios (por ejemplo, revistas, videos, medios digitales y redes sociales) en
varios idiomas, relacionados con el diseño electrónico y la electrónica de bricolaje. [Link]

APRENDE DISEÑO COMPARTE


Machine Translated by
Google
Tabla de contenido

Tabla de contenido
• Introducción................................................................................................................................................................................................................. 6

Capítulo 1 • Conceptos básicos..................................................................................................................................................................................... 8

1.1 • Entorno de Instalación y Programación. 1.2 • Python básico. 1.3


• ..........................8

Principios de una
buena ..............................................8

programación. 1.4 • Habilidades para resolver


problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

Capítulo 2 • Trabajar con números .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

2.1 • Fibonacci. 2.2 •


. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

Números primos. 2.3 • Colátz.


. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

2.4 •Pi.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2.5 • Cuenta atrás. 2.6 • . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

Espiral de Ulam. 2.7 • Caos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

total. 2.8 • Tres puntos . 2.9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

• Muy juntos. 2.10 • . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55


Retroceso.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.11 • Integración Numérica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

Capítulo 3 • Estadísticas y Simulaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

3.1 • Prueba de . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
velocidad.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.2 • Pi (nuevamente).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.3 • Paralelización.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.4 • Paseo aleatorio.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.5 • Juego de la Vida.

3.6 • Modelado de poblaciones. 3.7 • . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96


Dinero

Rápido. 3.8 • Muchos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102


círculos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
3.9 • Cerdo. 3.10 • Arranque.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

Capítulo 4 • Datos de texto y cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132


.

4.1 • Diccionario. 4.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132



LPS .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
4.3 • ECL.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

4.4 • Cifrado. 4.5 • Números.......................................................................................................................................................................... 141


romanos. 4.6 • Aritmética de coincidencias.
Machine Translated by
4.7 • Superpalíndromos. 4.8 • 2048
Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158

4.9 • Los próximos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164


pasos.

•5
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

• Introducción

¿Por qué Python?

No en vano Python se ha convertido en uno de los lenguajes de programación más populares del
mundo. Una sintaxis intuitiva y fácil de usar, una comunidad grande y motivada, junto con una multitud
de módulos y bibliotecas de programas, que permiten una implementación rápida y eficiente de cualquier
idea de proyecto, inspiran tanto a principiantes como a expertos. Por lo tanto, Python es un primer paso
ideal hacia la programación, pero también se recomienda para los veteranos que deseen afianzarse en
el ámbito de las ciencias de datos.

Este libro está escrito para lectores que ya tienen experiencia básica con Python, digamos después de
completar un primer tutorial, y ahora quieren aprender cómo aplicar Python de manera productiva y con
un enfoque en aplicaciones en entornos del mundo real. Por lo tanto, este no es un libro de texto clásico
que procesa todos los aspectos del lenguaje de manera lineal, sino que comienza con tareas y acertijos
muy concretos que quieren resolverse. Estos se han tomado de una gran cantidad de campos diferentes
para enfatizar que Python se puede aplicar en muchos contextos. En cada ejemplo, primero veremos
las ideas o tácticas generales sobre cómo resolver el problema y cuándo se pueden implementar con
trucos y ajustes especiales de Python.

Requisitos

Debe conocer el uso básico y los comandos antes de comenzar con el presente libro. Siempre que esté
informado sobre los tipos de datos más comunes (enteros, flotantes, cadenas, listas, diccionarios), sepa
cómo escribir una función simple y pueda manejar listas, podrá resolver todos los problemas planteados
en este libro. . Si quieres refrescarte rápidamente los aspectos más básicos del idioma, te recomiendo
el curso que ofrece la Universidad de Waterloo.1

Filosofía

Los acertijos presentados en este libro están dirigidos a principiantes con poca experiencia en temas
generales de programación. Si alguna técnica matemática es necesaria para resolver un problema, se
introducirá junto con el propio rompecabezas. El código que se muestra en este libro no aspira a ser la
solución más elegante, más corta ni más eficaz, sino que ilustra conceptos básicos de programación y
cómo pensar como un programador.
Para la mayoría de los acertijos presentados, existen algoritmos altamente especializados que pueden
mejorar la velocidad en gran medida, pero a menudo no son obvios para los principiantes y, en muchos
casos, requieren mucha información básica. Para resolver los problemas, no necesitará otras
herramientas, software o paquetes que el entorno nativo de Python (Python puro). Dicho esto, existe
una multitud de excelentes paquetes de Python que aumentan drásticamente la cantidad de funciones
de Python (por ejemplo, NumPy, SciPi o Pygame, solo por nombrar algunos). Sin embargo, estos suelen
venir con documentación extensa y necesitan tutoriales para que sean comprensibles para el principiante.
En general, los acertijos más fáciles se colocan al comienzo de un capítulo para introducir nuevos
conceptos y métodos que luego se supone que se conocerán en el siguiente.
1 [Link]

•6
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

rompecabezas. Por lo tanto, podría ser una buena idea trabajar los problemas siguiendo el orden del
libro. Sin embargo, si se siente seguro, no dude en saltar y jugar. Si hay comandos o conceptos
desconocidos, suele ser la forma más rápida de acceder a un motor de búsqueda y buscar cosas en
línea, ya que solo toma unos segundos y es la forma más fácil.

Expresiones de gratitud

Estoy muy agradecido a todas las personas que me ayudaron con este libro, especialmente a Florian
Scholze, Jannik Köster y Kurt Bittmann. Simon Wolf revisó todo el código meticulosamente y lo mejoró
más allá de la imaginación. Sin Tam Hanna, no habría una versión en inglés de este libro: estoy
profundamente agradecido por este entusiasmo y tutoría. Además, quiero agradecer a la Python
Software Foundation en general por donar este maravilloso regalo al mundo. Finalmente, muchas
gracias a todos los hombres y mujeres que contribuyen a proyectos gratuitos de código abierto como
Wikipedia y Wikimedia Commons, que me permiten incluir una gran cantidad de figuras de alta calidad
en este libro.

Todo el código disponible en: [Link]

•7
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Capítulo 1 • Conceptos básicos

1.1 • Entorno de instalación y programación

Asegúrese de haber instalado la versión más actualizada de Python desde [Link]. Para ejecutar el código
presentado en este libro necesita al menos la versión 3.6. Si ejecuta Linux o Mac, lo más probable es que Python
ya esté preinstalado en su sistema. Para probar qué versión está ejecutando, abra una terminal (Linux o Mac) o
Power Shell (Windows). Luego escriba python3 para iniciar una sesión interactiva. Luego se mostrará la versión
actual.

Recomiendo usar Geany1 como IDE o editor. Esta pequeña aplicación de código abierto (16 MB)
es perfecta para principiantes y usuarios avanzados y viene con muchas funciones sin ser
voluminosa ni demasiado complicada. Además, una gran cantidad de temas, esquemas y
complementos permiten ampliar fácilmente las funciones básicas. Geany está disponible para Linux, Windows y M

1.2 • Python básico

Las siguientes páginas sirven como un curso intensivo y se recomiendan para todos los usuarios
que quieran actualizar sus habilidades, así que siéntase libre de seguir adelante si así lo desea. A
diferencia de la mayoría del código que se muestra en este libro, aquí nos referiremos a una sesión
interactiva de Python, que se indica con >>> para visualizar el carácter interactivo del código. Esto
significa que escriba una línea, presione Intro y verá instantáneamente el resultado, que es diferente
a escribir un script grande y luego ejecutarlo completo. La salida, si la hay, se muestra en la siguiente
línea sin >>>.

>>> a = 12
>>> b = 3.141
>>> c = "Tomate"
>>> d = [a, b, c]
>>> mi = (1.734, 3.822)
>>> f = {3, 8, 99, 4}
>>> g = {"Hola": 5, "No": 4, "Ego": 3, "Cohete": 6}

Aquí, a es un número entero, b un flotante, c una cadena, d una lista, e una tupla, f un set y g un
diccionario. Como ves, declarar una variable sólo requiere el signo de igualdad. Cuando trabaje con
expresiones matemáticas, asegúrese de recordar PEDMSR (paréntesis, exponentes, división,
multiplicación, suma, resta), ya que esto le ayuda a memorizar el orden en que se abordan los
operadores. Tenga en cuenta que los bloques de código más largos se dividen en varias líneas si es
necesario utilizando "\" como indicador de un salto de línea. Si ingresa el código en su editor, no
escriba este signo ya que es solo una ayuda visual para la versión impresa.

Índices y cortes

Para Python, las listas son una herramienta multiuso que se puede utilizar en la mayoría de situaciones. Conjuntos, tuplas
1 [Link]

8
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

y los diccionarios agregan muchas más funciones y, a menudo, son más rápidos o más convenientes, pero a
Python le encantan las listas. Puede almacenar cualquier elemento o tipo de datos en una lista y, por supuesto,
también más listas anidadas. Recupera elementos de una lista a través de su índice. Recuerde, en Python (como
en la mayoría de los otros lenguajes de programación), el primer elemento de una lista siempre recibe el índice 0.

1>>> a = [1, 2, 3]
>>> b = ["Hola", 1, "Rojo", 6.87, [1, 2, 3, ["Ratón"]], 95]
>>> un[0]
1

>>> b[2]
"Rojo"

>>> b[4][1]
2
>>>b[1]
95

>>> solo(b)
6

>>> solo(b[4])
4

Como puede ver, los elementos de las listas anidadas se recuperan combinando varios índices directamente. Por ejemplo, si
desea recuperar el número entero 2 de la lista b, primero seleccione la lista anidada contenedora (que tiene el índice 4) y luego el
índice de esta sublista (que es el índice 1), de modo que el resultado final sea b[4][1]. Aquí, utilice siempre corchetes (esto también
se aplica a tuplas y dictados). Si desea recuperar el último elemento de una lista, independientemente del número de elementos
contenidos, utilice índices negativos. El último elemento siempre recibe el índice 1. El número de elementos de una lista se

informa mediante len(). Si desea cortar una lista en partes, lo llamamos corte.

>>>a = [1, 2, 3, 4, 5, 6, 7]
>>> un[0:3]
[1, 2, 3]
>>> un[2:5]
[3, 4, 5]
>>> un[::2]
[1, 3, 5, 7]
>>> a[::1] [7, 6, #Invertir una lista

5, 4, 3, 2, 1]

El operador de corte tiene tres partes: inicio, final y paso. El inicio siempre se incluye en la lista resultante,
el final siempre se excluye. Si no se establece ningún paso explícitamente, se implica 1. Si se omiten el
inicio o el final, Python usa el primer o el último elemento. Además, tenga en cuenta que las listas y las
cadenas se pueden dividir en la misma forma.

•9
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

>>> w = "trebuchet"
>>> en[3]
"b"
>>> w[2::2]
"oh"

Diccionarios

Los diccionarios o dicts son útiles cuando desea crear una base de datos muy simple para realizar búsquedas.
Aquí se crean pares de claves y valores, que no se seleccionan por índice sino por clave. Pongamos un ejemplo
sencillo con fechas de nacimiento.

>>> fechadenacimiento = {"Dawkins": 1941, "Dostojewski": 1821, "Goethe": 1749}


>>> fechadenacimiento["Goethe"]
1749
>>> fecha de nacimiento["Boyle"] = 1948
>>> fecha de nacimiento
{"Dawkins": 1941, "Dostojewski": 1821, "Goethe": 1749, "Boyle": 1948}

El primer valor (antes de los dos puntos) es la clave, el que está después del valor. Para recuperar el valor, simplemente
ingrese la clave entre paréntesis. La adición de nuevos elementos se realiza de la misma manera. Tenga en cuenta
que las claves deben ser inmutables, por lo que puede utilizar números enteros, flotantes, cadenas o tuplas, pero no
listas. Para los valores, cualquier tipo de datos está bien. Los dictados tienen la ventaja sobre las listas de que la
búsqueda es más rápida. Una tarea muy común es recorrer claves, valores o ambos y recuperar ciertos elementos.
Aquí tienes varias opciones para hacer esto.

>>> para la clave en [Link]():


>>> llave
Dawkins
Goethe
Dostoievski
boyle

>>> para el valor en [Link]():


>>> valor
1941
1821
1749
1948

>>> para clave, valor en [Link]():


Machine Translated by
Google
Capítulo 1 • Conceptos básicos

("Dostoievski", 1821)
("Goethe", 1749)
("Boyle", 1948)

El último esquema es especialmente útil ya que recuperas claves y valores al mismo tiempo en una tupla y
puedes trabajar con ellos inmediatamente. El orden en el que se recuperarán los elementos del dict era aleatorio
hasta la versión 3.7, después de eso, cada dict viene con un orden inherente, que podría ser útil para ciertas
aplicaciones. Más adelante veremos cómo podemos ordenar los dictados de forma arbitraria. Como nota al
margen: siempre que trabajemos en la sesión interactiva como en el último ejemplo, es opcional usar la
declaración de impresión para generar resultados, ya que simplemente llamar a una variable o función producirá
automáticamente un resultado visual en la consola.
Sin embargo, si desea utilizar el mismo código en un archivo, ajuste siempre estas variables en print(); de lo
contrario, no se mostrará.

Bucles

Python conoce varias formas diferentes de realizar bucles. Usando for, puede recorrer directamente todos los
elementos de un iterable o iterador dado, por ejemplo, un rango, lista o tupla.2 Los bucles while son útiles cuando
no se sabe de antemano con qué frecuencia se ejecuta un bucle y se desea para salir dinámicamente. Echemos
un vistazo a tres ejemplos.

>>> para i en el rango(0, 10, 2):


>>> i
0
2
4
6
8

>>> lista de palabras = ["Esto", "es", "bien"]


>>> para palabra en la lista de palabras:
>>> palabra
'Este'
'es'
'bien'

>>> valor = 0
>>> mientras que el valor < 64:
>>> va

2 En Python, un iterable es un objeto sobre el que se puede iterar, digamos una lista o tupla. Un iterador es
un generador que guarda su propio estado interno, lo cual resulta útil cuando se vuelve a llamar al mismo objeto.
Solo se pueden llamar iteradores usando next(). Más adelante veremos cómo se puede utilizar esto para
nuestro beneficio.

11
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

0
1
2
4
dieciséis

El primer ciclo produce todos los números pares del 0 al 10 (exclusivamente). Al igual que con los cortes, el primer
valor es el inicio, el segundo la parada y el tercero el paso. La variable i es el índice y puede nombrarse
arbitrariamente. El segundo bucle itera sobre todos los elementos de la lista dada. El último bucle continúa
ejecutándose hasta que se cumple la condición de salida. En este ejemplo, el valor debe ser menor que 64 para
que el ciclo continúe. Si se viola esta condición, el ciclo ya no se inicia. Los bucles que nunca cumplan esta
condición de salida se ejecutarán para siempre y el usuario deberá finalizarlos (bucle infinito). Por lo tanto,
asegúrese de que la variable que controla la salida se manipule en algún lugar dentro del bucle, ya que sólo
entonces es posible una salida.

Si desea salir de un bucle prematuramente, utilice break. Continuar es útil cuando desea mantener el bucle en
ejecución pero omitir ciertos elementos, posiblemente para mejorar el rendimiento o evitar errores obvios (como
cuando desea procesar números enteros pero aparece una cadena en una lista). Con continuar, Python siempre
saltará inmediatamente al inicio del ciclo, independientemente de dónde se ejecute el script en ese momento
dentro del ciclo. Utilice pass como marcador de posición genérico que no hace exactamente nada, como su
nombre indica. Echemos un vistazo a tres ejemplos.

>>> para número en el rango(1, 5):


>>> imprimir (número)
>>> si número == 3:
>>> romper
>>> imprimir(número * 10)
>>> print("Bucle exterior ahora")
1
10
2
20
3
Bucle exterior ahora

Tan pronto como se llegue a la pausa , Python abandonará el bucle de inmediato y continuará con el código
siguiente. Se omitirá cualquier código dentro del bucle debajo de la interrupción.

12
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

>>> para número en el rango(1, 5):


>>> imprimir (número)
>>> si número == 3:
>>> continuar
>>> imprimir(número * 10)
>>> print("Bucle exterior ahora")
1
10
2
20
3
4
40
Bucle exterior ahora

Cuando se llega a continuar , Python volverá al inicio del ciclo y continuará con el siguiente elemento del iterable.
Se omite el código dentro del bucle siguiente continuar.

>>> para número en el rango(1, 5):


>>> imprimir (número)
>>> si número == 3:
>>> aprobar
>>> imprimir(número * 10)
>>> print("Bucle exterior ahora")
1
10
2
20
3
30
4
40

Cuando se alcanza el pase , no pasa nada. El bucle no se sale y Python continúa ejecutando cualquier código
siguiente, si lo hay. El pase suele funcionar como marcador de posición.

Comprensiones

Las comprensiones se pueden utilizar como una alternativa muy compacta para los bucles y también podrían
mejorar el rendimiento. Si bien distinguimos comprensiones de lista, dictado, conjunto y generador, su sintaxis es
casi idéntica. Supongamos que desea generar una lista con todos los números enteros menores de 100 que sean
divisibles por 3 y 7. Usando comprensiones podemos resolver esto dentro de un

13
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

línea de código.

>>> [i para i en el rango(100) si i % 3 == 0 y i % 7 == 0]


[0, 21, 42, 63, 84]
>>> [i ** 2 para i en (1, 2, 3, 4, 5)]
[1, 4, 9, 16, 25]

Los corchetes indican que queremos crear una lista, i es el índice que toma todos los valores del 0 al
99. Como puede ver, incluimos un filtro para clasificar todos los números enteros que no se ajustan a
nuestra condición. El segundo ejemplo ilustra cómo podemos transformar dinámicamente los
resultados antes de agregarlos a la lista. If...else también se permiten construcciones con una sintaxis
ligeramente diferente (tenga en cuenta el orden de los elementos).

>>> [1 si x > 5 sino 0 para x en el rango(10)]


[0, 0, 0, 0, 0, 0, 1, 1, 1, 1]

En este ejemplo, recibimos una lista que muestra un 1 para cualquier número mayor que 5 y un 0 en
caso contrario. If y else ahora se colocan en el lado izquierdo del iterador, ya que este ya no es un
filtro sino el operador ternario. Del mismo modo se pueden crear conjuntos y dictados, la única
diferencia es el tipo de corchetes.

>>> {i para i en el rango(10)}


{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {palabra: len(palabra) para palabra en ["Nosotros", "nos divertimos", "divertimos"]}
{'Nosotros': 2, 'tenemos': 4, 'divertido': 3}

Tenga en cuenta el hecho de que las comprensiones pueden volverse fácilmente complejas cuando
se incluyen comprensiones anidadas. En este ejemplo, creamos una matriz simple, que es una lista
con sublistas.

>>> [[i * j para i en el rango(4)] para j en el rango(4)]


[[0, 0, 0, 0], [0, 1, 2, 3], [0, 2, 4, 6], [0, 3, 6, 9]]

Python trabaja de adentro hacia afuera, creando primero una lista que contiene los productos de i y j.
Después de eso, las cuatro listas nuevas se devuelven juntas en una lista superior. Como puede ver, esto se
vuelve difícil de leer y, si bien las comprensiones permiten expresiones muy compactas y sofisticadas, pueden
convertirse fácilmente en una molestia para sus colegas (o para usted mismo después de regresar a su código
después de un descanso de dos semanas). Siempre que se anidan bucles, se recomienda especial precaución

14
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

para generar código benigno y legible.

Funciones

Siempre que necesite resolver tareas más complejas, se recomienda dividir el código en partes funcionales y crear
varias funciones combinadas. Esto tiene muchas ventajas: en primer lugar, las funciones se pueden reutilizar
fácilmente e incluso importar a otros documentos. En segundo lugar, depurar funciones suele ser más fácil que
bloques de código más grandes, ya que puedes probar cada función por separado. Resumido: ¡divide y vencerás!

En Python, las funciones se pueden definir con dos expresiones. El primero es def(). Una función puede incluir un
número arbitrario de argumentos, que también pueden establecerse como predeterminados.3 Veamos esto en
acción con una calculadora de suma muy simple.

>>> def sumador(x, y):


>>> volver x + y
>>> sumador(1, 1)
2

Esta función tiene dos argumentos, x e y. Estos siempre deben ser especificados por el usuario al llamar a la
función. Usando return especificamos qué valor queremos recibir de la función. Si el programador no establece
ningún retorno o si nunca se alcanza, la función devolverá Ninguno. En muchos casos, esto es irrelevante, por
ejemplo, cuando una función se usa sólo para mostrar algo en la sesión interactiva.

>>> def saludos(nombre):


>>> print("Hola " + str(nombre) + "!")
>>> saludos("Python") "¡

Usando los valores predeterminados podemos preespecificar ciertos argumentos que el usuario puede
sobrescribir si lo desea.

>>> def exponenciar(x, y=2):


>>> devolver x ** y
>>> exponencial(3)
9
>>> exponencial(2, 4)
dieciséis

3 En este libro, los términos parámetros y argumentos se utilizan de manera variable con respecto a las funciones.

15
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

También podemos crear funciones anónimas usando lambda. Estas funciones suelen ser muy compactas ya que
constan de una sola expresión y se pueden definir "sobre la marcha".

>>> sumador = lambda x, y: x + y


>>> sumador(2, 2)
4

Como es necesario restringir la funcionalidad a una expresión, éstas normalmente no se aplican a tareas más
complejas. En este punto, también debes tener en cuenta el hecho de que ciertas expresiones se pueden acortar
para hacer el código más compacto.

x = x + 5 <=> x += 5
x = x 5 <=> x = 5
x = x * 5 <=> x *= 5
x = x / 5 <=> x / = 5

Controles internos y tratamiento de excepciones

Escribir software para usuarios finales requiere mucho tiempo y esfuerzo para garantizar que las entradas estén
desinfectadas y que solo ciertos tipos de datos se introduzcan en funciones especiales. Por ejemplo, una aplicación
de calculadora nunca debería tener que lidiar con cadenas, ya que para la aritmética solo se usan números. Al
escribir código para una aplicación web, asegúrese de que una dirección de correo electrónico siempre contenga
exactamente una arroba (@). En algunos casos, la función de recepción notará el problema y generará una
excepción o un mensaje de error, lo cual suele ser bueno ya que se le alertará de que algo salió mal. A veces, estos
problemas pasan desapercibidos y el primer problema que notarás más adelante, tal vez después de recibir un
resultado incorrecto.
Encontrar el error puede ser tedioso y difícil, por lo que crear algunos puntos de control suele ser una buena idea.
Para verificar entradas no válidas o resultados incorrectos, podemos usar afirmar. En este ejemplo, queremos
asegurarnos de que un correo electrónico determinado contenga al menos un signo de arroba.

>>> correo electrónico1 = "prueba@[Link]"

>>> afirmar "@" en el correo electrónico1, "¡Entrada no válida!"


>>> correo electrónico2 = "correo electró[Link] electró[Link]"

>>> afirmar "@" en el correo electrónico2, "¡Entrada no válida!"


Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
AssertionError: ¡Entrada no válida!

Si bien la primera prueba está bien ya que @ está incluido en la cadena dada, esta suposición se viola en el
segundo ejemplo. Luego, Python deja de procesar el script de inmediato y lanza un

16
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

excepción para que estemos informados sobre el problema. Sin embargo, tenga en cuenta que afirmar se puede utilizar
como diagnóstico interno para las primeras comprobaciones, pero asegúrese de definir las excepciones adecuadas y,
especialmente, más pruebas para desinfectar la entrada del usuario. Además, las declaraciones de afirmación se
eliminan del código cuando algunos compiladores optimizan el rendimiento.

Sin embargo, a veces queremos silenciar los errores de forma explícita y continuar con el script. Esto se hace usando

try...except. Si ocurre un error, podemos especificar de antemano cómo manejarlo.

Como ejemplo, supongamos que desea acceder a un determinado índice en una lista que no existe.
Normalmente, Python detendría el script y se quejaría.

>>> a = [1, 2, 3]
>>> un[20]
Rastreo (llamadas recientes más última):
Archivo "<stdin>", línea 1, en <módulo>
IndexError: índice de lista fuera de rango

Como la lista tiene sólo tres elementos, no hay ningún elemento con índice 20 disponible. Sin embargo, cuando

detectamos este error podemos continuar con el script.

>>> para lista en matriz:


>>>
intentar:

>>> imprimir(lista[20])
>>>
excepto IndexError:
>>>
print("Índice no encontrado, continuar")

Este script toma listas de una matriz determinada y siempre muestra el elemento con índice 20.
Es posible que algunas listas más cortas no contengan tantos elementos, lo que causaría problemas.
Sin embargo, como podemos prever que este error podría ocurrir, definimos que nuestro script detectará todos los
IndexErrors, produciremos una breve nota de advertencia y luego continuaremos con el código.
También existe la posibilidad de crear catchall, que son declaraciones que silencian cualquier tipo de error. Tenga
mucho cuidado al trabajar con estas cosas y es mejor especificar de antemano qué errores son posibles.

Módulos

Algunas funciones u objetos siempre están disponibles en Python, por ejemplo, listas o las funciones len() o max().
Algunas otras funciones también son partes oficiales de Python, pero están agrupadas en módulos que deben
importarse antes de su uso. Esta es una solución eficiente ya que no todas las funciones siempre se cargan en Python
y hay muchos más nombres para variables y funciones disponibles. Para acceder a estas otras funciones necesitamos
importar los módulos respectivos. Demostremos su uso con alguna función matemática.
Machine Translated by
Google 17
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

>>> importar matemáticas

>>> matemá[Link](matemá[Link])
1.0

Aquí importamos el módulo matemático para acceder a una constante y una función desde este módulo. El
prefijo math se utiliza posteriormente para indicarle a Python de dónde tomar las funciones. Sin embargo, escribir
esto todo el tiempo puede resultar tedioso, por lo que existen soluciones. Por ejemplo, podemos acortar el
nombre de un módulo para que escribir y leer código sea más conveniente.

>>> importar itertools tal como está


>>> lista([Link]([1, 2], 2))
[(1, 2), (1, 3), (2, 3)]

Siempre que solo se requieran unas pocas funciones, también podrá importar solo esta función.

>>> desde itertools importar combinaciones


>>> lista(combinaciones([1, 2], 2))
[(1, 2), (1, 3), (2, 3)]

Si necesita todas las funciones, utilice el asterisco como marcador de posición genérico.

>>> desde la importación de itertools *


>>> lista(combinaciones([1, 2], 2))
[(1, 2), (1, 3), (2, 3)]

Cuando se trabaja con scripts más largos y tareas más complicadas, puede resultar especialmente
beneficioso mantener los prefijos de módulo respectivos para que todos los colegas tengan claro de
dónde se toman ciertas funciones.

1.3 • Principios de una buena programación

1. Las sangrías desempeñan un papel importante en Python, ya que reemplazan la mayoría de los paréntesis y
corchetes conocidos en otros lenguajes de programación. Si utiliza espacios o tabulaciones para las
sangrías es irrelevante siempre y cuando sea coherente y nunca los mezcle, lo que hace que Python
produzca un mensaje de error.
2. Todas las variables y objetos (y en Python, prácticamente todo es un objeto) deben tener un nombre único y
claro. Hay ciertos estilos para elegir, por ejemplo, Panelleft (caso Pascal), panelLeft (caso Came) o
panel_left (caso Serpiente). Sin embargo,

18
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

Sea coherente con su estilo. Puede que no sea necesario perder el tiempo pensando en nombres de
variables de índice (a menudo solo i) o variables muy temporales. Intente limitar el uso de variables
de una letra para bloques estrechos de código o comprensiones.
3. Las funciones normalmente deberían hacer exactamente una cosa. Si concluye que una función
determinada hace muchas cosas, tal vez debido al uso de muchas declaraciones if...else, sería
prudente dividirla. Además, nunca defina dos funciones en dos lugares diferentes del código que
hagan lo mismo, sino defínalas una vez y cuando las llame cuando sea necesario. Esto hace que
la depuración sea mucho más fácil y, si encuentra alguno, debe limpiar los errores solo en un
lugar. Además, una función normalmente sólo debería devolver un tipo de datos (por ejemplo, una
función matemática que sólo devuelve números enteros, pero no cadenas ni listas). Siempre que
algo salga mal, no devuelva un "código de error" especial o Falso , sino genere una excepción.4
4. Python fue creado para hacer las cosas y trabajar de manera eficiente. Por lo tanto, puede ser una
buena idea pensar en ciertos parámetros antes de iniciar un proyecto. ¿Cuánta gente está
involucrada y cuánto tiempo llevará? ¿Debería empezar a definir diez clases o son suficientes unas
pocas funciones para realizar el trabajo? ¿Volveré a trabajar con este código dentro de cinco años o
quedará obsoleto la próxima semana? Dependiendo de las respuestas, es posible que desees
dedicar más tiempo a preparar el proyecto y definir las cosas, posiblemente con tus colegas. Esto se
refiere a un estilo común de codificación, denominación de objetos y creación de documentación
compartida. Tenga en cuenta que incluso los proyectos más pequeños merecen cierta
documentación, aunque sea sólo para un proyecto de fin de semana.
5. La legibilidad es un factor importante en cualquier código. Por ejemplo, un espaciado constante hace
que sea mucho más fácil de entender. Por lo tanto, recomiendo hacer uso de él y escribir x = (5 + 5)
en lugar de x=(5+5). Nuevamente, no existen reglas estrictas, sino pautas que usted puede elegir.
En este libro insertaremos un espacio entre la mayoría de los números y operadores.
6. La documentación clara y significativa es el estándar de oro de la programación. Especialmente los
proyectos más grandes y de mayor duración con muchos compañeros de trabajo merecen una
documentación extensa que sea comprensible para todas las personas que trabajen en ellos
después de usted. E incluso si codificas solo, tu yo futuro estará muy agradecido si dedicas solo
unos minutos a documentar lo que hiciste. Por ejemplo, las cadenas de documentación de Python
("""Este es el comentario""") son muy útiles para describir lo que está haciendo una función o clase.
En este libro hay poca documentación dentro de los bloques de codificación ya que todo se explica
en detalle en los capítulos, por lo que probablemente no utilices esta plantilla a menos que estés
dispuesto a explicar todo como en un tutorial. Para comentarios en línea utilice el signo numérico #.
7. Cuando tenga poca experiencia con el software de control de versiones, puede ser una buena idea
dedicar algún tiempo a aprender sobre él, especialmente cuando esté trabajando en proyectos más
grandes o de mayor duración. Esto hace obsoleta la creación de muchos documentos que te
permiten volver a versiones anteriores de tu código (todos conocemos [Link], [Link], [Link],…).
El software básico que te ayuda es git o bazaar. Cuando colabore en línea, pruebe Github.

8. La depuración, es decir, encontrar y corregir errores y fallas en su código, generalmente requiere una
gran parte de su tiempo. Una ventaja de Python que nunca se puede subestimar es que los
mensajes de error y las excepciones suelen ser muy claros e intentan describir qué salió mal, lo que
facilita mucho la búsqueda del problema. A veces se trata de errores triviales, como paréntesis o
letras faltantes. Si desconoce un error, simplemente busque
4 Para obtener más información sobre código limpio, investigue los trabajos de Robert C. Martin. Youtube ofrece
algunas presentaciones excelentes.

19
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

búsquelo en línea y las cosas podrían quedar mucho más claras. Además, cuando Python informe
una línea junto con el error, asegúrese de verificar las líneas antes y después si no la encuentra en
la que se informó.
9. No existe una regla sin excepción. Las pautas presentadas aquí son sólo principios básicos y no están
escritas en piedra. Puede haber buenas razones para desviarse de ellas, pero asegúrese de que
estén justificadas. Si se siente demasiado cansado o perezoso para seguir un determinado estilo,
tal vez sea hora de tomar un descanso en lugar de escribir código lento.
10. Si busca información más detallada sobre estilo y principios de codificación, consulte
asegúrese de echar un vistazo a la guía de estilo oficial de Python PEP8.5

1.4 • Habilidades para resolver problemas

Como se indicó anteriormente, este no es un libro de texto clásico y teórico, sino que se centra en
aplicaciones y habilidades de resolución de problemas del mundo real. Suponga que su jefe le asigna
una tarea y no está interesado en cómo la procesa exactamente, siempre y cuando presente rápidamente
los resultados. Depende de usted descubrir cómo hacerlo. Considerándolo todo, Python es una
herramienta valiosa para abordar tareas complejas. Viene con una amplia gama de bibliotecas, módulos
y paquetes que en muchos casos están relacionados de alguna manera con su tarea específica y se
pueden adaptar fácilmente. Dado que el rendimiento de las computadoras modernas es enorme, hoy en
día también es posible abordar problemas haciendo cálculos numéricos (soluciones de fuerza bruta) o
realizando simulaciones para soluciones aproximadas en lugar de pensar en una solución analítica que
requiere mucho conocimiento teórico, tiempo y experiencia. . ¿Cómo sería exactamente ese flujo de trabajo?

En primer lugar, es relevante comprender la tarea o problema en cuestión y obtener una visión general
de la situación. ¿Ya ha trabajado en desafíos relacionados en el pasado? ¿Conoces problemas similares?
Intenta deducir lo desconocido a lo conocido, lo cual es bastante fácil gracias a los motores de búsqueda
o Wikipedia. En muchos casos, encontrará soluciones en línea listas para usar que quizás solo requieran
implementación en Python. A veces tienes suerte y todo lo que necesitas hacer es copiar algunas líneas
de código. Dicho esto, por supuesto no es el objetivo de este libro resolver las tareas aquí presentadas
buscando en línea y buscando soluciones listas para usar; esto sólo entrenaría sus habilidades de
investigación. Por lo tanto, si está atrapado en un problema y se le acaban las ideas, tal vez simplemente
pase a la siguiente tarea y vuelva más tarde. El cerebro humano trabaja incansable y subconscientemente
en problemas no resueltos que pueden conducir a momentos Heureka.

Una vez que tengas un plan en mente, es hora de trabajar en la implementación en Python, lo cual es
una tarea fácil. Como se mencionó anteriormente, a menudo es una buena idea dividir los problemas
complejos en partes pequeñas que puedan resolverse fácilmente. Entonces, usar funciones como
implementación es bastante conveniente. En esta etapa no se esfuerce por alcanzar la perfección, ya
que probablemente desee obtener un primer resultado rápidamente, que luego podrá optimizar. A
menudo, su jefe podría estar contento con una primera aproximación siempre que se presente a tiempo.
Si tiene dificultades con la fase de implementación, podría ser beneficioso consultar un libro de texto o
una guía sobre la técnica requerida. Dado que Python viene con tantas funciones, rara vez es necesario
reinventar las cosas; sea inteligente y asegúrese de hacer uso de las funciones y módulos disponibles;
estos están probados y aprobados por la comunidad. La documentación oficial viene con muchos ejemplos y sirve como
5 [Link]

20
Machine Translated by
Google
Capítulo 1 • Conceptos básicos

Una maravillosa guía y maestro. Si necesita funciones especiales, podría ser aconsejable invertir algo
de tiempo en estudiar la documentación de estos paquetes externos, especialmente cuando profundiza
en algún material y planea trabajar más tiempo en proyectos relacionados.

Si su primer intento está completo y el código está escrito, es hora de realizar la prueba. A menudo, el código no
funcionará directamente según lo planeado, lo que provocará un error de ejecución o un resultado obviamente incorrecto.
Los errores de sintaxis se depuran fácilmente debido a los mensajes de error bastante específicos que
produce Python. Puede resultar más difícil eliminar errores lógicos en su código que se relacionen más
con sus algoritmos y estrategia que con la implementación en sí. Si esto sucede, primero intente probar
individualmente cada función para reducir la posible fuente del problema. Piense en casos cuya
corrección sea fácil de comprobar y avance hasta llegar a entradas más complejas. Es justificable colocar
declaraciones de impresión temporales dentro del código para observar el estado de las variables. Al
agregar declaraciones de suspensión, también puede ejecutar el código en cámara lenta y rastrear el
flujo. Aunque esta técnica de depuración suele ser ridiculizada, existen buenas razones para utilizarla,
especialmente en proyectos pequeños. Por supuesto, un depurador real es mucho más potente, pero a
menudo depende del IDE que utilices y requiere más experiencia. Python viene con el sistema de
depuración interno pdb6 que le permite seguir la ejecución de su código paso a paso. Para detectar
errores lógicos, asegúrese de explicar los principios y algoritmos de su código a sus colegas. Esto le
obligará a explicar claramente qué hace el código, lo que le ayudará a aclarar sus ideas.

Si el código se ejecuta y no tiene errores obvios, puede intentar optimizarlo. Especialmente cuando
trabaja en proyectos más largos que se ejecutarán con más frecuencia, aumentar el rendimiento y la
refactorización pueden ser de gran ayuda. Entonces deberías intentar trabajar en la legibilidad, la
documentación y el rendimiento para que tu código sea mejor y más agradable. Esta tarea suele ser
más relajada ya que tu jefe ya está contento con el primer resultado y hay menos presión. Intente
identificar bloques de código demasiado complejos y reescríbalos limpiamente. Agregue más
documentación mientras trabaja en ella. Cuando se trabaja en el rendimiento, probar funciones
individualmente le ayuda a identificar las partes lentas que podrían beneficiarse de diferentes enfoques.
En este libro, también hablaremos sobre medir las velocidades de ejecución y trabajar en la optimización.

6 [Link]

21
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Capítulo 2 • Trabajar con números

2.1 • Fibonacci

La secuencia de Fibonacci no sólo es conocida por los matemáticos desde hace milenios, sino que se
encuentra en aspectos muy diferentes de la naturaleza, por ejemplo en los pétalos de las flores, las leyes de
población y la proporción áurea. La secuencia está definida por una ley recursiva. Los dos primeros elementos
son 1 (f1=f2=1). Todos los elementos siguientes (i >= 3) están definidos por fi = fi1 + fi2. En palabras: el
siguiente elemento de la secuencia es la suma de los dos predecesores. Los primeros diez elementos de la
secuencia son, por tanto, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55. Esta definición recursiva requiere el cálculo del enésimo
elemento de la secuencia para calcular todos los predecesores. En este capítulo, hablaremos sobre diferentes
métodos de implementación.
Comenzamos desde el principio de la secuencia, generamos los primeros elementos y los usamos para avanzar
más. Una implementación muy simple podría verse así:

def fibonacci(n):
afirmar n > 0
a, b = 1, 1
para i en el rango (n):
imprimir(un)
a, b = b, a + b

Tenemos esta función que imprime todos los elementos hasta n. Definimos que el primer elemento recibe el
índice 1, por lo que incluimos una declaración de afirmación para desinfectar las entradas. No se permiten como
entradas índices cero o negativos. Aquí, b es el último y a el penúltimo elemento conocido de la secuencia. Al
principio, inicializamos estas variables con 1. En esta línea, usamos un atajo de Python (asignación de tupla). En
la última línea, utilizamos un truco similar que nos permite evitar el uso de una variable temporal para
intercambiar a y b .
Esta función sólo imprime resultados y no guarda nada en la memoria. Por lo tanto, no podemos trabajar con ellos.
Primero veamos esto en acción y pasemos a un segundo enfoque.

>>> fibonacci(10)
1
1
2
3
5
8
13
21
34
55
Machine Translated by
Google
22
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Este resultado es correcto. Ahora trabajemos con listas y mantengamos los elementos calculados en la memoria.

def fibonacci2(n):
elementos = [1, 1]
para i en el rango (n):
[Link](elementos[1] + elementos[2]) elementos
de retorno[:2]

Ahora renunciamos a la verificación de afirmación y definimos una lista que contenga los dos primeros
elementos. Usamos un bucle for para calcular tantos elementos nuevos como deseemos. Luego, la función
agrega el último y el penúltimo elemento de la lista y agrega este nuevo elemento. Finalmente,
devolvemos la lista completa pero cortamos los dos valores más extremos para corregir el desplazamiento
debido a los
dos elementos iniciales para que los usuarios reciban n elementos exactamente.

Como se describió anteriormente, la definición de la secuencia es recursiva. Parece una buena idea utilizar
este concepto para la implementación. A menudo, el código recursivo es compacto y bastante elegante; sin
embargo, puede resultar difícil de entender cuando se realizan tareas más complejas. Otra desventaja es
que la sobrecarga que se crea cuando la función se llama a sí misma reduce el rendimiento y consume
memoria, por lo que otros enfoques podrían ser más rápidos.
Otra cosa a considerar es la profundidad limitada de la recursividad en Python, que el usuario puede ajustar
si es necesario. Si se excede este límite, Python se cerrará con un mensaje de error. En general, la
recursividad es una herramienta útil y perfecta para este ejemplo. Para acelerar las cosas, utilizaremos la
memorización para mantener los elementos calculados en la memoria y buscarlos en lugar de calcularlos
nuevamente en cada ciclo recursivo. Esto requiere que escribamos una función anidada, ya que solo la
interna se llamará a sí misma de forma recursiva.

def fibonacci3(n):
elementos = {1:1, 2:1}
definición interior(n):
si n no está en elementos:
elemento_siguiente = interno(n1) + interno(n2)
elementos[n] = siguiente_elemento
elementos de retorno [n]
retorno interno (n)

En este ejemplo, solo devolvemos el enésimo elemento de la secuencia. La función externa define un dictado
que contiene los dos primeros elementos con su índice. Después de esto, definimos la función interna que
devuelve un elemento del dict si ya está incluido; de lo contrario, lo calculará y lo guardará en el dict. Si
omitimos la función interna, cada llamada comenzaría con un dictado recién creado y no se produciría
ninguna memorización, lo que daría como resultado una velocidad sustancialmente menor.

23
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Asignaciones

1. Codifique una función que produzca los primeros 5000 números de Fibonacci y los devuelva en
una lista.

2. Es posible calcular el enésimo número de Fibonacci sin recursividad utilizando el


Fórmula de MoivreBinet. Implementarlo en Python y calcular el elemento número 1000 de la
secuencia. Utilice el enfoque habitual que se muestra arriba y compare los resultados. ¿Que
notaste? ¿Qué salió mal?

3. Compare el desempeño de dos implementaciones diferentes de funciones que generan números de


Fibonacci. Sugerencia: [Link]() o [Link]() se pueden utilizar para medir el tiempo de
ejecución de funciones.
4. El cociente de dos números de Fibonacci adyacentes se acerca a la proporción áurea (1,6180339887…)
cuando n tiende al infinito. Calcule el cociente de los elementos 101, 102, 103, 104 y 105 y la
desviación porcentual del resultado real.
5. Calcula la suma de la inversa de los primeros 5000 números de Fibonacci.
6. Según el teorema de Zeckendorf, cualquier número entero se puede escribir como la suma de
exactamente dos números de Fibonacci diferentes y no adyacentes. Por ejemplo, 6 se puede
expresar como la suma de 5 y 1. Cree una función que acepte un número entero como entrada y
lo fraccione en dos números de Fibonacci. Sugerencia: puede buscar el algoritmo requerido en línea.1
7. En la última función, fibonacci3(), utilizamos dos funciones anidadas. Vuelva a escribir esta función
para crear una solución recursiva que no requiera una función interna. Sugerencia: aquí no hay
necesidad de variables globales.

Apéndice: Comprender la recursividad

Si nunca antes ha trabajado con funciones recursivas, puede resultarle difícil comprender el concepto.
Especialmente con aplicaciones más largas o complejas, puede resultar difícil seguir su flujo. Por ello
queremos resaltar aquí los conceptos básicos de esta técnica. La idea central de la recursividad es
escribir una función que modifique un problema determinado y luego se llame a sí misma.
Esto puede parecer extraño, pero una función puede llamarse a sí misma. Es como levantarse por sí
solo, pero es una buena y válida idea en programación. Para que esto funcione, deben cumplirse dos
supuestos básicos. Primero, debe haber un caso base que sea el caso que detenga la recursividad. Si
esto no se define o nunca se alcanza, la recursividad se ejecutará para siempre, lo que normalmente no
es lo que queremos. Es una buena idea comenzar definiendo este caso base y luego continuar con el
resto de la función. En segundo lugar, cuando la función se llama a sí misma, los argumentos utilizados
en esta llamada no pueden ser idénticos a los originales. De lo contrario, no hay progreso y la
recursividad se bloquea nuevamente. Normalmente, el argumento se incrementa o se reduce. Pongamos
otro ejemplo. En matemáticas, el factorial se define así:

1 [Link]

24
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Esto significa que el factorial de 5 es 120 (1x2x3x4x5). Implementemos esta fórmula usando recursividad. Como
aprendemos de la definición, comenzamos con 1 y contamos hasta n. De lo contrario, comenzamos con n y
contamos hacia atrás hasta llegar a 1. Esto garantiza que obtendremos todos los números enteros intermedios.
En consecuencia, definimos 1 como nuestro caso base. Además, queda claro que utilizamos la misma operación
una y otra vez (multiplicación), sólo que con argumentos diferentes.

def hacer(n):
print("Calculando el factorial de:", n)
si norte == 1: #Caso base
imprimir("Devolver: ", 1)
regresar 1

demás: #llamándose a sí mismo


resultado = n * fac(n 1)
imprimir("Retorno: ", resultado)
resultado de retorno

Incluimos varias declaraciones impresas para rastrear lo que sucede cuando llamamos a esta función. Para una
prueba, llamamos a la función con la entrada 3. Internamente, la función primero verifica si la entrada es igual al
caso base. No, ya que 3 no es igual a 1. Por lo tanto, se ingresa la cláusula else.
Ahora tenemos que calcular el resultado. Esto se hace multiplicando n (3) por el factorial de n 1 (2). Aquí la
función se llama a sí misma. Observe cómo la entrada es diferente a la anterior. Al hacerlo, nos acercamos al
caso base ya que disminuimos de 3 a 2. Ahora la nueva instancia de la función se crea y se ejecuta mientras la
primera instancia tiene que esperar a que la instancia interna devuelva el resultado. Veamos el rastro completo.

>>> Yo hago(3)
Calcular el factorial de: 3
Calcular el factorial de: 2
Calcular el factorial de: 1
Retorno: 1
Retorno: 2
Retorno: 6
6

Ahora debería quedar más claro lo que sucede. Primero, llamamos a la función con 3 (desde "afuera").
Como no se alcanza el caso base, se ejecuta la cláusula else y se crea una nueva instancia, que muestra el
mensaje. Esto sucede hasta que se alcanza el caso base para la función más interna. Ahora, esta función
presiona retorno y produce una salida, que se devuelve al

25
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

próxima instancia. Por lo tanto, propagamos el retorno hacia arriba y cada función que llama usa el
resultado para calcular su salida. Finalmente, tenemos el resultado correcto, que es 6. Este proceso
se puede visualizar mediante un diagrama.

Figura 2.1: Esquema de recursividad

Como ejercicio, intente escribir algunas funciones que implementen operaciones matemáticas básicas
mediante recursividad. Por ejemplo, suma, multiplicación o exponenciación. El esquema es similar y
los resultados se pueden comprobar fácilmente. Asegúrese de incluir declaraciones impresas para
que pueda verificar el flujo interno de las funciones.

2.2 • Números primos

Los números primos no sólo han intrigado a los humanos durante milenios, sino que también tienen muchos
propósitos prácticos: por ejemplo, en seguridad o criptografía. La generación y verificación de grandes
números primos es un desafío de gran relevancia para las ciencias informáticas aplicadas. Un número primo
es un número entero que es divisible sólo por 1 y por sí mismo. Definimos 2 como el número primo más
pequeño para todas las siguientes tareas y ejemplos. Por lo tanto, la secuencia de números primos comienza
con 2, 3, 5, 7, 11, 13, 17 y 19. Si bien encontrar números primos extremadamente grandes se ha convertido
en una especie de deporte en la intersección de las ciencias de la computación y las matemáticas,
trabajaremos con números primos mucho más pequeños. Se encuentran disponibles una gran cantidad de
heurísticas y técnicas para encontrar o generar números primos. Probablemente la más sencilla sea la
técnica de la fuerza bruta, que encuentra números primos probando todos los posibles divisores propios.
Dado un número entero n, entonces n es primo sólo si no hay divisores adecuados para n. Por lo tanto, al
probar todos los números enteros hasta n como divisores eventualmente se revelará si n es primo o no.

Codificar una prueba de este tipo es bastante simple y puede usarse como un maravilloso ejemplo
para echar un vistazo a otra especialidad de Python: los generadores. Mientras que una función
normal procesa números y finalmente devuelve algo para finalizar la función, un generador puede
devolver algo varias veces y almacenar su estado actual en la memoria. Cada vez que se llama al generador,

26
Machine Translated by
Google
Capítulo 2 • Trabajar con números

producirá una salida basada en el estado guardado hasta que se agote (si esto puede ocurrir).
Dado que hay un número infinito de números primos, un generador de primos puede funcionar para
siempre. La única diferencia entre una función regular y un generador es el hecho de que el retorno se
reemplaza por el rendimiento. Además, los generadores se manejan de manera un poco diferente, ya que
deben configurarse explícitamente y pueden llamarse usando next(). Veamos esto en acción.

def generador primario(n=2):


"""Crea números primos consecutivos mayores o iguales a
n""" si norte <= 2:
rendimiento 2
norte = 3

si n % 2 == 0:
norte += 1

mientras que Verdadero:

para divisor en rango(3, int(n ** 0,5 + 1),


2): si n % divisor == 0:
romper
demás: #descanso nunca alcanzado
rendimiento sustantivo, masculino—

norte += 2

Aquí definimos un valor predeterminado para que el generador comience a producir números primos comenzando
con 2 si el usuario no establece un número mayor. Además, agregamos una cadena de documentación para
describir lo que está haciendo el generador. Dado que 2 es el único primo par, tenemos que manejar este caso
explícitamente.
Después de esto, todos los números enteros con los que estamos tratando deben ser impares ya que cada
número par se puede dividir por 2. Para probar la divisibilidad de dos números usamos el módulo (%), que se
puede describir como devolver el resto de una división. Si hay un divisor propio, el resto es cero.
Si este no es el caso, sabemos que después de la división el divisor no era el correcto.
Esto se puede utilizar para comprobar si un número es par o impar. Divídelo entre 2 y mira el resto: si es cero el
número era par, en caso contrario es impar. Usamos esta técnica para asegurarnos de que solo se utilicen
números impares como primos potenciales. Este truco ordena la mitad de todos los números enteros y acelera el
cálculo.
Entramos en un ciclo while que se ejecuta hasta que se prueban todos los divisores potenciales. Siempre
comenzamos con 3 y avanzamos hasta la raíz cuadrada de n.2
¿Por qué? Es fácil ver que podemos dejar de probar cuando el divisor es mayor que la mitad de n (ya que el
resultado debe ser menor que 2). Sin embargo, con un poco más de matemáticas también se puede demostrar que
basta con ir sólo a la raíz cuadrada de n. Si el divisor es mayor no puede ser adecuado, así que podemos parar.
Tenga en cuenta que en lugar de cargar el módulo matemático y la función de raíz cuadrada, exponenciamos en 0,5
y obtenemos el mismo resultado. Aquí nos aseguramos de generar siempre un número entero a partir de la raíz
cuadrada para que el rango funcione correctamente.
Si el resto de este cálculo es cero, se encontró un divisor adecuado y podemos detenernos inmediatamente ya
que n no puede ser primo. Esto significa que pulsamos pausa, dejamos el bucle for y saltamos hasta el final del
bucle while adjunto, aumentamos n en 2 y continuamos con el siguiente.
2 [Link]
Machine Translated by
Google 27
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

potencial primo. Sin embargo, cuando nunca se alcanza la interrupción en el bucle for hasta que se agotan todos
los divisores, Python salta a la cláusula else. Esto significa que después de probar todos los divisores no
encontramos ninguno, por lo que n debe ser primo. Luego devolvemos el número usando rendimiento. Si se
vuelve a llamar al generador después de esto, continuará después de esto, aumentando así n en dos y
comenzando una nueva ronda. Este concepto de usar else en combinación con un bucle for puede ser nuevo
para usted, pero en realidad es bastante pitónico. Puedes pensar en él como "nobreak" para memorizar cuál es
su función. Ahora podemos ver cómo invocar el generador y producir números primos.

>>> primos = generador de primos()


>>> para i en el rango(5):
>>> siguiente (primos)
2
3
5
7
11

Si necesita números primos más grandes, simplemente llame al generador con un número entero más grande como argumento.
Técnicamente, los números primos son un iterador y se pueden utilizar de muchas maneras, por ejemplo usando
map() o una comprensión. Si necesitamos una determinada subsección de todos los números primos, esto se
puede lograr usando islice de itertools. Si necesitamos todos los números primos del elemento 100 al 120

(exclusivamente), podemos hacerlo de la siguiente manera.

>>> importar itertools


>>> primos = generador de primos()
>>> lista([Link](primos, 100, 120))
[547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659]

Finalmente, debemos admitir que nuestro generador es bastante lento y puede tener problemas de rendimiento
cuando necesitamos números primos realmente grandes. Aunque eliminamos todos los números pares como
divisores potenciales, el código no amplía este patrón. Después de probar 3 como divisor, podríamos ordenar
automáticamente todos los divisores que son divisibles por 3, digamos 15. Sin embargo, dado que creamos este
generador con solo unas pocas líneas de código, estos problemas son aceptables por el momento.

Asignaciones

1. Calcule los primeros 5000 números primos y guárdelos en una lista.


2. Un primo gemelo ocurre cuando dos primos consecutivos están separados por 2, por ejemplo,
41 y 43. ¿Cuántos primos gemelos hay entre 2 y 5000?
3. La distancia entre dos números primos también se llama brecha entre primos (Gn = pn+1 – pn).

28
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Por lo tanto, la brecha entre primos 13 y 17 es 4. ¿Cuál es la brecha entre primos más grande para todos
los primos desde 2 hasta 5000?
4. Los semiprimos son números enteros que son producto de exactamente dos primos, por ejemplo, 35 como
producto de 5 y 7. Crea una función que pruebe si un número entero dado es semiprimo.

2.3 • Colátz

La conjetura de Collatz impresiona por un lado por su sencillez y por el otro por la tenacidad con la que evade la
solución.

1, es fácil ver que esto resulta en un ciclo infinito (1, 4, 2, 1). Hasta ahora, a pesar de intensos esfuerzos, no se
ha encontrado ni un contraejemplo ni una prueba formal o refutación de la afirmación. Teóricamente, todavía
existe la posibilidad de que la secuencia crezca infinitamente o que se alcance otra secuencia cíclica que no
contenga 1. Primero, podemos definir una función muy simple que prueba si alguna vez se alcanza 1 para un
entero n dado.

def colatz1(n):
mientras norte > 1:
si n % 2 == 0:
norte = norte // 2
demás:

norte = (norte * 3) + 1
devolver verdadero

El código se explica por sí mismo. Tenga en cuenta que estamos usando división de enteros (//); de lo contrario,
Python se convertirá a flotante, lo cual no tiene sentido porque solo pueden ocurrir números enteros. Como
puede ver, esta función solo puede devolver Verdadero , lo que significa que ya hemos incorporado nuestra
suposición al programa. De esta manera no podríamos encontrar un contraejemplo.
Para encontrar un ciclo que no contenga 1, debemos mantener un registro de qué números ya han sido visitados.
Dado que el algoritmo es estrictamente determinista y cada número sólo puede tener exactamente un sucesor,
podemos reconocer un ciclo por el hecho de que el mismo número ha sido visitado varias veces.3

Cabe señalar que los mismos números pueden tener dos predecesores diferentes.
3 Por ejemplo, puedes llegar a 16 desde 5 o 32.

29
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def colatz2(n):
visto = establecer()
mientras que Verdadero:

si norte == 1:
devolver verdadero
elif n en visto:
falso retorno
demás:

[Link](n)
si n % 2 ==
0:
norte = norte // 2
demás:

norte = (norte * 3) + 1

Para realizar un seguimiento de los números que ya se han visto, utilizamos un conjunto. Esto es más
rápido que una lista con respecto a la velocidad de búsqueda. Los conjuntos son similares a las listas pero
no tienen un ordenamiento y no pueden contener el mismo elemento más de una vez. Sin embargo, en este
caso esto es irrelevante porque nos detenemos tan pronto como se conoce el número encontrado. Si
llegamos a 1, generamos True. La suposición se confirmó en el ejemplo. Si por el contrario llegamos por
segunda vez a un número ya conocido, se devuelve False . De lo contrario, el algoritmo continúa según lo
planeado. Cabe señalar que hasta ahora se han probado todos los números hasta 87 x 260 y ninguno de
ellos no llegó a 1 al final.4 Sin embargo, nuestra función no puede detectar si alcanzamos una secuencia
que continúa creciendo infinitamente.
Esto no se puede probar mediante prueba y error ya que la verificación requeriría que siguiéramos la
secuencia hasta el final, lo cual es imposible por definición. A este respecto, corresponde a los matemáticos
proporcionar una prueba formal en este punto.

Asignaciones

1. Escriba una función que calcule el número total de números enteros probados por el algoritmo para
un punto inicial dado n. Pruebe todos los números del 2 al 5000. ¿Qué número produce la
secuencia de Collatz más larga?
2. Pruébelo y pruebe si termina un número muy grande. Medir el tiempo de
ejecución del intento.

2.4 •Pi

Pocos números gozan de tanta popularidad como Pi. El cálculo de tantos decimales como sea posible de
este número trascendental e irracional, que define la relación entre la circunferencia de un círculo y su
diámetro, ha sido un ejercicio aritmético popular durante siglos. Existen numerosas fórmulas y métodos
para elegir, pero en este capítulo nos limitaremos a un cálculo matemático. La implementación de una
fórmula de este tipo en Python es sencilla en principio, pero rápidamente encuentra problemas si el objetivo
es el cálculo de muchos decimales. Mientras que Python puede manejar números enteros de cualquier
tamaño y está limitado
4 [Link]
Machine Translated by
Google • 30
Machine Translated by
Google
Capítulo 2 • Trabajar con números

sólo por la memoria y la capacidad computacional, la situación es diferente para los números decimales.
Normalmente, los números decimales se manejan como flotantes, que se almacenan en Python con doble
precisión, es decir, con 64 bits. Esto conduce rápidamente a errores de redondeo, como muestra un simple
cálculo:

>>> 1.1 + 2.2


3.3000000000000003

De dónde viene el número tres al final de los flotantes parece inexplicable al principio, pero es la consecuencia
de la representación interna de los números de punto flotante en binario.5 Estos errores no son problemáticos
para la mayoría de las aplicaciones, pero no si queremos calcular miles. o incluso más decimales. Esto requiere
varios trucos y una estrategia de implementación inteligente. Pero empecemos de forma sencilla. Para calcular
Pi, implementamos la fórmula de John Machin, conocida desde 1706:

Aquí, arctan es el arcotangente o la función inversa de la tangente. Calcular Pi, por tanto, depende de un cálculo
muy preciso de esta función trigonométrica. Esto por sí solo no nos ayuda, ya que el arcotangente también es
irracional y no se puede expresar fácilmente, por ejemplo, usando fracciones. Sin embargo, podemos usar series
para aproximarlo.

Al principio, puede resultar sorprendente que una suma con un número infinito de sumandos dé un resultado
finito, pero esto es posible siempre que los sumandos se hagan cada vez más pequeños. En este caso se habla
de una serie convergente. Cuantos más elementos de suma se incluyan, más exacto será el resultado final.
Lógicamente, un cálculo fáctico de una suma infinita es imposible, sólo se puede lograr una aproximación. Con
este tornillo de ajuste podemos influir en el resultado final: cuantos más decimales necesitemos, más términos
sumaremos. En general, debemos evitar los números de punto flotante o flotantes. Esto es posible utilizando
varios trucos.

En primer lugar, se da el caso de que la arcotangente se define sólo entre Pi/2 y +Pi/2. Sin embargo, ya
podemos deducir de la fórmula mostrada arriba que sólo necesitaremos los valores 1/5 y 1/239 para calcular Pi,
que son positivos y menores que 1. Además, podemos evitar los números decimales multiplicando todos los
sumandos de la suma con una constante μ (My) de cualquier tamaño. Así obtenemos la siguiente suma:

5 Para obtener una explicación, consulte [Link]

31
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Ahora podemos elegir que μ sea tan grande como queramos, digamos 101000 si queremos 1000 decimales,
por ejemplo. Sin embargo, todavía existe el problema de que x es menor que uno y la información se pierde
a medida que avanzan los términos. Por ejemplo, (1/5)10 es un número extremadamente pequeño que
Python almacena internamente como flotante, es decir, con precisión limitada. Cuanto mayores son los
poderes, más grave se vuelve el problema. Comenzando con un exponente de aproximadamente 500, el
número en este ejemplo es simplemente cero para Python y un cálculo de términos posteriores no tiene sentido.
Por tanto, el truco debe consistir en evitar los flotadores. Puedes ver cómo funciona esto si reorganizas un poco los
términos. Miremos el segundo término de la suma y reorganicémoslo:

Sacamos x del denominador y tomamos el inverso de eso. Pero sabemos que x en nuestro ejemplo siempre será menor
que uno (1/5 o 1/239). Si ahora usamos este valor como ejemplo, obtenemos

Siempre que μ y, por tanto, el numerador sea mayor que el denominador, evitamos los números decimales y solo podemos
calcular con números enteros. Esto funciona siempre que solo usemos valores entre 0 y 1 para x y μ sea lo
suficientemente grande. La fórmula que queremos implementar es la siguiente:

con z = 1/x

Veamos esto en código.

32
Machine Translated by
Google
Capítulo 2 • Trabajar con números

importar matematicas

importar itertools

def arctan(z, dígitos):


dígitos_extra = [Link](math.log10(dígitos /
math.log10(z))) signo = 1
término = 10 ** (dígitos + dígitos_extra) // z
resultado = término
para poder en [Link](3, 2):
término //= z ** 2
si término < potencia:
romper
resultado += (signo * término) // potencia
signo *= 1
devolver resultado // (10 ** dígitos_extra)

La función acepta dos argumentos, el inverso del número a calcular y el número de dígitos significativos.
Para protegerse contra errores de redondeo, también aumentamos la cantidad de dígitos
utilizados para todos los cálculos. Seguimos siendo flexibles y solo agregamos tantos dígitos como
sean necesarios. Si quisiéramos 3000 dígitos para z, siempre agregaríamos 5 lugares
internamente.
Definimos el signo, que alternativamente se vuelve negativo y positivo. Luego inicializamos el primer
término de la suma en término, sumando los dígitos adicionales calculados. Posteriormente, resultado
siempre será el valor de la suma total ya calculada, término es el nuevo término a agregar.

Iniciamos un bucle que se ejecuta a menos que lo salgamos explícitamente usando break más
adelante. Para esto usamos [Link](). Esta sencilla función no hace más que inicializar la
potencia con el valor 3 y sumar 2 en cada ronda, de modo que la potencia tenga los valores 3, 5, 7,
9,... como prescribe la fórmula que se muestra arriba. Para preparar el siguiente término dividimos el
anterior entre z2. Así, por ejemplo, pasamos de μ/z a μ/z3, para aumentar el exponente en el
denominador en 2. Sigue una comprobación: si el término es menor que la potencia, podemos
detener el cálculo, ya que entonces se obtiene un número menor. Se crea menos de 1, que se
redondea a 0. Esto lo vemos en la siguiente línea: aquí multiplicamos el término por el signo actual y
luego dividimos por la potencia, de modo que obtenemos de μ/z3 a μ/(3z3). Luego se suma al
resultado general. Luego invertimos el signo y el bucle comienza de nuevo.

Una vez que hayamos salido del bucle, sólo nos queda eliminar del resultado global los lugares
significativos creados adicionalmente. Logramos esto usando división simple. Cuando se aplica,
sólo tenemos que acordarnos de especificar el valor deseado como fracción de barrido. Entonces,
si se requiere el resultado de 1/5, insertamos 5 en la función. La parte fraccionaria se devuelve
como un número entero. Con esta función y la fórmula de Machin ahora podemos calcular Pi.

def pi(dígitos):
devolver 4 * (4 * arctan(5, dígitos) arctan(239, dígitos))
Machine Translated by
Google 33
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

El resultado es Pi como un número entero, sin el separador decimal.

>>> pi(30)
3141592653589793238462643383268

Asignaciones

1. Calcula los primeros 2500 lugares de Pi y mide el tiempo. Repetir para los primeros 5.000
lugares. ¿Qué notas sobre el tiempo de ejecución?
2. Calcule los primeros 20.000 lugares de Pi y guárdelos para buscarlos. ¿Encuentra su fecha de nacimiento,
código postal o número de teléfono?
3. El número de Euler e (2,718281828...) se define de la siguiente manera:

Cree una función para calcular este número con precisión arbitraria.

Apéndice: Mayor precisión con decimales

Hay una segunda forma de manejar más decimales en Python que invoca el uso de un módulo adicional:
simplemente importe decimal y especifique cuántos decimales necesitamos.
Los números ya no se consideran flotantes, sino objetos independientes con propiedades similares. Internamente,
el decimal funciona de forma muy parecida al ejemplo anterior. La desventaja es que no podemos simplemente
convertir los números decimales existentes en decimales, porque los flotantes ya están limitados y la precisión
faltante no se puede simplemente "sumar". Sin embargo, si toma números enteros como punto de partida, la
precisión se producirá como se desee. Veamos un ejemplo.

>>> 1/3 #precisión regular


0.3333333333333333

>>> desde importación decimal *


>>> obtener contexto().prec = 25
>>> a = Decimal(1) / Decimal(3)
>>> un
Decimal('0.3333333333333333333333333')
>>> tipo(a)
<clase '[Link]'>
>>> Decimales(1/3) #¡Precaución!

34
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Importamos el módulo y configuramos la precisión en 25 dígitos. Como podemos ver, esto funciona bien: la
precisión es mayor. También está claro que se trata de un nuevo tipo de datos. Sin embargo, si desea
convertir flotantes ya existentes, obtendrá resultados sin sentido. Ahora podemos realizar varias operaciones
matemáticas con estos objetos, pero deben estar disponibles en el módulo.

>>> un
Decimal('0.3333333333333333333333333')
>>> [Link]()
Decimal('0.5773502691896257645091488')
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801689')
>>> Decimal(2).exp()
Decimal('7.389056098930650227230427')
>>> Decimal(2).ln()
Decimal('0.6931471805599453094172321')

La documentación explica exactamente qué comandos están disponibles y cómo usarlos.6


En resumen, el módulo es extremadamente útil para cálculos utilizando números muy precisos pero
requiere la creación de sus propias funciones y métodos si desea resolver tareas más complejas. En este
sentido, siempre debes considerar si deseas utilizar decimales o si puedes encontrar una solución
manejando hábilmente los números enteros.

2.5 • Cuenta atrás

Antes vimos cómo se pueden acelerar las funciones recursivas almacenando en caché los resultados
anteriores y usándolos para calcular elementos adicionales (memorización). Esto también funciona en
contextos más complejos y puede conducir a aumentos de velocidad verdaderamente gigantescos. El
siguiente ejemplo puede parecer inofensivo al principio, pero es bueno: tome un número entero n, que debe
reducir al valor 1 con la menor cantidad de operaciones aritméticas posible. Hay tres operaciones
disponibles: dividir por 2, dividir por 3 y restar 1. Por supuesto, las divisiones sólo se podrán realizar si el
resultado vuelve a ser un número entero al final. Tomemos el ejemplo de 5: Para reducir este número a 1,
podríamos restar 1 cuatro veces seguidas, es decir, a 4, 3, 2 y finalmente 1, que son cuatro operaciones en
total. ¿Podemos hacerlo mejor? Sí, primero restamos 1 y obtenemos 4, luego dividimos por 2 dos veces, por
lo que hemos realizado la tarea con solo 3 operaciones. No existe una solución más rápida para 5, solo
soluciones equivalentes (resta 1 dos veces y luego divide por 3). Esta tarea es perfecta para un programa
recursivo: tomamos el número inicial y probamos las 3 operaciones.
Así obtenemos un máximo de 3 números nuevos. Luego volvemos a aplicar el algoritmo a cada uno de estos
números y mantenemos un registro del número total de operaciones y las secuencias resultantes.
Al final, elegimos la variante que menos operaciones ha necesitado. El código para esta tarea es
bastante claro:

6 [Link]/3.6/library/[Link]

• 35
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def cuenta atrás1(n, contador=0, secuencia=""):


si norte == 1:
retorno (contador,
secuencia) contador += 1
resultados = []
si n % 2 == 0:
[Link](countdown1(n // 2, contador, secuencia +
"2")) si n % 3 == 0:
[Link](cuenta regresiva1(n // 3, contador, secuencia + "3"))
[Link](cuenta regresiva1(n 1, contador, secuencia + "1"))
devolver min(resultados)

Nuestra función tiene un solo argumento, es decir, el número entero que queremos procesar. Sin embargo, dado
que llamamos a la función repetidamente en la recursividad, aquí especificamos algunos valores predeterminados
que luego podemos reemplazar en llamadas posteriores. Esta es la variable contador, que almacena cuántas
operaciones ya hemos realizado, y la cadena de secuencia, que mantiene el orden y el tipo de operaciones
realizadas.

Primero, definimos el caso base, que es la condición que finaliza la recursividad. Aquí es cuando se alcanza 1,
luego el contador y la secuencia se devuelven como salida. Si el número actual sigue siendo mayor que 1, el
algoritmo se ejecuta normalmente. Incrementamos el contador actual en 1 y creamos una lista vacía en la que
recopilamos los resultados. Como tenemos 3 posibilidades (dividir entre 3, dividir entre 2 y restar) tenemos que
considerarlas todas. Ahora, si es posible una división entre 2, aplicamos el algoritmo a ese número nuevamente y
agregamos "2" a la secuencia actual para que sepamos más tarde que se realizó esta operación. El procedimiento
para dividir por 3 es similar y, como la resta siempre es posible, podemos omitir la prueba aquí. Finalmente,
obtenemos hasta 3 tuplas en la lista. Luego seleccionamos la tupla que tiene el valor más pequeño en el primer
elemento, es decir, el contador más pequeño. Veamos ahora esto en acción.

36
Machine Translated by
Google
Capítulo 2 • Trabajar con números

>>> tiempo de importación

>>> para k en el rango(20, 320, 20):


>>> tstart = [Link]ónico()
>>> k, cuenta atrás1(k), ronda([Link]() tstart, 3)

20 (4, '2133') 0,0


40 (5, '22133') 0,001
60 (5, '23133') 0,002
80 (6, '222133') 0,007
100 (7, '1331133') 0,016
120 (6, '223133') 0,042
140 (9, '113133113') 0,073
160 (7, '2222133') 0,132
180 (6, '233133') 0,228
200 (8, '21331133') 0,379
220 (7, '2212333') 0,614
240 (7, '2223133') 0,948
260 (9, '213123123') 1,429
280 (8, '13313133') 2,12
300 (8, '22312223') 3.059

Primero deberíamos comprobar si el algoritmo funciona según lo previsto. A partir de 20, obtenemos la
siguiente secuencia: 20 → 10 → 9 → 3 → 1. Esto está bien. Sin embargo, cuando miramos los tiempos
de ejecución, hacemos un descubrimiento alarmante. Si bien estos son extremadamente cortos al
principio, aumentan rápidamente. Por ejemplo, necesitamos menos de 0,02 segundos para 100, pero
casi 3,1 segundos para 300. Si el número se triplica, ¡el tiempo de ejecución aumenta en un factor de
190! 500 ya lleva casi un minuto, lo que hace fácil ver que números mayores probablemente eludirán el
cálculo. ¿Como puede ser? Cuanto mayor sea el número, más posibilidades se probarán y para cada
posibilidad nuevamente hasta tres posibilidades, y así sucesivamente. Además, no almacenamos nada,
muchas secuencias se calculan dos veces. Por ejemplo, si terminamos con 50, calculamos el resultado,
pero otras secuencias recursivas no se benefician de esto. Si también llegan a 50 de otra manera,
tendrán que repetir el cálculo en lugar de utilizar el resultado conocido. Esta es una seria desventaja.
Para visualizar gráficamente el problema, veamos un ejemplo, aquí el número 9.

37
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Figura 2.2: Árbol de búsqueda recursiva para el número inicial 9

Para el número 9 hay dos opciones, dividir por 3 y restar 1. Las mismas reglas se aplican una y otra
vez, es decir, de forma recursiva, a los resultados. Los extremos u hojas del árbol son siempre 1,
nuestro caso base. Aquí podemos ver claramente el problema: el número 3, por ejemplo, se alcanza
cuatro veces de forma independiente. Por lo tanto, cada vez se debe generar de nuevo el árbol de
búsqueda completo para este número. Si ahora tenemos números mayores, se crean árboles de
búsqueda gigantescos, en los que hay que completar las mismas tareas una y otra vez. Esto ralentiza
considerablemente la búsqueda. La solución al problema es mantener en la memoria los árboles de
búsqueda generados previamente y recuperar los resultados dinámicamente cuando se vuelvan a ejecutar.
Supongamos que primero se crea la rama izquierda del árbol y que el resultado de 3 ya está
disponible. Si el algoritmo vuelve a encontrar 3, por ejemplo al dividir 6 entre 2, simplemente se
devuelve el resultado conocido en lugar de iniciar otra búsqueda recursiva. Entonces necesitamos
una subfunción que se llame recursivamente una y otra vez, pero al mismo tiempo, queremos
mantener una parte estática que almacene los resultados conocidos (de las recursiones anteriores o
paralelas). Podemos lograr esto usando un contenedor.

38
Machine Translated by
Google
Capítulo 2 • Trabajar con números

def cuenta atrás2(n):


libro = {1: (0, "")}
definición interior(n):
si n en el libro:
libro de devolución[n]
resultados = []
si n % 2 == 0:
contador, secuencia = interno(n // 2)
[Link]((contador + 1, "2" + secuencia))
si n % 3 == 0:
contador, secuencia = interno(n // 3)
[Link]((contador + 1, "3" + secuencia))
contador, secuencia = interno(n 1)
[Link]((contador + 1, "1" + secuencia))
libro[n] = min(resultados)
libro de devolución[n]
devolver interior(n)

Llamamos a nuestra función cuenta regresiva2 y solo especificamos un argumento. Por qué esto es así
será más obvio en un momento. Definimos un dict que al principio sólo contiene nuestro caso base, es
decir, el final de la recursividad. Si se alcanza 1, se devuelve una tupla que contiene el número de pasos
(0) y la secuencia más corta (cadena vacía). Ahora definimos otra función dentro de countdown2(), a la que
llamamos internal(). La idea básica es la siguiente: si se realiza una autollamada recursiva, se llama a la
función interna. Nuestra base de datos, que creamos en un libro, se mantendrá y se ampliará continuamente.
De esta forma, nuevas instancias de la función pueden acceder a los resultados ya calculados y así guardar
cálculos duplicados.

De esta forma podemos comprobar directamente si el número n a probar ya tiene resultado.


Si está disponible, el resultado se devuelve inmediatamente. De lo contrario, se inicia el algoritmo recursivo.
Con los resultados, creamos una lista vacía para almacenar los cálculos y verificar qué operaciones
aritméticas se aplican a n. Por ejemplo, si es posible una división entre 2, iniciamos una llamada recursiva
con el nuevo número (n // 2). Desempaquetamos el resultado (devuelto como una tupla) directamente al
contador y secuencia de variables deseadas y luego podemos procesarlos más.
Sólo necesitamos aumentar el contador en 1 y asegurarnos de agregar el nuevo paso aritmético a la
secuencia de operaciones.

Supongamos que nuestro número actual a probar es 8 y, por lo tanto, divisible por 2. La función primero
verifica si el siguiente número (4) a probar ya existe en el dict. Si este es el caso, encontramos un resultado
conocido y podemos recuperarlo. El resultado que encontramos sería entonces (2, "22") ya que se
necesitan 2 pasos para llegar de 4 a 1, la secuencia indica que esto se logra mediante dos divisiones con
2. Como ya sabemos 4, pero aún no 8, ahora debemos aprovechar este resultado. Necesitamos otro paso
(es decir, de 8 a 4 dividiendo por 2), así que aumenta el contador en 1. Además, tenemos que sumar el
paso necesario
al resultado conocido. Aquí tenemos que prestar atención al orden. Como "22" ya existe, tenemos que
insertar el nuevo paso delante, ya que la parte trasera describe el camino restante hasta 1, no podemos
influir en esto.

39
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

ya no. Por lo tanto, aquí configuramos "2" + secuencia . El mismo procedimiento se utiliza para las otras dos
opciones. De este modo obtenemos hasta tres posibles "destinos" para nuestro número actual. Finalmente
solo nos queda comprobar cuál es la mejor opción. Los agregamos al dictado para que otras recursiones
también puedan usar este nuevo resultado. Después de esto, la función devuelve el resultado. Para iniciar la
recursividad, llamamos a la función internal() y la dejamos regresar.

Resumamos la lógica nuevamente. Llamamos a la función countdown2() con un número a probar, digamos 10. En la
función misma, creamos los parámetros o variables que almacenan nuestros resultados. Luego entregamos el número
10 a la función internal(). Comienza la recursividad.
Dado que 10 no existe en el libro, todos los candidatos posibles, en este caso, 9 y 5, se definen como números nuevos y
se inician nuevos ciclos de recursividad para ellos. Tan pronto como una de las ramas del árbol de búsqueda arroja un
resultado para un número, se almacena permanentemente en el libro y las demás recursiones tienen acceso a él. Esto
acelera enormemente la búsqueda.
¿Vale la pena? Hagamos algunos números.

>>> importar sistema


>>> [Link](15000)

>>> tstart = [Link]ónico()


>>> para i en (500, 2000, 5000):
>>>
yo, cuenta atrás2(yo)
>>>
[Link]ónico() tstart
500 (9, '213113333')
2000 (10, '2133312233')
5000 (13, '2221222231223')

Ahora incluso las cifras más grandes se analizan en una fracción de segundo. Debemos aumentar el número máximo de
recursiones permitidas. Python tiene que generar muchos de ellos en este punto, lo que puede generar un mensaje de
error. Con esta configuración permitimos que se inicien más recursiones. En principio, sólo el rendimiento de su sistema
limita el número de recursiones potenciales. Sin embargo, si se desean cálculos para números aún mayores, puede ser
necesario cambiar a otro método. Por lo tanto, una solución recursiva no siempre es la mejor manera, pero puede resultar
muy elegante si las condiciones básicas son correctas. Finalmente, queremos comparar las dos funciones, cuenta
regresiva1() y cuenta regresiva2(), un poco más de cerca. Hasta ahora conocemos los tiempos de ejecución aproximados,
pero ¿qué pasa internamente? Para poder realizar tales análisis, Python ofrece herramientas para la creación de perfiles.
Esto significa dividir un comando, función o script en sus partes y verificar con qué frecuencia se llama a un determinado
bucle o subfunción. Esto facilita ver qué partes son lentas y merecen más atención. Usamos cProfile en este punto porque
es muy fácil de usar.

40
Machine Translated by
Google
Capítulo 2 • Trabajar con números

>>> importar cPerfil


>>> [Link]("cuenta regresiva1(30)")
1222 llamadas a funciones (421 llamadas primitivas) en 0,000 segundos

Ordenado por: nombre estándar

ncalls tottime percall cumtime percall nombre de archivo:lineno(función)


1 0.000 0.000 0.000 0.000 <cadena>:1(<módulo>)
802/1 0.000 0.000 0.000 0.000 cuenta [Link](cuenta regresiva1)
1 0.000 0.000 0.000 0.000 {método incorporado [Link]}
417 0.000 0.000 0.000 0.000 {método incorporado [Link]}
1 0.000 0.000 0.000 0.000 {método 'deshabilitar' de '_lsprof.
Objetos del perfilador}

Primero debe asegurarse de pasar la función a probar como una cadena a cProfile; de lo contrario, recibirá un
mensaje de error. Podemos ver que se llamaron un total de 1222 funciones, 421 de las cuales son primitivas, es
decir, no activadas por una recursividad. Ya podemos ver aquí que la mayoría de las funciones fueron creadas por
recursividad. Más abajo vemos que se llamó recursivamente a countdown1() 802 veces. El otro número grande,
417, proviene de la función min(), que utilizamos para ordenar las listas. Aunque los tiempos de ejecución son en
general tan rápidos que no se pueden medir, muestra lo que realmente sucede detrás de escena. Entonces, ¿qué
pasa con la versión mejorada?

>>> importar cPerfil


>>> [Link]("cuenta regresiva2(30)")
62 llamadas a funciones (34 llamadas primitivas) en 0,000 segundos

Ordenado por: nombre estándar


ncalls tottime percall cumtime percall nombre de archivo:lineno(función)
1 0.000 0.000 0.000 0.000 <cadena>:1(<módulo>)
1 0.000 0.000 0.000 0.000 cuenta [Link](cuenta regresiva2)
29/1 0.000 0.000 0.000 0.000 cuenta [Link](interior)
1 0.000 0.000 0.000 0.000 {método incorporado [Link]}
29 0.000 0.000 0.000 0.000 {método incorporado [Link]}
1 0.000 0.000 0.000 0.000 {método 'deshabilitar' de '_lsprof.
Objetos del perfilador}

En la versión mejorada sólo se activan 62 funciones, lo que supone casi 20 veces más que en la versión ingenua.
Si bien no podemos ver cuánta memoria adicional estamos usando porque ahora tenemos que mantener el libro
en la memoria, esto parece ser mucho mejor, ya que podemos estimar que la sobrecarga generada por cada nueva
recursión será mucho mayor que los datos adicionales. en el dict.

41
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Asignaciones

Resuelva la tarea analizada en este capítulo sin utilizar ninguna forma de recursividad. Compare el
tiempo de ejecución de su solución con countdown2(). ¿Cuál es tu conclusión?

2.6 • Plato en espiral

La Espiral, que lleva el nombre de su descubridor Stanisław Marcin Ulam, es una representación
gráfica de los números primos. La idea es muy simple: escribir los números enteros, comenzando
con 1, en espiral, y marcar todos los números primos al final. Si haces esto el tiempo suficiente y
miras la imagen resultante desde la distancia, se crean patrones interesantes.

Figura 2.3: Visualización de la espiral de Ulam: creada por Aydolen (Wikimedia


Commons)

Siempre que desee limitarse únicamente a la salida de la consola, sin paquetes adicionales, esta tarea
no es factible en Python. Por tanto, en este punto implementaremos el primer paso, es decir, la
construcción de la espiral.

Primero, defina la notación. No importa cuántos números queramos representar al final, podemos
imaginar la posición de cada número en un sistema de coordenadas cartesiano. Al primer número (1)
en el centro de la espiral se le dan las coordenadas (0, 0). Este orden es útil para nuestra imaginación
pero no es útil para la implementación. Si queremos almacenar datos en una matriz, es decir, una lista
con sublistas, tenemos que definir el número de filas y columnas necesarias al principio. Sin embargo,
estos valores sólo pueden estar entre 0 yn, de modo que, a diferencia del sistema de coordenadas, no
son posibles valores negativos. Por lo tanto, necesitamos una función que convierta las diferentes
coordenadas entre sí. A modo de ilustración, podemos utilizar la siguiente figura.

42
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Figura 2.4: Visualización de las diferentes estructuras de datos dentro de la espiral, incluidos los primeros 22 números enteros

El primer número de cada celda representa el número entero, seguido de las coordenadas en el
sistema cartesiano, y el cuadro central representa el origen. La tercera información describe la
posición de la celda en nuestra matriz de datos, es decir, una lista con sublistas. Primero, tenemos
que seleccionar cuántos números se deben asignar (n). Luego generamos una lista. El número de
sublistas especifica el número de filas de la matriz. La longitud de cada sublista especifica el número
de columnas. El número de filas y el número de columnas deben ser idénticos. Sin embargo, se
necesitan menos de n filas o columnas, ya que los números se agrupan inicialmente en el centro de
la espiral y crecen lentamente hacia los bordes. Por lo tanto, debemos asegurarnos de que el primer
número se cree en la fila y columna del medio de la matriz. Para ello utilizamos la siguiente función
de conversión:

def cart_to_matrix(posición, tamaño):


"""Convierte una posición del sistema cartesiano a la listamatriz"""
columna = (tamaño // 2) + posición[0]
fila = (tamaño // 2) posición[1]
retorno (fila, columna)

Como entrada, la posición cartesiana se pasa como una tupla (por ejemplo, (0, 0)), así como el
número de filas o columnas tal como las hemos definido. La división de números enteros (//) asegura
que la tupla siempre se redondee y se proporcione la posición correcta. Por ejemplo, si tenemos cinco
filas y columnas, el punto medio es la tercera fila con la tercera columna. Dado que Python comienza
a contar desde cero, el valor del índice 2 es correcto (5 // 2 es 2 ya que los flotantes se redondean
hacia abajo mediante la división de enteros). El próximo desafío del programa es encontrar la
siguiente posición en la espiral. Siempre queremos ir en el sentido de las agujas del reloj. La idea
básica es simple: dado que es posible un máximo de tres campos posteriores (ya que no se puede
volver al campo anterior y los movimientos diagonales están prohibidos), sólo necesitamos comprobar
cuál de
los tres campos adyacentes sigue vacío y también está más cerca de el origen. Esto evita salir de la espiral.

43
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def next_position(datos, posición):


vacío = []
# Posiciones abajo, derecha, arriba e izquierda
# El orden es relevante para que las esquinas se traten correctamente
para x, y en [(0, 1), (1, 0), (0, 1), (1, 0)]:
px, py = posición[0] + x, posición[1] + y
pos = cart_to_matrix((px, py), len(datos))
si datos[pos[0]][pos[1]] == "":
vací[Link]((px, py, px ** 2 + py ** 2))
devolver min(vacío, clave=lambda f: f[2])[:2]

La función acepta dos argumentos, la matriz de datos y la posición actual. Inicializamos una lista
vacía en la que almacenamos los resultados. Ahora podemos procesar los cuatro campos posibles
de forma secuencial. Aquí podemos repasarlos todos explícitamente ya que sólo hay cuatro. El
orden también es importante. Empezamos desde abajo y luego vamos en sentido antihorario. ¿Por
qué?
Lo veremos en un momento. Luego calculamos las nuevas coordenadas y las convertimos a la
posición de la matriz usando la función auxiliar definida anteriormente. Luego comprobamos si el
campo respectivo está vacío. Si es así, lo sumamos al vacío y también calculamos la distancia al
origen usando el teorema de Pitágoras. Finalmente, ordenamos todos los elementos en vacío por
esta distancia y seleccionamos el elemento con el valor más pequeño. Esto garantiza que se
seleccione el campo correcto y que se conserve la forma de espiral. Al final, devolvemos sólo la
posición, es decir, cortamos el tercer valor, la distancia, del resultado, para lo cual usamos un corte ([:2]).

Veamos un ejemplo de para qué podemos usar la Figura 2.4. Ahora estamos en el campo 9 y
aparentemente hay dos campos vacíos adyacentes: a la izquierda y arriba. Observa que la distancia
al origen es la misma para ambos campos, por lo que debemos tener cuidado al seleccionar el
correcto (el superior). Aquí es donde entra en juego la clasificación, como se mencionó al principio.
Dado que probamos los campos en el sentido contrario a las agujas del reloj, el campo superior
viene antes que el izquierdo y, por lo tanto, se selecciona. Esto garantiza que no nos desviaremos
hacia la izquierda. Al final resulta evidente que la distancia al origen o, en estos casos límite, la
clasificación garantizan que nuestra espiral continúe como deseamos. Este caso especial sólo
ocurre cuando se alcanzan las
esquinas superiores izquierdas de la espiral, el siguiente campo sería el número 25. Ahora sigue el programa
principal.
Machine Translated by
Google
44
Machine Translated by
Google
Capítulo 2 • Trabajar con números

plato definido (n):


tamaño = máx(15, (int(n ** 0.5) // 2) * 2 + 11)
datos = [[""] * tamaño para i en
rango(tamaño)] i = carrito_a_matriz((0, 0),
tamaño) datos[i[0]][i[1]] = 1
i = carrito_a_matriz((0, 1),
tamaño) datos[i[0]][i[1]] = 2
posición = (1, 1)
para contador en rango (3, n + 1):
a = cart_to_matrix(posición, tamaño)
datos[a[0]][a[1]] = contador
posición = siguiente_posición(datos, posición)
print_field(datos)

Nuestra función acepta un argumento, la longitud de la espiral. Especificamos el tamaño de la matriz de


datos en tamaño. Para ahorrar espacio, lo hacemos en una sola operación: o n es pequeño y establecemos
el tamaño en 15; Sin embargo, si n es grande, utilizamos un algoritmo de estimación para que nuestra matriz
de datos no sea demasiado pequeña y se "desborde". Después de determinar el tamaño, generamos la
matriz vacía. Luego creamos manualmente los dos primeros números, que en la vista cartesiana obtienen los
valores (0, 0) y (0, 1). La siguiente posición es (1, 1) porque siempre vamos en el sentido de las agujas del
reloj. A partir de aquí, el siguiente bucle se hace cargo y crea todos los números siguientes. Guardamos la
posición en un usando la función de ayuda y escribimos el siguiente número en esta celda. De esta manera,
la matriz de datos se llena gradualmente con números hasta que hayamos procesado todos los números
hasta n. El último paso es desplegar nuestra espiral. Para ello utilizamos otra función de ayuda.

def print_field(datos):
tamaño = longitud (datos)
print("".join(["*" para i en el rango(tamaño * 4)]))
para fila en datos:
para elemento en fila:
si elemento == "":
imprimir(" " * 4, fin = "")
#Exactamente 4 espacios
demás:

print( f"{elemento:02d} ", end = "") #Espacio \


antes de la cuerda f
imprimir("")

En esta función solo debemos ingresar la matriz de datos. En la parte superior e inferior del campo,
colocamos un delimitador por razones ópticas. Primero, creamos una lista con los delimitadores
deseados y luego usamos join() para combinarlos en una cadena y generarla. Luego iteramos sobre
todas las filas y dentro de una fila sobre todas las columnas. Si encontramos una celda vacía que no
contiene un número, mostramos exactamente cuatro espacios. Modificamos print() con la opción end
para que después de mostrar cada carácter, no salte inmediatamente a la siguiente línea. si nos
encontramos

45
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

un número, usamos una cadena f para crear una visualización más agradable. Dado que en este
ejemplo nos limitamos a números de dos dígitos, mostraremos un 0 inicial para los números de un
dígito, es decir, 09 en lugar de 9. De esta manera, todos los números se alinean al final y se crea
una bonita espiral. . Si no hiciéramos esto, las filas a veces se deslizarían debido a que algunos
números se muestran con solo tres caracteres en total. Esto no sería agradable de ver. Una vez
terminada una línea, ahora tenemos que insertar un salto de línea, lo que logramos simplemente
mostrando una cadena vacía. De lo contrario, todas las sublistas de la matriz de datos se mostrarían
en una línea, lo cual no queremos. Finalmente, se inserta una línea separadora, que completa la
función. Al final, el resultado es impresionante, aquí usando el ejemplo de los primeros 55 números enteros.

>>> plato(55)
************************************************** *********

50 51 52 53 54 55
49 26 27 28 29 30 31
48 25 10 11 12 13 32
47 24 09 02 03 14 33
46 23 08 01 04 15 34
45 22 07 06 05 16 35
44 21 20 19 18 17 36
43 42 41 40 39 38 37

************************************************** *********

2.7 • Caos total

Algunas cosas son más profundas de lo que parecen. Por ejemplo, solemos asociar las matemáticas con
fórmulas, reglas y orden. Pero como se muestra aquí, incluso los algoritmos bastante inofensivos pueden
degenerar rápidamente en caos. Veamos primero un modelo simple que se puede utilizar, por ejemplo, para
describir cómo cambia una población con el tiempo.

Xn+1 = rxn(1xn)

Donde x es un valor entre 0 y 1 y describe la proporción de la población actual.


Por lo tanto, un valor alto significaría que la población casi ha alcanzado su tamaño máximo. También utilizamos
un factor de escala y crecimiento r, que indica si la población está aumentando o disminuyendo. Suponiendo que
r sea 2, la población se duplicaría cada año. Este

46
Machine Translated by
Google
Capítulo 2 • Trabajar con números

daría lugar a una población en constante crecimiento, lo cual no es realista ya que el hábitat y el suministro de alimentos son
limitados. Para evitar esto, se introduce el último término para reflejar las limitaciones del entorno. Cuanto mayor sea x, menor
será el factor y, por tanto, el valor para el año siguiente. Veamos un ejemplo. Como valor inicial para x, elegimos 0,7 y como tasa
de crecimiento 2. ¿Cómo cambia la población con el tiempo? El resultado es:

0,7
0,42
0.4872
0.4997
0,5
0,5
0,5

Así, la población primero se reduce, luego vuelve a crecer y se estabiliza en un valor de 0,5, es decir, la
mitad de la población máxima. ¿Qué sucede si ahora comenzamos con una población mucho más
pequeña, digamos 0,2? Obtenemos el siguiente desarrollo:

0,2
0,32
0.4352
0.4916
0,4999
0,5
0,5
0,5

Sorprendentemente, la población también se encamina muy rápidamente hacia un equilibrio idéntico. Es completamente irrelevante
con qué x comenzamos, el destino está determinado solo por r. Podemos probar esto. Para un cálculo, utilizamos la siguiente
función.

def fórmula del caos(x, r, n, prec):


para i en el rango (n):
imprimir (redondo (x, prec))
x = x * r * (1 – x)

Si ahora aumentamos lentamente el valor de r, observamos que los resultados tardan más en estabilizarse, es decir, en
converger hacia un valor límite. Lo que es muy sorprendente, sin embargo, es que este comportamiento cambia cuando r se
hace aún mayor y excede 3: de repente hay dos límites. Comparemos el resultado para r = 2,8 y r = 3,1:

47
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

(...)
0,6425
0.6431
0.6426
0,643
0.6427
0,643
0.6428
0.6429
0,6428
0.6429
0,6428
0.6429
0.6428
0.6429
0.6428
0.6429
0.6428
0.6429
0.6429
0.6429

Después de unas 30 iteraciones, el valor se estabiliza y alcanza 0,6429. Parece sorprendente que los
mismos valores tengan sucesores diferentes (por ejemplo, para 0,643). Esto se debe a que aquí se
muestran valores redondeados. Internamente, por supuesto, se utiliza la máxima precisión para los números
de punto flotante. Por tanto, este comportamiento, aunque no sea muy agradable, no debería sorprender demasiado.
Ahora calculemos la secuencia para 3.1:

(...)
0.7647
0.5578
0.7646
0.5579
0.7646
0.5579
0.7646
0.558
0.7646
0.558
0.7646
0.558
0.7646
0.558
0.7646

48
Machine Translated by
Google
Capítulo 2 • Trabajar con números

0.558
0.7646
0.558

Encontramos que efectivamente hay dos límites entre los cuales oscila la secuencia. La diferencia es grande y
asciende a más de 0,2. Con una dimensión de este tipo no es posible un error de redondeo.
De hecho, hay dos valores diferentes que se alternan, sin importar cuántos decimales tengamos en cuenta o
cuánto tiempo dejemos correr la secuencia. ¿Continuará creciendo el número de puntos distintos a medida que
aumenten los valores de r? Sí, pero caóticamente. Esto significa que a partir de cierto punto, incluso cambios
muy pequeños en r conducirán a una cantidad enormemente fluctuante de puntos de convergencia. Veamos
primero el proceso de convergencia para algunos valores seleccionados de r (ver figura 2.5). ¿Cómo podemos
determinar cuántos puntos de convergencia producirá un valor dado de r?

Figura 2.5: Desarrollo de valores de x utilizando diferentes valores para r. El valor inicial de x es siempre 0,5

La idea es la siguiente: comenzamos como antes con la fórmula conocida y primero generamos un cierto número
de iteraciones para asegurarnos de haber alcanzado un punto donde la secuencia es estable, es decir, alterna
entre los mismos elementos. Si elegimos un r pequeño, este posiblemente será un único punto de convergencia;
a partir de valores más grandes para r, puede haber dos o muchos más límites de este tipo. Entonces, cuando
hayamos terminado las primeras iteraciones, a las que nos referimos como burnin, almacenamos todos los
valores recién calculados junto con la iteración en la que se crearon. Para cada elemento posterior, simplemente
verificamos si ya existe el mismo valor. Si

49
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

este es el caso, sabemos que un ciclo está completo. Entonces sólo necesitamos comprobar después de cuántas
iteraciones ha sucedido esto y sabemos el número de puntos de convergencia. Como ayuda, convertimos la
función que se muestra arriba en un generador para que podamos dejarla ejecutar tantas veces como sea necesario.

def logística(x, r):


mientras que Verdadero:

rendimiento x

x = x * r * (1 x)

desde itertools importar islice


def buscador de ciclos(x, r):
números = logístico(x, r)
# saltar el primer millón de iteraciones
números = islice(números, 10**6, Ninguno)
visto = {}
para iteración, x en enumerar (números):
para el elemento visto:

si abs(elemento x) < 1e6:


iteración de retorno visto[elemento]
visto[x] = iteración

El generador aplica la fórmula, pero se ejecuta con tanta frecuencia como queramos, lo cual necesitamos en la función real,
Cyclefinder(). También importamos islice de itertools. La función acepta dos argumentos: x y r. En números inicializamos el
generador, al que luego podemos llamar.
Ahora queremos quemar este generador. Esto lo llamará un millón de veces, lo que llevará menos de un segundo. De esta
forma conseguimos que las secuencias iniciales inestables se salten y no afecten al resultado. Cuanto mayor sea r, más largo
debe ser el período de precalentamiento. La implementación técnica se realiza mediante islice. Al igual que con un corte
normal, cortamos un área determinada de un iterable. Pero como nuestro generador es inagotable, tenemos que utilizar islice.
Especificamos que queremos la región del generador que comienza en un millón de ejecuciones. Dado que configuramos
Ninguno como argumento final, tomamos la porción desde un millón hasta el final del generador.

Ahora creamos un dictado vacío en el que almacenamos todos los resultados. Implementamos la idea de solución explicada
anteriormente. Repetimos todos los elementos posteriores en números y también empaquetamos este iterador en
enumerate(). De esta forma obtenemos una tupla con dos elementos, la iteración actual y el valor real para cada nueva
solicitud. En la iteración almacenamos la llamada actual (que comienza en 0), en x el valor de retorno del generador. Una vez
que hemos creado esta tupla, iteramos sobre todas las entradas vistas y verificamos si el valor x creado actualmente ya está
allí. Como estamos trabajando con flotadores, probamos la igualdad por una diferencia. Si esta diferencia es muy pequeña
(menos de una millonésima), consideramos que los números son iguales. Si este es el caso y se encuentra un elemento ya
conocido, devolvemos la diferencia entre la iteración actual y la iteración donde se encontró el elemento conocido. De esta
manera determinamos el período. Si, por otro lado, el valor actual x aún no está presente, lo agregamos y guardamos la
iteración actual con él.

• 50
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Si jugamos con esta función, descubrimos que el valor 3 es una discontinuidad de salto: antes de esto,
todos los valores convergen a un límite. Si los valores son mayores que 3, existen al menos dos límites.
Esto ya está validado matemáticamente, por lo que podemos utilizar este límite como puntos de referencia.7
Según esto, el primer punto de salto se encuentra exactamente en 3. El segundo en 3.44948974... En
este punto, el número de límites cambia de 2 a 4, y hay una visualización fascinante de este desarrollo
llamado mapa logístico.

Figura 2.6: En el eje x se muestran los valores de r, en el eje y el valor hacia el que converge la secuencia.
Creador: PAR (Wikimedia Commons)

Visto de izquierda a derecha, al principio ocurre muy poco, la secuencia siempre converge
exactamente hacia un valor. Esto cambia en el primer punto de salto (3), a partir de ahí el gráfico
se divide y hay exactamente dos valores alternos. Luego hay otro punto de salto y ahora hay cuatro
valores. Luego cae en el caos. Sin posibilidad alguna de predicción, a partir de ahí el número varía
aparentemente de forma arbitraria, de modo que surgen estos interesantes patrones. La siguiente
tarea es la siguiente: ¿Cómo podemos determinar numéricamente un punto de salto, por ejemplo,
si no tenemos la figura que se muestra arriba? Una idea de solución es la siguiente: bajamos
lentamente el eje x de izquierda a derecha, es decir, elegimos valores cada vez mayores de r. En
determinados puntos comprobamos entonces cuántos valores límite se pueden encontrar. Si este
valor cambia de un punto en adelante, sabemos que hemos alcanzado o superado un punto de
salto. Si 2,95 tiene la salida 1, pero 3,05 tiene la salida 2, está claro que el punto de salto debe
estar entre estos dos valores de r. Podemos utilizar un algoritmo iterativo que haga esto por nosotros. Aquí pod
7 [Link]

51
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

una modificación de la paradoja de Zenón: si tenemos una distancia x a un objeto y el primer día cubrimos la
mitad de la distancia, el segundo día nuevamente la mitad de la distancia restante, y así sucesivamente.
¿Cuándo llegamos al objeto? Matemáticamente hablando, nunca, ya que una repetida reducción a la mitad de un
número produce valores cada vez más pequeños, pero nunca llega a 0. Dado que Python y todas las
computadoras no pueden calcular con precisión infinita, en algún momento todavía se alcanza 0 y con él el
objetivo. Podemos aprovechar esto.

Entonces definimos un valor inicial que sabemos que está bastante cerca de la discontinuidad de salto que
estamos buscando. Luego avanzamos por un valor a. Si r1 y r2 todavía están a la izquierda de este punto,
movemos ambos hacia la derecha según el valor del paso en el eje x. Si cruzamos el punto en cualquier momento,
dividimos a y movemos el punto a la derecha de la discontinuidad de salto (r2) de regreso en la dirección opuesta.
De esta manera, sólo r2 puede estar a la derecha de la discontinuidad del salto, pero r1 nunca (ver también figura
2.7).

def buscar_discontinuidad(x, r1, precisión=4):


p1 = buscador de ciclos (x, r1)
tamaño de paso = 0,1
mientras que el tamaño del paso > 0,1 ** precisión:
r2 = r1 + tamaño de paso
imprimir(r1, r2)
p2 = buscador de ciclos (x, r2)
si p1 == p2:
r1 = r2
demás:
tamaño de paso /= 2
ronda de retorno ((r1 + r2) / 2, precisión)

La función acepta el valor de x (que siempre fijaremos en 0,5), el valor inicial de r1 y la precisión. r1 especifica el
valor a partir del cual se busca la discontinuidad. La precisión está limitada por las otras funciones, por ejemplo,
qué tan preciso es Cyclefinder() . Más adelante veremos que es bastante posible conseguir entre cuatro y cinco
decimales. Primero, calculamos el número de períodos en el valor r1 en p1. Establecemos el tamaño del paso en
0,1. El siguiente valor a verificar, r2, es r1 + tamaño de paso. Para entender esto, es útil tener una visión clara de
la nomenclatura: r1 siempre está a la izquierda de r2 en el rayo numérico o en el eje x. De manera similar, p1
indica el número de períodos en r1, p2 el número en r2. Luego ingresamos al bucle while, que se ejecuta hasta
que se encuentra el resultado. Establecemos el nuevo valor para r2 e intencionalmente dejamos un comando de
impresión en el código para luego poder reproducir las iteraciones o el proceso de convergencia contra la
discontinuidad. Calculamos p2 y luego verificamos si r1 y r2 están en el mismo lado, por lo que tienen el mismo
valor para p1 y p2. En este caso tenemos que movernos más hacia la derecha en el eje x, por lo que hacemos de
r2 nuestro nuevo r1 y luego comenzamos el ciclo nuevamente desde el principio.

Pero si este no es el caso y p1 y p2 tienen valores diferentes, dividimos el tamaño del paso entre 2 y comenzamos
el ciclo nuevamente. Por lo tanto, en la siguiente iteración, r2 volverá a estar más cerca de r1, es decir, se
deslizará hacia la izquierda en el rayo numérico. Esto queda más claro en la figura 2.7. En la iteración 9, r1 y r2
tienen períodos diferentes, por lo que en la iteración 10, r2 nuevamente se desliza hacia la izquierda. El proceso

52
Machine Translated by
Google
Capítulo 2 • Trabajar con números

continúa hasta que nos aproximamos a la discontinuidad. Probemos la función con un valor inicial de 2,7, que ya está muy cerca
del
valor conocido de 3:

>>> buscar_discontinuidad(0.5, 2.7)


2,7 2,8000000000000003
2.8000000000000003 2.9000000000000004
2.9000000000000004 3.0000000000000004
2.9000000000000004 2.9500000000000006
2,9500000000000006 3,0000000000000004
2.9500000000000006 2.9750000000000005
2,9750000000000005 3,0000000000000004
2.9750000000000005 2.9875000000000003
2,9875000000000003 3,0000000000000004
2.9875000000000003 2.9937500000000004
2.9937500000000004 3.0000000000000004
2.9937500000000004 2.9968750000000006
2.9968750000000006 3.0000000000000004
2.9968750000000006 2.9984375000000005
2.9984375000000005 3.0000000000000004
2.9984375000000005 2.9992187500000003
2.9992187500000003 3.0000000000000004
2.9992187500000003 2.9996093750000004
2.9996093750000004 3.0000000000000004
2.9996093750000004 2.9998046875000006
2.9998046875000006 3.0000000000000004
2.9998046875000006 2.9999023437500005
2.9999

Poco a poco nos acercamos al límite. La precisión que alcanzamos aquí parece adecuada para demostrar la técnica. ¿Qué pasa si
elegimos un punto de partida que está bastante alejado del límite que buscamos? Podemos probar nuestro algoritmo nuevamente
usando 3.1 como punto de partida. Este proceso se visualiza en la figura 2.7.

53
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Figura 2.7: Aquí se visualiza el proceso de convergencia para el punto de partida r = 3,1. Las primeras iteraciones
no se muestran, por lo que la escala no se altera demasiado. Como puede ver, solo hay dos casos: r1 y r2
están ambos en el lado izquierdo de la discontinuidad o r2 está solo en el derecho de este límite.

>>> buscar_discontinuidad(0.5, 3.1)


3.1 3.2
3.2 3.3000000000000003
3.3000000000000003 3.4000000000000004
3.4000000000000004 3.5000000000000004
3.4000000000000004 3.4500000000000006
3.4000000000000004 3.4250000000000007
3.4250000000000007 3.4500000000000006
3.4250000000000007 3.4375000000000004
3.4375000000000004 3.4500000000000006
3.4375000000000004 3.4437500000000005
3.4437500000000005 3.4500000000000006
3.4437500000000005 3.446875000000001
3.446875000000001 3.4500000000000006
3.446875000000001 3.4484375000000007
3.4484375000000007 3.4500000000000006
3.4484375000000007 3.4492187500000004
3.4492187500000004 3.4500000000000006
3.4492187500000004 3.4496093750000005
3.4492187500000004 3.4494140625000007

54
Machine Translated by
Google
Capítulo 2 • Trabajar con números

3.4494140625000007 3.4496093750000005
3.4494140625000007 3.4495117187500006
3.4495

Aunque el punto de partida está bastante desviado, finalmente llegamos al límite correcto. Sin embargo, cuando
aumentamos aún más los valores de r, notamos que resulta imposible encontrar discontinuidades ya que los
ciclos se vuelven caóticos y no emergen patrones claros. El algoritmo falla porque su precisión es limitada.
Cuando el sistema cae en el caos, ya no parece posible distinguir puntos de discontinuidad.

2.8 • Tres Puntos

Un problema clásico de la antigüedad, también conocido como el problema de Apolonio en honor a Apolonio de
Perge, es el siguiente: Hay tres puntos diferentes en un plano. ¿Cómo se puede construir un círculo que corte a
los tres? La siguiente figura puede servir como ilustración.

Figura 2.8: ¿Cómo podemos construir un círculo que corte los tres puntos dados?

Siempre es posible una solución si hay tres puntos distintos y no todos están en línea recta (en este caso no hay
solución). ¿Cómo se puede resolver este problema si no se conoce el algoritmo? La definición de círculo puede
servir como punto de partida. Se define por su centro, es decir, una coordenada en un plano, y su radio, que es
la distancia de todos los puntos al centro. Entonces la idea es encontrar un punto en el plano que tenga la misma
distancia a los tres puntos dados. ¿Cómo se puede hacer esto? Aquí es factible un procedimiento iterativo.

55
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Usted elige cualquier punto de partida y mide la distancia a los tres puntos, a los que nos referiremos
como A, B y C a continuación. Se puede suponer que al principio las distancias son desiguales.
Ahora se mueve ligeramente el punto seleccionado y comprobamos cómo cambian las distancias.
Si convergen, el nuevo punto es mejor. En caso contrario se deberá elegir otro punto. Este principio
estándar de optimización iterativa funciona muy bien cuando hay una medida de mejora. Pero
¿cómo determinamos si un nuevo punto nos acerca a la solución?

Este no es un problema trivial e implica varias trampas. El objetivo debe ser que las tres distancias
al final sean iguales. Por lo tanto, el valor medio de las distancias no es útil, ya que el radio a
encontrar es desconocido y el valor no permite juzgar si nos estamos acercando al verdadero
centro. Más prometedora parece ser la desviación estándar, es decir, la diferencia media de las tres
distancias con respecto al valor medio. Si la desviación estándar se vuelve cero, las tres distancias
son idénticas y se encuentra el centro. Entonces nos acercamos a la solución cuando la desviación
estándar se hace más pequeña, ¿verdad? Lamentablemente, no es tan sencillo. La desviación
estándar también puede reducirse si el punto se aleja del verdadero centro. ¿Como puede ser? Si
las tres diferencias van hacia el infinito, la diferencia entre sí se vuelve más pequeña y con ella la
desviación estándar. Al final, nuestro "centro" está infinitamente lejos de A, B y C, la desviación
estándar es cero y estamos más lejos que nunca de la solución.

Una forma que funciona es un poco más compleja y requiere algunos conocimientos de vectores y
geometría, pero debería conducir a la solución. Como se describió anteriormente, nuestro punto P
elegido tiene una distancia a cada uno de los puntos dados A, B y C, que siempre debe ser positiva.
Si ahora imaginamos un cubo, podemos trazar la distancia AP en el eje x, la distancia BP en el eje
y y la distancia CP en el eje z. La solución se encuentra cuando todas estas distancias son idénticas.
Cuando imaginamos esto en un gráfico 3D, la solución debe estar en una línea recta que corta el
origen del sistema de coordenadas y el punto (1, 1, 1). Al principio, no sabemos exactamente en
qué punto de esta línea estará la solución, pero a medida que nos acerquemos lentamente a la
línea, deberíamos avanzar. Para una visualización de esto, consulte la figura 2.9.

Figura 2.9: En los ejes, medimos la distancia entre el punto central actual y cada punto dado A, B y
C. Tan pronto como estas distancias son iguales, tocamos la diagonal y se encuentra el verdadero
centro que corta a A, B y C. Creado con [Link]

56
Machine Translated by
Google
Capítulo 2 • Trabajar con números

El objetivo debe, por tanto, ser minimizar la distancia entre el punto seleccionado actualmente y la
línea (diagonal). Cuanto más pequeña es esta diferencia, más nos acercamos a la solución. Entonces
es necesario algo de matemática. Definimos la diagonal g como una ecuación de una recta en forma
de parámetro:

Aquí b también se denomina vector de soporte. u es un vector de dirección y s es un factor de escala.


Como vector de soporte, elegimos el origen del sistema de coordenadas, por lo que tenemos que
definir u solo. Con tres dimensiones obtenemos la siguiente ecuación:

¿Cómo calculamos la distancia de un punto y una línea?

Aquí, x simboliza el producto cruzado. Las barras de valor absoluto indican que necesitamos la norma
de los vectores. El producto cruzado se define como:

Y la norma de un vector como:

57
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Ahora hemos recopilado todos los componentes matemáticos que necesitamos para el cálculo. Lo
que queda es la implementación en Python. Hacemos esto sin crear objetos o clases especiales y
usamos tuplas o listas simples para contener nuestros vectores.

norma de definición (vector):


"""Norma de un vector"""
retorno (suma(x ** 2 para x en vector)) ** 0.5

def producto cruzado(a, b):


"""producto cruzado de los vectores a y b""" afirmar
len(a) == len(b) == 3 return [a[1] * b[2]
- a[2 ] * b[1], a[2] * b[0] a[0] * b[2], a[0] * b[1]
- a[1] * b[0]]

def línea_punto_distancia(línea, punto):


"""Calcula la distancia entre una línea y un punto.
La línea se ingresa como una tupla con soporte y dirección.
El apoyo, la dirección y el punto se dan como listas con 3 elementos.
"""

soporte, dirección = línea d = [s


p para s, p en zip(soporte, punto)] return norma(producto
cruzado(d, dirección)) / norma(dirección)

Aquí utilizamos zip() para crear tuplas a partir de dos listas. Los elementos de las listas se emparejan
según sus índices. Para una explicación detallada, consideremos el siguiente breve ejemplo:

>>> x1 = [1, 2, 3] >>>


x2 = ["a", "b", "c"] >>> lista(zip(x1,
x2)) [(1, 'a') , (2, 'b'), (3, 'c')]

Como zip() crea un objeto generador, usamos list() para mostrar todos los elementos. De lo contrario,
también podríamos iterar sobre todos los elementos del generador.

def point_point_distance(x, y): """Distancia


entre dos puntos en 2D""" afirmar len(x) == len(y) ==
2 return ((x[0] y[0]) ** 2 + (x[1] y[1])
** 2) ** 0,5

58
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Además creamos una función que calcula la distancia entre dos puntos. Sin embargo, todavía existe el
problema de los tres puntos que se encuentran en una línea recta. Tenemos que reconocer tales
entradas, porque de lo contrario la función no puede generar una solución correcta. Mientras una
coordenada sea idéntica para los tres puntos, este problema es trivial, por ejemplo, si todos los puntos
se encuentran en el eje x. Pero ¿qué pasa con los otros casos? Los puntos (1, 1), (2, 2) y (3, 3) serían
un ejemplo de ello. La idea de la solución es calcular el vector dirección entre A y B y luego entre B y C.
Esto significa que solo es necesario calcular la diferencia entre los dos puntos (esto es posible porque
podemos considerar puntos en el sistema de coordenadas como vectores) . Si los vectores directores
calculados son iguales, los puntos se encuentran en una recta.

def norma_vector(vector):
"""Crea un vector con longitud 1 que mantenga la dirección del vector de entrada"""

longitud = norma(vector)
retorno [x / longitud de x en vector]

def falls_on_line(punto_a, punto_b, punto_c, tolerancia):


"""Prueba si abc se encuentra en una recta"""
dirección_ab = norma_vector([a b para a, b en zip(punto_a, \ punto_b)])

dirección_bc = norma_vector([b c para b, c en zip(punto_b, \ punto_c)])

producto_escalar = suma(x * y para x, y en zip(dirección_ab, \


dirección_bc))
retorno 1 abs(producto_escalar) <tolerancia

Para hacer esto, definimos una función que normaliza un vector, es decir, mantiene su dirección, pero le
da una longitud de 1. No nos interesa qué tan separados están los vectores, sino solo si apuntan en la
misma dirección. Con la segunda función de ayuda falls_on_line() comprobamos si los puntos están en
línea recta. Para hacer esto, primero calculamos los vectores de A a B y de B a C y los normalizamos.
Luego calculamos su producto escalar. Aquí, dos vectores son paralelos si su producto escalar es 1 o
1. Usamos la siguiente fórmula:

La siguiente función toma cuatro puntos: la estimación actual del centro de nuestro círculo y los tres
puntos dados A, B y C. Luego calcula la distancia de nuestra estimación desde la línea diagonal en 3D
como se explicó anteriormente.

59
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def calcular_distancia(vector, a, b, c):


diagonal = ((0, 0, 0), (1, 1, 1)) # soporte y dirección
distancias = [punto_punto_distancia(vector, p) para p en (a, b, c)]
distancia = línea_punto_distancia(diagonal, distancias)
distancia de retorno, distancias, vector

Definimos la línea diagonal, que siempre es la misma. Luego determinamos las distancias pareadas
entre nuestro centro de círculo estimado, al que aquí nos referiremos como vector, y los tres puntos
dados.
Almacenamos esta información en una lista. Luego usamos la función line_point_distance() ya
definida y determinamos qué tan lejos está nuestra estimación de la línea diagonal.
Por último, pero no menos importante, necesitamos una función que cambie nuestro valor medio actual
y genere nuevos centros potenciales. También subcontratamos esto para que la función principal no se
alargue demasiado. Una implementación simple podría verse así:

def move_vector(vector, coordenada, movimiento):


si coordenada == 0:
retorno [vector[0] + movimiento, vector[1]]
demás:
retorno [vector[0], vector[1] + movimiento]

Aceptamos tres argumentos, el centro actual del círculo, que volvemos a tomar como vector, la coordenada
a mover (movemos las coordenadas x o y, pero no ambas al mismo tiempo) y la distancia a mover. Dado
que aquí la coordenada es solo 0 o 1, solo hay dos condiciones a considerar. Finalmente, hemos creado
todas las funciones auxiliares y ahora podemos centrarnos en su integración.

importar matematicas

def buscador de círculos(a, b, c, tolerancia=0.01,


maxiter=10**5): si a == b o b == c o c==a:
rise ValueError("¡Ingrese tres puntos distintos!")
si falls_on_line(a, b, c, tolerancia=0.1):
rise ValueError("¡Todos los puntos dados se encuentran en una
línea!") centro = [(a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3]
paso = 1
dist1, distancias, _ = calcular_distancia(centro, a, b, c)
para iteración en rango (maxiter):
candidatos = []
para iniciar sesión (1, 1):
para coordinar en (0, 1)
[Link](calcular_distancia \

• 60
Machine Translated by
Google
Capítulo 2 • Trabajar con números

(move_vector(centro, \
coordenada, signo * paso), a, b, c))
nueva_dist1, nuevas_distancias, nuevo_centro = min(candidatos)
si nuevo_dist1 < dist1:
dist1, distancias, centro = nueva_distancia1, nuevas_distancias,\nuevo_centro

demás:
paso *= 0,5
si dist1 < 0,01 * tolerancia:
romper
demás:
elevar ArithmeticError ("No converge")

dist_a, dist_b, dist_c = distancias


si no ([Link](dist_a, dist_b, abs_tol=tolerancia)
y [Link](dist_a, dist_c, abs_tol=tolerancia)):
elevar ArithmeticError ("La estimación no es el centro verdadero")
retorno (redondo (centro [0], 3), redondo (centro [1], 3)), redondo (dist_a, 3)

La función acepta cinco argumentos: los tres puntos dados (A, B, C), la tolerancia (que determina la precisión de nuestro
resultado) y el número máximo de iteraciones. Discutiremos el significado de este valor con más detalle en un momento.
A continuación comprobamos directamente si los puntos son idénticos o se encuentran en una recta. Si es así, arrojamos
un mensaje de error. Luego calculamos una primera estimación para el centro del círculo como un promedio simple de
los puntos dados, de modo que esté disponible un valor inicial. El paso, que determina qué tan lejos movemos el centro
del círculo en la búsqueda de mejores posiciones, se establece inicialmente en 1. Además, para estos valores,
calculamos la primera distancia de nuestra estimación desde la diagonal. Aquí usamos el desempaquetado de tuplas.

Como no usamos el tercer valor de retorno, lo descomprimimos en una variable no utilizada, a la que nombramos con un
guión bajo. A esto le sigue el bucle prin_ci.pal, que termina a más tardar cuando se alcanza el límite de iteración. Esta es
una copia de seguridad que evita que la función se ejecute por mucho tiempo. De esta manera es posible que incluso
después de muchos intentos no se encuentre una buena solución, lo que puede ocurrir si los puntos están desfavorables,
por ejemplo cuando están casi en línea recta.

En el bucle principal, creamos una lista vacía en la que recopilamos los nuevos candidatos de punto central. Esperamos
que uno de los puntos sea una estimación mejor que nuestro valor actual.
Repetimos los signos y las coordenadas, lo que significa que siempre queremos probar cuatro nuevas coordenadas en
función de nuestro valor medio actual. Estos se desplazan en dimensiones horizontales o verticales según el valor del
paso. Si nuestra estimación central actual fuera (0, 0), probaríamos los valores (1, 0), (0, 1), (1, 0) y (0, 1) en la primera
iteración. Usando mover_
vector() calculamos estos cuatro puntos primero y luego calculamos, para cada uno de los cuatro puntos, si este punto
aporta una mejora. Esto se puede medir por el hecho de que nuestro punto está más cerca de la diagonal en la
visualización 3D. Así, al final elegimos el valor más pequeño respecto a este valor de distancia almacenado en los
candidatos. Si este valor es menor que el valor anterior, hemos encontrado una mejora y aplicamos estos valores a
todos los relevantes.

• 61
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

variables. Si este no es el caso, significa que los cuatro candidatos potenciales no son mejores que la
posición actual. Esto significa que ya no nos estamos acercando al verdadero centro, lo que puede deberse
a que ya estamos muy cerca de él, pero el paso es demasiado grande para permitir una aproximación
efectiva. En este caso, reducimos el paso a la mitad. Finalmente comprobamos si ya estamos muy cerca del
verdadero centro y así podemos salir del bucle. De lo contrario, comienza la siguiente iteración. Si
alcanzamos el límite de iteración, abortamos y generamos un mensaje de error.

Si se ha encontrado una buena aproximación, podemos realizar una comprobación final. Simplemente
probamos si las distancias entre el punto central encontrado y los tres puntos dados A, B y C son
aproximadamente iguales. Si es así, la prueba es exitosa y mostramos el resultado.
De lo contrario, generamos un mensaje de error. Ahora podemos calcular un ejemplo. Por ello damos tres
puntos.

>>> buscador de círculos((2, 2), (5, 1), (1, 6))


((1,085, 1,406), 4,595)

El centro del círculo se devuelve en una tupla, el tercer valor es el radio del círculo.
Finalmente, hemos encontrado el círculo correcto y el desafío está completado.

Apéndice: Decoradores

Si queremos cambiar el comportamiento de las funciones, por supuesto podemos reescribirlas. Pero ¿y si
queremos hacerlo dinámicamente? También queremos ajustar el comportamiento de múltiples funciones de
la misma manera. Normalmente tendríamos que reescribir cada función por separado. Para mitigar el
problema, Python proporciona decoradores. Un decorador puede adaptar dinámicamente el comportamiento
de cualquier función, haciendo que el código sea flexible.

En este apéndice, nos gustaría ver un ejemplo basado en Circlefinder() como se muestra arriba. Esta
función toma tres puntos en un plano y encuentra el círculo que corta a los tres. La salida es una tupla con
las coordenadas del centro y el radio del círculo. Supongamos que necesitamos extender esta función a una
tercera salida, es decir, la fecha actual, que puede ser útil para fines de registro. Para hacer esto,
tendríamos que ajustar la función: extender la tupla antes de la salida o insertar una declaración de
impresión. ¿Hay otra manera? Sí, con decorador.
La idea básica es tratar las funciones en Python como objetos que pueden usarse como argumentos en otras
funciones. Para hacer esto, primero codificamos el decorador como una función normal:

def date_adder(func):
fecha = "2020_03_04"
def interior(*args, **kwargs):
imprimir("Fecha actual:", fecha)
función de retorno(*args, **kwargs)
volver interior

• 62
Machine Translated by
Google
Capítulo 2 • Trabajar con números

La nueva función date_adder() tiene exactamente un argumento, es decir, la función que queremos modificar.
Queremos ser lo más flexibles posible y usar *args y **kwargs para asegurarnos de que se mantengan todos los
argumentos posibles de la función a decorar. Luego viene nuestro verdadero ajuste, es decir, la visualización de
la fecha. Después de esto, la función a cambiar se llama normalmente con sus argumentos. Esto completa la
función interna . Ahora solo necesitamos regresar interior en la función exterior. Atención, la función no se llama
(de lo contrario habría que escribir internal()). Ahora tenemos que asegurarnos de que el decorador esté activo.
Como queremos llamar a la función original al final, como de costumbre, la envolvemos en el decorador. Hacemos
esto de forma interactiva y llamamos a todo como prueba:

>>> buscador de círculos = sumador_fecha(buscador de círculos)


>>> buscador de círculos((2, 2), (5, 1), (1, 6), 3)
Fecha actual: 2020_03_04
((1,085, 1,406), 4,595)

Simplemente redefinimos la función: usamos el mismo nombre y pasamos la función original al decorador. Luego
llamamos a la función normalmente, por lo que nada ha cambiado en el manejo y no tenemos que cambiar nada
más en el código, lo cual es una bendición para scripts más largos.
Obtenemos el resultado correcto, pero antes de hacerlo, se muestra la fecha según lo solicitado. Es importante
comprender cuándo se llama a una función y cuándo se trata como un objeto.
Si piensa en la función como una máquina, la iniciamos cada vez que usa paréntesis, por ejemplo func() o
func(argumento), luego la función se activa y devuelve el resultado deseado. Si se utiliza una función sin estos
paréntesis, es como transportar la máquina, ponerla en una lista o incluso pasarla a otra máquina. Esto es
exactamente lo que estamos haciendo. Tomamos una segunda máquina y pasamos la primera con algún código
adicional. Cuando inicia la segunda máquina, se ejecuta el código adicional y la primera máquina se inicia como
de costumbre. No notas esto porque simplemente cambiamos el nombre de la segunda máquina a la primera. La
idea es que puedas aplicar este decorador a cualquier función según sea necesario. Aquí hay un ejemplo:

@fecha_adder
suma definida (x, y):
volver x + y

¿Qué sucede cuando llamamos a la suma() ahora?

>>> suma(1, 2)
Fecha actual: 2020_03_04
3

• 63
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Veremos cómo podemos utilizar el decorador alternativamente con @ (sintaxis algo más elegante, el
funcionamiento es idéntico). Es importante tener en cuenta que esta acción debe aplicarse en la definición
de la función, no en la llamada a la función. Esto significa que un decorador modifica el comportamiento
de una función globalmente. No importa dónde se llame posteriormente, el decorador siempre está activo
al mismo tiempo.

En resumen, los decoradores son herramientas poderosas que, en algunos casos, permiten una
personalización rápida, dinámica y completa de una o más funciones. Para las tareas muy claras que se
muestran en este libro, a menudo no son tan útiles porque podemos cambiar el código directamente. A
este respecto, es importante considerar cuidadosamente cuándo se puede utilizar un decorador con
ventaja y cuándo es más fácil cambiar la función en sí.

2.9 • Muy juntos

Dado es un número de puntos en un plano. Ahora te toca a ti encontrar el par de puntos con la menor
distancia entre sí. Suena fácil, ¿verdad?

Figura 2.10: ¿Cuáles dos puntos tienen la distancia más corta entre sí?

No es difícil idear un algoritmo ingenuo. Calcule simplemente la distancia entre todos los pares imaginables.
Entonces, del punto A al B, luego de A al C, de A al D, y así sucesivamente... En n puntos, estas son un
total de (n(n – 1)) / 2 operaciones, es decir, en 1.000 puntos casi la mitad. un millón. El tiempo de ejecución
de este algoritmo no es precisamente corto o, dicho de otra manera, ¿podemos hacerlo mejor? En efecto.
Primero implementemos el algoritmo ingenuo, que aquí llamaremos enfoque de fuerza bruta. Para el
cálculo utilizamos el módulo itertools , que se asegura de obtener todos los pares y no contar dos veces,
es decir, primero medimos de A a B y luego de B a A, ya que la distancia es simétrica. Aquí vale la pena
echar un vistazo a cómo funciona la función combinaciones() .

>>> desde itertools importar combinaciones


>>> x = ["A", "B", "C", "D"]
>>> para elemento en combinaciones(x, 2):
>>> ( elemento

• 64
Machine Translated by
Google
Capítulo 2 • Trabajar con números

('A', 'C')
('A', 'D')
('ANTES DE CRISTO')

('B', 'D')
('CD')

Tenemos que tener en cuenta que itertools crea un iterador en este punto, es decir, un generador que
genera todas las combinaciones posibles. Ahora podemos usar esto para el cálculo. Calculamos la
distancia entre dos puntos en el plano con el teorema de Pitágoras, lo cual hacemos en una pequeña
función.

desde itertools importar combinaciones


distancia definida (p1, p2):
"""Distancia de dos puntos"""
xdiff = p1[0] p2[0]
ydiff = p1[1] p2[1]
retorno (xdiff ** 2 + ydiff ** 2) ** 0.5

def fuerza bruta(puntos):


"""Encuentra el emparejamiento con la distancia más corta"""
retorno mínimo(
(distancia(*emparejamiento), emparejamiento)
para emparejar en combinaciones(puntos, 2)
)

De hecho, podemos encajar toda la función en una sola expresión. Echemos un vistazo más de cerca.
Primero, combinaciones() genera el objeto iterador que nos proporciona un emparejamiento de todas las tuplas.
Como invocamos la opción 2, se crean pares de dos. Ahora recorremos este iterador y alimentamos las
tuplas resultantes en distancia(), para lo cual usamos el desempaquetado de tuplas (operador de asterisco).
Devolvemos la tupla con la distancia mínima y el emparejamiento en sí, ya que usamos la función min
en la expresión generadora creada. Si cree que esto es demasiado complejo, intente reescribirlo de
manera más explícita.

Hasta ahora, muy lento. Como se explicó anteriormente, esta función actúa en todos los puntos y,
por lo tanto, garantiza el resultado correcto. Pero podemos ser más rápidos si dividimos y
conquistamos. La idea es simple: tenemos un problema que no podemos resolver porque es
demasiado grande o complejo. Por lo tanto, dividimos el problema en subproblemas más pequeños.
O cada subproblema tiene solución o lo volvemos a dividir. Hacemos esto hasta que encontramos un
problema que podemos resolver. Luego propagamos la solución hacia arriba hasta llegar al origen.
Esto funciona en este caso
porque el esfuerzo de probar nuestro problema en el algoritmo ingenuo no crece linealmente, sino
cuadráticamente.
El doble de puntos significa cuadruplicar las operaciones.
Machine Translated by
Google 65
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

El procedimiento ahora es el siguiente: Tomamos la lista de puntos y comprobamos cuántos


elementos contiene. Si hay menos de cinco, utilizamos el algoritmo ingenuo y devolvemos el resultado.
Si hay más, primero ordenamos los puntos por su coordenada x. Luego dividimos los puntos en dos
listas del mismo tamaño, el lado izquierdo y el derecho. Ahora aplicamos el algoritmo de forma
recursiva a cada sublista. O cada lista es lo suficientemente corta y obtenemos un resultado
directamente, o dividimos la lista nuevamente. Al final, obtenemos la distancia mínima para cada
sublista. Ahora podemos comparar ambos y saber si el límite superior está en el lado izquierdo o
derecho. Esto deja sólo un problema: en teoría, la distancia más corta también puede existir entre
puntos que están en la otra lista. Entonces P1 está en el lado izquierdo del límite, P2 está en el lado
derecho y la distancia P1 P2 es más corta que la que se encuentra en la lista izquierda y derecha.
Aquí utilizamos una solución simple. Tomamos el límite superior más corto encontrado hasta el
momento de la sublista izquierda o derecha y lo etiquetamos como δ. Si este límite no es también el
límite inferior, la diferencia aún por encontrar debe ser menor que δ. Sólo se consideran los puntos
con una distancia desde la "línea de separación" menor que δ, ver figura 2.11.

Figura 2.11: Si la distancia de un par de puntos que se encuentran en diferentes lados de la línea divisoria es
más corta que la distancia más corta en el lado izquierdo o derecho, que llamamos δ, este par debe caer dentro del
cuadro representado. Nota: δ = min(δ1, δ2) Creador: Subhash Suri, UC Santa Barbara.

Recopilamos estos puntos en L1 (izquierda del centro) y L2 (derecha del centro). En promedio, el número
de puntos en ambas listas será considerablemente menor que el número total de puntos, por lo que ahora
podemos probar todos los emparejamientos nuevamente. Además, sólo tenemos que probar los pares que
están en diferentes lados de la línea media, de lo contrario, ya han sido probados antes. Se puede
demostrar que existe una solución aún mejor. Dado que la prueba no se puede presentar de manera concisa en este

66
Machine Translated by
Google
Capítulo 2 • Trabajar con números

En este punto, nos remitimos a la literatura y mantenemos el algoritmo más simple.8

Ahora podemos implementar este enfoque con bastante facilidad. Dado que es una función recursiva, debemos
recordar especificar primero el caso base . Si se alcanza esto, las funciones llamadas deben comenzar a
producir retornos; de lo contrario, la recursividad es infinita. En nuestro caso, el caso base es que una
lista contiene menos de cinco elementos.

def mindistance(lista de puntos):


"""Encontrar la distancia más corta con divide y
vencerás""" longitud = len(lista de puntos)
si longitud #Caso base
<5:
devuelve fuerza bruta (lista de puntos)

puntos_izquierda = lista de puntos[:longitud // 2]


puntos_derecho = lista de puntos[longitud // 2:]
min_izquierda = distancia mental(puntos_izquierda)
min_right = mindistance(puntos_right)
d = min(min_izquierda, min_derecha)[0]
limit_left = [p para p en puntos_izquierda si abs(p[0] puntos_derecha[0]
\ [0]) <= d]
limit_right = [p para p en puntos_right si abs(p[0] \
- puntos_izquierda[1][0]) <= d]
distancias = [min_izquierda, min_derecha]
para x en limit_left:
para y en limit_right:
[Link]((distancia(x, y), (x, y)))

La función es sorprendentemente compacta. Definimos el caso base y llamamos a la función naive cuando
la lista de puntos a resolver es muy corta. De lo contrario, dividimos la lista (¡asumimos que ya está
ordenada!) por la mitad. Aquí es donde comienza la recursividad: ¡Resolvemos cada sublista
(puntos_izquierda y puntos_derecha) exactamente con la función que estamos escribiendo! Esto nos hace
volver a utilizar la técnica del bootstrapping, sacándonos del pantano por las correas ya que a estas alturas
ya damos por hecho que nuestra función funciona. Recibimos el resultado de ambas sublistas y
almacenamos el valor más corto en
d. Ahora definimos dos listas nuevas, que a su vez son sublistas de las otras listas. Establecemos el punto
más cercano al medio (es decir, los extremos de la lista) como referencia y medimos la distancia desde este
valor. Por lo tanto, solo incluimos puntos que pueden estar dentro del cuadro mostrado. Luego creamos otra
lista, distancias. En esta lista ahora guardamos todos los pares de limit_left y limit_right. Iteramos sobre
todos los elementos y calculamos las distancias. Al final solo tenemos que generar el valor más pequeño y
listo.

Para ver una presentación de la búsqueda mejorada, consulte [Link] 6.838-


old/handouts/[Link] o [Link] bioinfoconferencias/
puntos de [Link]
Machine Translated by
Google 67
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Si le duele la cabeza con las recursiones, debería pensar en un ejemplo simple con una lista corta y seguirlo con lápiz y
papel o en la propia consola usando declaraciones impresas. Ahora no nos queda otra opción que probar si podemos
cumplir lo que prometimos. ¿Somos realmente más rápidos? ¿Valió la pena? Para ello escribimos una función principal
con una prueba.

tiempo de importación

importar aleatoriamente

def prueba de tiempo():


[Link](1234)
todos los puntos = [([Link]() * 100, [Link]() * 100) para i \
en el rango (5000)]
inicio = [Link]ónico()
imprimir (fuerza bruta (todos los puntos))
imprimir([Link]ónico() inicio)

inicio = [Link]ónico()
todos los [Link]()
imprimir(distancia mental(todos los puntos))
imprimir([Link]ónico() inicio)

Definimos una semilla para poder reproducir los puntos aleatorios en una llamada repetida y crear una lista que contenga
puntos aleatorios. Luego detenemos el tiempo, una vez ingenuamente y otra recursivamente. Con la variante recursiva,

todavía debemos acordarnos de ordenar la lista. ¿Cuál es la diferencia?

>>> prueba de tiempo()


(0.012268040707845339, ((88.13955858203019, 90.2421702279523), (88.14403029179554,
90.23074619037429)))
7.409728050231934
(0.012268040707845339, ((88.13955858203019, 90.2421702279523), (88.14403029179554,
90.23074619037429)))
0.06520891189575195

Hemos mejorado de 7,41 a 0,065 segundos, ¡un factor de 113! Esto no es insignificante y muestra lo que se puede
lograr con un poco de reflexión. Además, ni siquiera hemos implementado la mejor versión, por lo que todavía hay
margen de mejora.
Asignaciones

1. En el ejemplo mostrado, ordenamos la lista de puntos en la función principal y no en la función de recursividad real,
lo cual es un problema. Si alguien quiere cargar la función en su script e importa el archivo como módulo, los
resultados serán incorrectos porque la lista no necesariamente está ordenada al principio. Una solución sencilla
sería incluir la clasificación en

• 68
Machine Translated by
Google
Capítulo 2 • Trabajar con números

la recursividad, pero tiene la desventaja de que cada vez que la función se llama a sí misma, la lista se ordena
nuevamente, lo cual es innecesario porque la clasificación no cambia, incluso después de dividirla. Por tanto,
esto sería una desaceleración innecesaria. Vuelva a escribir mindistance() para que se garantice la
clasificación
por coordenada x y, sin embargo, la velocidad no se vea afectada. Sugerencia: Tendrá que definir una función
dentro de la función.

Apéndice: *args y **kwargs

En esta tarea, utilizamos el operador de descompresión, que generalmente se conoce como *args (el nombre
args, que se refiere a "argumentos", es arbitrario, la declaración en el código se realiza mediante el operador de
asterisco
*). Este operador es muy útil cuando definimos una función, pero queremos permitir una cantidad arbitraria de
argumentos. Ya hemos visto varias veces cómo podemos crear una función sencilla que sume exactamente dos
números. ¿Y si tenemos más de dos? Además, ¿qué pasa si queremos permitir no sólo la suma sino también la

multiplicación, por ejemplo? Con *args podemos ser muy flexibles.

calculadora def(operador, *args):


si operador == "agregar":
suma devuelta (argumentos)
si operador == "multiplicar":
resolución = 1

para elemento en argumentos:


res *= elemento
devolver resolución

Nuestra función parece aceptar exactamente dos argumentos: el tipo de operación aritmética y *args. Usamos el
asterisco aquí para indicar que aceptamos cualquier cantidad de argumentos adicionales.
Posteriormente, Python los recopilará en una tupla para nosotros. Dependiendo del tipo de operaciones, realizamos
suma o multiplicación. Como puede ver, internamente tratamos *args como una lista o tupla normal. Se puede

considerar como un iterador. Ahora probemos esta función.

>>> calculadora("sumar", 1, 2, 3)
6
>>> calculadora("multiplicar", 1, 2, 3, 4)
24

No importa cuántos argumentos agreguemos. Cabe enfatizar que no los recopilamos en una lista o tupla de
antemano, por lo que no pasamos una lista con cualquier cantidad de elementos, sino argumentos adicionales. Al
definir la función con *args, Python puede manejar esto de manera flexible.
También muestra que este truco sólo funciona si esto se ha tenido en cuenta en la definición.
Por lo tanto, si usamos las funciones internas de Python, debemos verificar o probar si se permite *args. De manera
similar, existen **kwargs ("argumentos de palabras clave"), que se tratan como un
Machine Translated by
Google

• 69
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

dict. Esto puede ser útil, por ejemplo, si escribe una función que acepta diferentes opciones pero no está
claro de antemano exactamente cómo se llaman o cuántas están disponibles.

def mostrar(nombre, **kwargs):


imprimir("Hola", nombre)
para clave, valor en [Link]():
imprimir (clave, valor)

>>> display("Usuario", Día = 1, Lugar = "Oeste", Bandera = Verdadero)


Hola usuario
Día 1
Lugar Oeste

Marcar verdadero

Puede especificar *args y **kwargs en una función; sin embargo, siempre deben colocarse como los
últimos argumentos.

2.10 • Retroceso

Retroceder es el proceso de resolver un problema probando sistemáticamente todas las soluciones


posibles. Se trata por tanto de un método de fuerza bruta que puede resultar útil en algunos casos.
Siempre que la tarea no implique demasiadas posibilidades, retroceder puede ser un enfoque inteligente.
Como ejemplo se puede mencionar la búsqueda de la salida de un laberinto. En una bifurcación del
camino, siempre eliges los caminos de derecha a izquierda y marcas un camino ya elegido.
Si llega a un callejón sin salida, regresa a la última rama no utilizada. Si utilizas este método de forma
constante, llegarás al final del camino en algún momento (en el peor de los casos, tendrás que probar
todos los caminos). Todo lo que necesitas es una regla de decisión y memorizar todos los caminos ya
tomados. De esta forma, una ruta sólo se utiliza una vez.

Como ejemplo aplicado, consideraremos el problema del recorrido del Caballero (ver figura 2.12).
Se trata de mover el caballo sobre un tablero de ajedrez vacío para que entre en cada una de las 64
casillas exactamente una vez. La salida está en una de las esquinas. Mediante prueba y error, descubrirás
que esto no es fácil y, a menudo, llegarás a una posición en la que no es posible ningún movimiento
válido a una casilla previamente desocupada. La idea de la solución es la siguiente: el caballero comienza
en una esquina y elige aleatoriamente un campo que no ha sido visitado. Esto se hace hasta que llega a
un callejón sin salida, es decir, no puede moverse sin violar la regla. Luego regresa al último campo
donde todavía hay un campo no visitado disponible. El camino sin salida se guarda en la memoria y
nunca se vuelve a entrar.

• 70
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Figura 2.12: Se muestra un camino para el caballero que ingresa a cada casilla exactamente una vez. Creador:
[Link] (Wikimedia Commons).

La tarea se divide en múltiples subtareas o funciones. En primer lugar, el tablero de ajedrez debe
estar representado numéricamente. Una posibilidad es utilizar un sistema de coordenadas
cartesiano, donde cada cuadrado está definido por dos coordenadas (fila y columna). Para acelerar
el cálculo, en el ejemplo consideramos un tablero de ajedrez con sólo 25 casillas, es decir, cinco filas
y columnas. Sin embargo, diseñaremos el programa de tal manera que el tamaño del campo se
pueda definir arbitrariamente, por lo que cabe señalar que no existen soluciones para todos los
tamaños de campo. El campo (0, 0), que queremos ver en la esquina superior izquierda, tendría así
el campo número 0, el campo (4, 4) está en la esquina inferior derecha con el campo número 24.
Entonces necesitamos otra función. que calcula todos los movimientos disponibles para el caballo.
Hay que tener en cuenta tanto los límites del tablero para que el caballo no salte como todas las
casillas que ya han sido visitadas en jugadas anteriores y que por tanto están bloqueadas.

def posfinder(posición, ruta, callejón sin salida, tamaño):


"""Encontrar todas las casillas disponibles para el caballero"""
campos de pos = []
para a, b en [(2, 1), (2, 1), (1, 2), (1, 2), \
(1, 2), (1, 2), (2, 1), (2, 1)]:
a += posición[0]
b += posición[1]
si 0 <= a <= tamaño 1 y 0 <= b <= tamaño 1:

• 71
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

# La posición está dentro del tablero de ajedrez.

si (a, b) no está en la ruta y (ruta + [(a, b)]) no está en \


callejón sin salida:

[Link]((a, b))
campos de retorno

La función acepta cuatro argumentos: la posición actual del caballo, el camino tomado actualmente (es decir,
una lista de todas las casillas en las que ha entrado desde el inicio), una lista de todos los caminos ya
tomados que terminaron en un callejón sin salida, y el tamaño del tablero de ajedrez. Creamos una lista
vacía en la que se almacenan los posibles campos de movimiento. Luego iteramos sobre todas las
posiciones posibles.
Como puedes comprobar fácilmente, hay un máximo de ocho movimientos posibles para un caballero
(menos si está parado en el borde). Podemos revisarlos manualmente y almacenarlos en una lista.
Calculamos el nuevo campo para cada posibilidad y comprobamos si todavía está dentro de los límites del
tablero. Si este es el caso, comprobamos si ya se ha introducido en la ruta actual o si se trata de un campo
bloqueado al que ya no se puede acceder. Para hacerlo, "damos cuenta" del movimiento y lo agregamos a
la ruta actual para realizar pruebas. Si esta ruta ya aparece en un callejón sin salida, hemos encontrado un
callejón sin salida previamente conocido y debemos ordenar este campo. Si se excluyen estas
contingencias, el campo se puede agregar a posfields como un cuadrado potencial al que saltar. Ahora se
puede crear la función caballero() . Utiliza posfinder() para encontrar los movimientos legales y, de lo
contrario, solo implementa la lógica general de retroceso.

caballero definidor(tamaño=5):
posición inicial = (0,
0) ruta = [[Link]]
callejón sin salida = []
iteración = 1
mientras len(ruta) < tamaño ** 2:
iteración += 1
# Generar todos los movimientos adicionales:
mueve = posfinder(ruta[1], ruta, callejón sin salida, tamaño)
si se mueve:
[Link](se mueve[0])
ruta elif == [[Link]]:
aumentar ValueError ("No se puede resolver")
demás:
#Retroceder cuando estés en un callejón sin salida:

callejón sin [Link](ruta)


ruta = ruta[:1]
print("Iteraciones:", iteración)
imprimir (ruta)
print([b * tamaño + a para a, b en la ruta])

Nuestra función acepta el tamaño del tablero de ajedrez como único argumento, que establecemos aquí como

• 72
Machine Translated by
Google
Capítulo 2 • Trabajar con números

El valor predeterminado es 5. Inicializamos la posición inicial del caballero y creamos una lista en la ruta donde
se almacena la ruta actual. Al principio, esta lista contiene sólo la posición inicial.
Con el callejón sin salida utilizamos una segunda lista en la que almacenamos las rutas que hemos probado y
que ya no deberíamos utilizar. En las iteraciones almacenamos la frecuencia con la que se ejecutó el bucle
principal. El circuito principal continúa hasta que el camino alcanza una longitud máxima. Este es el caso cuando
cada campo se ingresa exactamente una vez, que es el cuadrado del tamaño.

Aumentamos el contador en 1 y utilizamos nuestra función de ayuda previamente definida para generar
movimientos potenciales en movimientos. Si la lista no está vacía, seleccionamos el primer mejor movimiento, lo
agregamos a nuestra ruta actual y comenzamos el ciclo desde el principio. Dado que nuestra función de ayuda
ya comprueba que la mudanza es legal y aún no se ha realizado, no son necesarias más comprobaciones.
Sin embargo, si la lista está vacía, esto indica que no es posible realizar ningún movimiento legal desde la
posición actual. Luego comprobamos si estamos en el campo de salida. Si este es el caso, hemos llegado a una
situación en la que la solución es imposible. Esto puede suceder con ciertos tamaños de campo.
Sin embargo, si no estamos en el campo de salida, hemos llegado a un callejón sin salida. En este caso, se
invoca el mecanismo de retroceso real. Agregamos la ruta actual al callejón sin salida para memorizar que no la
usaremos nuevamente en el futuro. Luego eliminamos el último movimiento de la ruta actual y reiniciamos el ciclo.
Desde que actualizamos el punto muerto, posfinder(), el mismo movimiento no se puede utilizar la próxima vez
que lo ejecutemos.

Si probáramos todas las posibilidades de esta manera, el tablero se declararía irresoluble o habríamos encontrado
una manera. En este caso, obtenemos algunas estadísticas y mostramos el camino del caballero para que
podamos volver sobre él si es necesario. Ahora podemos probar la función.

>>> caballero()
Iteraciones: 9995
[(0, 0), (1, 2), (0, 4), (2, 3), (0, 2), (1, 0), (3, 1), (4, 3), ( 2, 4), (0, 3), (1, 1), (3, 0), (2, 2), (1, 4), (3, 3), (4, 1),
(2, 0), (0, 1), (1, 3), (3, 4), (4, 2), (2, 1), (4, 0), (3, 2), (4, 4) ]

[0, 11, 20, 17, 10, 1, 8, 19, 22, 15, 6, 3, 12, 21, 18, 9, 2, 5, 16, 23, 14, 7, 4, 13, 24 ]

Asignaciones

1. Según HC von Warnsdorf, existe una heurística sencilla para acelerar la solución.
El caballo siempre debe moverse al campo desde el cual tiene la menor cantidad de movimientos disponibles.
Agregue esta regla en el programa actual y compruebe si se puede mejorar la velocidad con esto.

2. Esta tarea en realidad se resolvió sin recursividad. Cambia esto y combina la recursividad.
y retroceder para resolver el recorrido del caballero.
3. Sudoku es un rompecabezas popular, donde en un cuadrado de 9x9 los números del 1 al 9 solo pueden
aparecer exactamente una vez en cada fila, columna y en cada uno de los nueve cuadrantes. Escribe una
función que acepte un Sudoku sin resolver y lo resuelva retrocediendo. Consejos:

• 73
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

• Primero, piense en cómo se puede representar numéricamente un tablero de Sudoku en Python.


• Escribe una función que compruebe si el Sudoku se ha resuelto correctamente y si cada campo tiene un
número.
• Escriba una función que verifique qué números aún son posibles para un campo determinado. Estos
Luego se puede probar sistemáticamente.

2.11 • Integración Numérica

La derivación e integración de funciones es uno de los intereses centrales del análisis.


Probablemente no exista disciplina científica que no utilice funciones y cálculo diferencial para describir y
evaluar modelos sobre la realidad. Si bien la derivación de funciones normalmente se puede realizar muy bien
con algoritmos porque sólo es necesario aplicar unas pocas reglas, la integración es mucho más desafiante.
Aunque existen reglas y algoritmos básicos disponibles, las funciones más complejas requieren mucha
experiencia e incluso creatividad. No en vano, algunas universidades organizan concursos para estudiantes de
matemáticas, en los que los participantes deben integrar determinadas funciones lo más rápido posible. Esta
es la razón por la que en el pasado los ordenadores tenían dificultades para realizar esta tarea. A menudo era
necesario recurrir a libros de consulta. Hoy en día, debido al enorme aumento de la potencia informática y al
progreso general de la informática, normalmente incluso funciones complejas pueden integrarse automáticamente.
En este punto, mostraremos una manera de calcular la integral de funciones complejas sin ningún conocimiento
de reglas y fórmulas básicas.

La integración consiste en determinar el área bajo una curva. Por tanto, buscamos un área de superficie para
una determinada función en una determinada sección de la función. Veamos primero un ejemplo sencillo (figura
2.13).

Figura 2.13: ¿Cómo se calcula el área total de S? Creador: 4C (Wikimedia Commons)

• 74
Machine Translated by
Google
Capítulo 2 • Trabajar con números

Se da la función f(x) así como los puntos en el eje x que limitan el área, es decir, el área de
integración (a y b). El área se define como el área entre la curva y el eje x. En este ejemplo, esto no
es un problema, porque la función no intersecta el eje.
Existen reglas básicas para diversas funciones. Veamos un ejemplo sencillo de matemáticas de secundaria.
Dada la función f(x) = x2. Queremos determinar el área de la curva en el rango de 0 a 2. Podemos
representar esto gráficamente de la siguiente manera (siguiente figura).

Figura 2.14: Queremos calcular el área sombreada en gris. Creador: Snucube (Wikimedia Commons)

Podemos buscar las reglas y aprender que la integral de la función original debe, por tanto, ser F(x)=
(1/3)x3. Usamos la letra mayúscula aquí para indicar que es la función raíz asociada.
Podemos hacer la prueba y derivar esta función raíz, que nuevamente devuelve la función original.

El cálculo del área se realiza de la siguiente manera:

Al especificar dx, determinamos que la función se integrará para x. Si aplicamos este procedimiento
a la función dada, obtenemos el siguiente resultado:

75
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Hemos determinado el área exactamente, pero sólo fue posible porque conocíamos la fórmula o la
buscamos. ¿Qué pasa con funciones mucho más complejas? ¿Qué pasa si no conocemos la regla de
integración? En este caso la integración numérica nos ayuda. La idea básica es simple: dividimos toda
el área bajo la curva en rectángulos de igual ancho. Podemos dibujarlos y determinar el área de todos
los rectángulos, lo cual es fácilmente posible porque podemos calcular la coordenada y calculándola con
la función dada. Luego sumamos todas las áreas parciales y aproximamos el área total. Cuantos más
rectángulos calculemos, más precisa será nuestra estimación. Veámoslo gráficamente (figura 2.15).

Figura 2.15: El seno se integra numéricamente dividiéndolo en rectángulos. Tenga en cuenta que el eje x está escalado
en Pi, por lo que 1,0 representa 3,141…. Creador: DMGualtieri (Wikimedia Commons, CC BYSA 4.0)

Entonces nuestro código tiene que hacer lo siguiente: primero, toda el área de integración se divide en n
secciones del mismo tamaño. Luego seleccionamos el valor de x para un punto específico dentro de cada
sección (simplemente seleccionaremos el margen izquierdo). Para este punto, determinamos el valor de
función correspondiente y. El área es entonces el producto de y por el ancho de la sección. Finalmente,
resumimos todas las subáreas. Lo que parece simple también es bastante compacto.

def integración(función, x1, x2, n):


si x1 >= x2:
elevar AssertionError ("¡x1 debe ser menor que x2!")
longitud total = x2 x1
longitud parcial = longitud total / n
suma = 0
para i en el rango (n):
valorx = x1 + longitud parcial * i
valor y = eval(funció[Link]("x", str(valorx)))
área parcial = valor y * longitud parcial
suma += abs(dividir)
ronda de retorno(área total, 5)

Nuestra función tiene cuatro argumentos: la función dada, el inicio, el final de la integración.

• 76
Machine Translated by
Google
Capítulo 2 • Trabajar con números

área y el número de rectángulos que se generarán. Primero comprobamos que el inicio sea más pequeño que el
final del área de integración. Determinamos la longitud total del área de integración y la longitud de un área parcial.
Por ejemplo, si el largo total es 10 y queremos dibujar 10 rectángulos, cada uno tiene exactamente 1 de ancho.
Luego iniciamos un bucle que itera sobre todas las áreas parciales. Debemos empezar en 0 e ir hasta n. El valor
de x siempre debe tomarse en el borde izquierdo de un área parcial. Entonces es el límite inferior del área de
integración más el producto de la longitud parcial y el número del rectángulo actual. El valor de y correspondiente
ahora debe calcularse usando la función dada. Para hacer esto, primero reemplazamos todos los valores de x en
la función con el valor actual. Entonces, si la función es f(x) = y = x2, el valor 0 se inserta en el primer paso y la
función se evalúa como f(x) = y = 02. Para esto usamos [Link](oldvalue, newvalue ) . Después de esto, se
lleva a cabo la evaluación real, el resultado se almacena en yvalue. El área parcial es el producto de la longitud
parcial y el valor de y. Sumamos esto al área total. Aquí especificamos que se utiliza la cantidad absoluta . Esto
es relevante si la función va por debajo del eje x, es decir, se producen valores y negativos. Si no lo manejamos
de esta manera se podrían producir zonas negativas que no queremos a estas alturas. Después de iterar sobre
todos los rectángulos, el área total se redondea y se devuelve. Hasta ahora todo está claro: es hora de realizar
una prueba.

Debemos pasar nuestra función como una cadena.

>>> integración("(x)**2", 0, 2, 10**4)


2.66707

Vemos que nuestro resultado anterior es aproximado. También es una buena idea ingresar la función de forma
especial. Ponemos paréntesis adicionales alrededor de cada X. Si no hacemos esto, podrían ocurrir errores si se
evalúan valores negativos. El siguiente ejemplo muestra por qué. Nuestra función es idéntica y queremos evaluar
el valor 5.

>>> 5**2
25
>>> (5)**2
25

Estos errores se pueden evitar con paréntesis adicionales. Ahora miremos nuevamente la figura 2.15 y
aproximaremos esta función. ¿Y si no sabemos integrar el seno?
Con la integración numérica, esto ya no es un desafío para nosotros. Entonces integramos el seno de 0 a 2 Pi,
que corresponde exactamente a la figura de arriba.

>>> importar matemáticas

>>> integración("[Link](x)", 0, 2 * [Link], 10**4)


4.0

• 77
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Para la validación, podemos saber que la función raíz del seno es el coseno y luego integrarla sección por
sección (con respecto a las raíces), o preguntarle a Wolfram Alpha, que también confirma nuestro cálculo.9

Una última palabra de advertencia sobre eval(). Usamos esta función para que la función matemática
proporcionada por el usuario pueda ejecutarse directamente como código Python. Esto puede ser peligroso
cuando el usuario ingresa no una función prevista por el programador sino un código malicioso. En teoría, el
usuario podría proporcionar un código que robe datos o borre el disco duro. Estos son ejemplos extremos,
pero deberían resaltar que eval() siempre debe usarse con precaución. Dado que no estamos escribiendo
software para el usuario final sino simplemente pequeñas herramientas para nosotros, esto no es motivo de
preocupación en este momento.

[Link]
9 de+0+a+2pi

• 78
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Capítulo 3 • Estadísticas y Simulaciones

Python es muy adecuado para el análisis estadístico y goza de una excelente reputación en la joven disciplina de
las ciencias de datos. En los siguientes ejemplos renunciaremos a las enormes posibilidades que ofrecen
paquetes adicionales como NumPy o Pandas y nos limitaremos a las herramientas estándar que ya permiten una
amplia gama de análisis. Incluso se pueden construir rápidamente simulaciones completas en Python, lo que nos
permite prescindir de cálculos analíticos. Esto es muy útil si dicha solución analítica no está disponible o es
extremadamente compleja. En última instancia, las simulaciones se basan en números aleatorios, que están
disponibles en Python mediante el módulo aleatorio .

3.1 • Prueba de velocidad

Ya medimos el tiempo de ejecución de funciones y programas en asignaciones anteriores. Esto puede resultar
extremadamente útil, por ejemplo, en pruebas comparativas o para determinar qué implementación de una tarea
es la más rápida. Hasta ahora, los enfoques de medición han sido ingenuos y se han basado exactamente en
una ejecución. Debemos tener en cuenta que dicho valor puede distorsionarse, por ejemplo, porque muchos
programas se ejecutan en segundo plano y consumen tiempo de cálculo.
Se pueden utilizar varios métodos para obtener un mejor resultado. Una posibilidad es realizar la tarea
repetidamente y promediar los tiempos medidos. De este modo se puede reducir el efecto de mediciones extremas
o valores atípicos. También es más conveniente probar varias funciones directamente entre sí en lugar de tener
que llamarlas individualmente. Por lo tanto, a continuación crearemos una función que acepte un número arbitrario
de funciones para probar y determine su tiempo de ejecución. Asimismo, podemos aleatorizar el orden en el que se
prueban las funciones para evitar efectos de posición.

tiempo de importación

importar aleatoriamente

importar estadísticas como estadísticas


def prueba de velocidad(funciones, n):
afirmar isinstance(funciones, lista)
veces = {f: [] para f en funciones}
para correr en rango (n):
[Link](funciones)
para función en funciones:

hora_inicio = [Link]ónica()
función()
tiempo_final = [Link]ónico()
veces[función].append(hora_final hora_inicio)
para función, tiempo de ejecución en [Link]():
print(f"{función}: {estadí[Link](tiempo de ejecución):.4f} | \
{estadí[Link](tiempo de ejecución):.4f}")

Primero, importamos tres módulos con las funciones que necesitamos. Podemos abreviar el módulo largo.

• 79
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

nombres para simplificar e introducir nuestra propia abreviatura. La función que creamos acepta dos
argumentos: una lista de todas las funciones que queremos probar y el número de pasadas. Queda
claro que podemos tratar funciones en Python como otros objetos y, por lo tanto, podemos agregar
funciones a otros objetos. Cuantas más pasadas elijamos, más preciso será nuestro resultado.
También verificamos que las funciones se pasen como una lista y no como una tupla, ya que solo
podemos mezclar listas aleatoriamente. Luego inicializamos un dict donde almacenamos los
resultados de los pases para cada función. Dado que las funciones son inmutables en Python,
podemos usarlas directamente como claves. Iniciamos un bucle que se ejecuta hasta que se procesan
todos los pases. En cada pasada, el orden de las funciones se aleatoriza posteriormente y se ejecuta
cada función. El tiempo se mide por la diferencia entre las dos marcas de tiempo. Agregamos los
tiempos en el dict a las funciones respectivas. Finalmente mostramos los resultados. Repetimos
todas las claves y valores en el dict y usamos FStrings para una visualización limpia.

Asignaciones

1. Elija un ejemplo anterior del libro y compare el tiempo de ejecución de diferentes


implementaciones usando speedtest().
2. En un ejemplo anterior hablamos de decoradores. Defina una función decoradora que se
pueda adjuntar dinámicamente a funciones arbitrarias y mida el tiempo de ejecución de la
función cuando se llama.
3. Para usar speedtest() con funciones que utilizan argumentos, puede usar [Link]().
Pruebe esto en acción. Para una demostración, consulte la página 143.

3.2 • Pi (otra vez)

En una tarea anterior calculamos la constante Pi con precisión arbitraria. La siguiente tarea tiene un
objetivo similar, pero se basará en sorteos aleatorios y estadísticas en lugar de cálculos numéricos.
La idea básica es dibujar repetidamente puntos aleatorios y comprobar si se encuentran dentro o
fuera de un círculo. Si se dibuja un número suficiente de puntos, se puede aproximar Pi de esta
forma.

Figura 3.1: Simulación de puntos extraídos aleatoriamente. Los huecos están dentro del círculo, los demás
afuera.

80
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

La idea básica es la siguiente. Dentro de un cuadrado con una longitud de lado 2 (por lo tanto con un área de 4)
se dibuja un círculo con radio 1. El área del círculo es r2 *pi, el área del cuadrado (2r)2.
Para simplificar las cosas, seleccionamos solo un cuarto del cuadrado, que tiene un área 1. Usando álgebra
simple podemos deducir lo siguiente:

Total = r2 = 1

El área del círculo dentro del cuadrado se calcula de la siguiente manera:

Un círculo = ¼ * r2 * Pi

Vemos que Pi está presente en esta fórmula, por lo que podemos deducirlo reorganizando la ecuación
original de la siguiente manera:

pi = 4 * (Un círculo / r2) = 4 * Un círculo

La solución es la siguiente: extraemos puntos al azar del cuadrado y comprobamos para cada punto si se
encuentra dentro o fuera del círculo. Sólo necesitamos calcular su distancia al origen para comprobarlo. De esta
forma, podemos aproximar el área del círculo por la proporción de puntos que caen dentro. Una vez que hemos
encontrado esta zona, tenemos Pi. Una implementación como función se escribe de forma compacta.

importar aleatoriamente

def pi2(n):
dentro = 0
para i en el rango (n):
x, y = [Link](), [Link]()
distancia = (x ** 2 + y ** 2) ** 0,5
si distancia <= 1:
dentro += 1
devolver 4* (dentro/n)

Creamos un bucle en el que cada vez se dibuja un punto aleatorio entre cero y uno, que se implementa mediante
una coordenada xey. Luego usamos el teorema de Pitágoras para calcular la distancia de este punto al centro
del círculo. Si este es menor o igual que el radio, sabemos que el punto se encuentra dentro del círculo. Si no es
así, lógicamente debe quedar fuera. Finalmente, necesitamos contar cuántos puntos hay afuera. Ahora probamos
el resultado.

>>> pi2(10**6)
3.141664574393

Incluso si este método funciona según lo previsto, no es muy eficaz. Para obtener un aceptable

81
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Para una aproximación de Pi necesitamos realizar al menos un millón de extracciones, lo cual es bastante.
Por lo tanto, en lo que respecta al rendimiento, la solución algorítmica podría ser preferible. No estoy seguro
de a cuántas personas se les podría ocurrir la solución que encontró John Machin. El enfoque estadístico es
fácil de entender y de implementar rápidamente, lo que demuestra que las simulaciones pueden ser una
herramienta válida para la inferencia y el análisis.

Asignaciones

1. Calcule Pi usando el método estadístico para 102, 103, 104, 105, 106 y 107 sorteos aleatorios, y
cada versión con 50 ejecuciones. ¿Cuantos decimales correctos se alcanzan en promedio?

2. El problema de Monty Hall proviene de un conocido programa de juegos estadounidense. El


procedimiento es bastante sencillo: una victoria y dos espacios en blanco (cabras) se esconden detrás
de tres puertas. El candidato elige una de las puertas (por ejemplo, la puerta 2). Luego, el director del
juego abre una puerta con un espacio en blanco (por ejemplo, la puerta 1). Ahora el candidato tiene la
opción de revisar su decisión original o de atenerse a ella. La pregunta ahora es: ¿Puede el candidato
aumentar sus posibilidades de ganar si cambia su elección después de abrir la primera puerta?
Suponemos que el director del juego siempre abre una puerta con un espacio en blanco. Defina una
función que aproxima
las posibilidades de ganar del candidato para ambas decisiones (cambiar o mantener) mediante simulaciones.
3. La probabilidad de que de un grupo de n personas al menos dos cumplan años el mismo día se puede
calcular con la siguiente fórmula. Escribe un programa que resuelva esta tarea usando simulaciones y
aproxima la probabilidad. Después de eso, implemente la fórmula que se muestra en Python y
compare los resultados.

4. Es bien sabido que las posibilidades de ganar son relativamente bajas cuando se juega a la lotería.
Implementar una función que simule el sorteo de los números de lotería 6 de 49 (incluido el número
adicional). Alimente la función con sus números de lotería y descubra qué beneficio total ha obtenido
después de 50 años jugando a la lotería. Supongamos que un juego cuesta 1,50. Las probabilidades
de ganar se muestran en la siguiente tabla.

82
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Apéndice: números aleatorios en Python

Las simulaciones y sorteos aleatorios están disponibles en Python mediante funciones en el módulo aleatorio .
Este apéndice presentará algunas de las funciones más importantes, tal como aparecerán repetidamente más
adelante en este libro.1 Primero, se debe importar el módulo y luego se pueden llamar las funciones individuales.
Veamos cómo podemos obtener resultados aleatorios pero reproducibles.

>>> importar aleatoriamente


>>> [Link](123)
>>> [Link]()
0.052363598850944326
>>> [Link](123)
>>> [Link]()
0.052363598850944326

Siempre que los resultados deben ser reproducibles, como durante la depuración o en aplicaciones científicas,
es necesario establecer la semilla. Las computadoras generan números aleatorios utilizando generadores de
números pseudoaleatorios (PRNG) porque, por diseño, son máquinas estrictamente deterministas y todas las
operaciones pueden reproducirse tal como ocurren en una CPU. Esto significa que las computadoras son malas
para generar aleatoriedad. Sin embargo, esta deficiencia se puede evitar utilizando algoritmos especiales que
generan números aparentemente aleatorios.
Las computadoras utilizan ciertos factores que presumiblemente son aleatorios, como la cantidad de procesos
que se ejecutan actualmente, la carga del sistema, la memoria disponible, la entrada del usuario, los movimientos
del mouse, etc. Estos factores aleatorios presumiblemente "reales" se incluyen en la semilla del algoritmo, lo que
garantiza que se generarán números diferentes la próxima vez que se llame al algoritmo. Si no lo desea, puede
especificar esta semilla y así obtener siempre el mismo resultado. Cuántos números se toman de la semilla es
irrelevante. El ejemplo anterior muestra cómo se puede utilizar esta función. Cabe señalar que los algoritmos
cambian con el tiempo y, por lo tanto, no se puede garantizar que diferentes versiones de Python siempre
produzcan los mismos números, incluso cuando se usa la misma semilla.

Si queremos números aleatorios de un rango determinado, randrange es útil. Combina el conocido operador de
rango con un elemento aleatorio. Por ejemplo, podemos producir números aleatorios entre 50 y 100 (exclusivo)
en un intervalo de 5 de la siguiente manera:

>>> z = [[Link](50, 100, 5) para i en el rango(10)]


>>> z
[55, 80, 70, 55, 50, 80, 90, 90, 75, 75]

El manejo es similar al rango(). Si queremos números aleatorios reales del intervalo


[Link]() como ya se demostró anteriormente. Estos números aleatorios son
1 Para obtener una descripción general completa, consulte [Link]/3.6/library/[Link]

83
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

distribuidos equitativamente, lo que significa que cada número en el rango tiene la misma probabilidad
de ser sorteado. Si queremos números distribuidos normalmente, usamos [Link](mu,
sigma), donde mu es la media y sigma la desviación estándar deseada. También están disponibles
muchas otras distribuciones. Si por el contrario no nos preocupamos de números sino de elementos,
como palabras, naipes o similares, hay funciones adicionales disponibles.

>>> datos = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
#Exactamente un elemento
>>> elecció[Link](datos)
A

#Saca una muestra de 5 sin reemplazo


>>> [Link](datos, k=5)
['C', 'I', 'H', 'E', 'G']
#Dibujar una muestra de 5 con reemplazo
>>> [Link](datos, k=5)
['G', 'B', 'I', 'G', 'H']

3.3 • Paralelización

Mientras que la frecuencia de reloj de los procesadores lleva algún tiempo estancada y aparentemente
ha alcanzado un límite físico, el número de núcleos de procesador, en cambio, aumenta rápidamente.
Hoy en día no es raro que las PC de escritorio tengan ocho o más núcleos físicos. Los servidores están
alcanzando magnitudes completamente diferentes. Así pues, la tendencia del futuro es la paralelización.
Sin embargo, las nuevas técnicas de programación también deben apoyar esta tendencia, ya que muchos
algoritmos están diseñados para procesamiento en serie y se deben adaptar arquitecturas o paradigmas
de programación. Siempre que una tarea se puede paralelizar adecuadamente, las ganancias de
rendimiento suelen ser enormes y, en el mejor de los casos, escalan linealmente con la cantidad de
núcleos o procesos. En este ejemplo, veremos cómo podemos paralelizar tareas simples con Python.

Como ejemplo, usaremos nuevamente números primos y asumiremos que necesitamos muchos números
muy grandes para una aplicación. Ya hemos mostrado cómo se pueden encontrar con bastante facilidad
mediante prueba y error. Pero cuanto mayores son las cifras, más lentamente se producen otras nuevas.
Si podemos utilizar varios núcleos en lugar de uno, el proceso se puede acelerar. Para esta tarea,
adaptamos la función anterior y utilizamos el módulo de multiprocesamiento de Python . Esto es necesario
siempre que se utilice más de un núcleo físico. La idea del programa es la siguiente: en lugar de llamar a
una función o generador solo una vez, lo llamamos varias veces y dejamos que las diferentes instancias
se ejecuten una al lado de la otra. Siempre que una de estas funciones produce un resultado, se almacena en una cola.
El hecho de que existan diferentes tipos de colas es irrelevante para nosotros aquí.2 Ajustamos la
función principal para tomar cada elemento recién llegado de esta cola y escribirlo en una lista. Tan
pronto como esta lista alcanza una longitud predefinida, todas las instancias o procesos en ejecución finalizan
2 Para ser más precisos, la cola es un objeto de primero en entrar, primero en salir (FIFO). El elemento
que llega primero también sale primero.

84
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

y se devuelve la lista. Para hacer esto, primero escribimos la función central que genera números primos.

def primegen(n, cola):


si n % 2 == 0:
norte += 1
mientras que Verdadero:

para i en rango(3, int(n**0.5 + 1), 2):


si n % i == 0:
romper
demás:

[Link](n)
norte +=
2

Esta función es casi idéntica a la versión anterior pero ahora acepta dos parámetros. n especifica el
número inicial para que podamos generar números primos arbitrariamente grandes. cola es el objeto al
que luego se pasan los resultados. El bucle principal continúa hasta que lo terminamos desde el exterior.
Tan pronto como se encuentra un número primo, el valor no se devuelve usando return, sino que se pasa
al objeto de la cola usando put(). Ahora a la función principal.

desde proceso de importación multiprocesamiento, cola


def multiprimegen(núcleos, nfinal):
q = cola()
procesos = []
para el número en el rango (1, núcleos + 1):
inicio = 10**14 // número
proceso = Proceso(objetivo=primegen, args=(inicio, q))
[Link]()
[Link](proceso)
primos = []
mientras que len(primos) < nfinal:
[Link]([Link]())
para proceso en procesos:
[Link]() d

Desde el módulo de multiprocesamiento importamos dos funciones que necesitamos. La función real
tiene dos argumentos: el número de núcleos o subprocesos que se utilizarán y el número total de números
primos que se generarán. Creamos un objeto de cola en q, en el que recopilamos los resultados de los
procesos individuales. Ponemos los procesos en sí en una lista para que podamos gestionarlos. Usando
un bucle, creamos cada proceso. En primer lugar, es importante tener en cuenta que cada proceso recibe
una inicialización diferente, de lo contrario todos generarían los mismos números primos, lo cual no
tendría sentido. Aquí utilizamos una fórmula de estimación cruda. En una aplicación real, este paso
debería considerarse más cuidadosamente para que la carga de trabajo de todos

85
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Los procesos son aproximadamente los mismos. Luego creamos el proceso en sí. En target abordamos la
función que se utilizará, luego todos los argumentos de la función se pasan en una tupla o lista. Esto debe
hacerse incluso si se pasa solo un argumento; de lo contrario, se emitirá un mensaje de error. Luego iniciamos
el proceso y lo colocamos en la lista definida anteriormente. Así procedemos hasta iniciar todos los procesos.

Luego creamos otra lista en la que se recopilan los resultados (primos). Ahora obtenemos nuevos resultados
del objeto de la cola hasta que se llene nuestra lista. Para ello utilizamos el método get del objeto. Una vez
que tengamos todos los resultados, podemos finalizar los procesos. Para hacer esto, iteramos sobre todos
los elementos de la lista creada inicialmente y usamos terminate(). Finalmente, devolvemos la lista generada.
Es hora de realizar una prueba.

>>> si nombre == ' principal ':


>>> multiprimegen(2, 10)
[50000000000053, 100000000000031, 50000000000099, 100000000000067, 50000000000113,
50000000000117, 100000000000097, 500000 00000143, 50000000000161,
100000000000099]

¿Cuál es la función de la primera expresión, bastante críptica, if name == ' main ':? La respuesta
corta es que nos permite ejecutar el programa actual como un programa independiente, que tenemos que
definir aquí para que el módulo de multiprocesamiento funcione correctamente. Cuando se trabaja con
multiprocesamiento, no es Python sino su sistema operativo el que maneja los diferentes procesos. Esto
se maneja de manera diferente, por ejemplo, en Linux o Windows. De esta forma se trata de un
mecanismo de seguridad que debería ejecutarse en todos los sistemas. El resultado final se ve bien.

En el ejemplo anterior, creamos la paralelización para que varias instancias de la misma función funcionen
simultáneamente y recopilen sus resultados en una cola. Como se muestra, esto puede resultar muy útil si
una sola función fuera demasiado lenta. ¿Qué pasa si queremos una disposición en serie, es decir, múltiples
funciones trabajando juntas para producir un resultado final? Esto podría verse así: La función A produce un
número y lo almacena en una cola. Tan pronto como hay al menos un elemento allí, la función B puede
recuperarlo, modificarlo de otra manera y así producir un resultado final. Incluso una aplicación así no supone
un gran desafío. Sin embargo, necesitábamos una segunda función auxiliar para realizar otra tarea. En este
ejemplo, la segunda función siempre debería multiplicar dos números primos entre sí y proporcionar el
resultado, lo que podría ser un escenario de aplicación en criptografía.

def prime_product(en cola, fuera de cola):


mientras que Verdadero:

prime_a = en [Link]()
prime_b = en [Link]()
cola de [Link](prime_a * prime_b)

86
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

El principio de esta función auxiliar es muy simple. Los números primos se toman de una cola.
Si hay dos disponibles, se multiplican y se transfieren a la segunda cola. La función principal real es
entonces la siguiente:

def serie (núcleos, nfinal):


procesos = []
q1 = Cola()
q2 = Cola()
para el número en el rango (1, núcleos + 1):
inicio = (10**14) // número
proceso = Proceso(objetivo=primegen, args=(inicio, q1))
[Link]()
[Link](proceso)
proceso = Proceso(objetivo=producto_principal, args=(q1, q2))
[Link]()
[Link](proceso)

salida = []
mientras len(salida) < nfinal:
[Link]([Link]())
para proceso en procesos:
[Link]()
salida de retorno

La estructura es muy similar a la primera función. Sin embargo, ahora creamos dos colas (q1 y q2). Solo
necesitamos múltiples procesos para las primeras funciones que generan números primos, porque esto
requiere mucho cálculo. Aquí invocamos nuevamente un bucle. La función que multiplica números primos
al final no está paralelizada, porque esta función es muy rápida. Aquí creamos exactamente un proceso. El
resto de la función es entonces análoga.
Ahora podemos hacer una prueba.

>>> si nombre == ' principal ':


>>> serie(2, 10)
[5000000000006850000000001643, 5000000000013250000000006633,
2500000000011500000000013221, 5000000000019150000000013871, 5
000000000021050000000015939, 5000000000024350000000023541,
2500000000024100000000057681, 5000000000033250000000036557, 5000

El resultado parece correcto. En aplicaciones reales, se debe invertir más tiempo para analizar primero
qué partes de su código son demasiado lentas y merecen más atención. Luego puede intentar hacer que
estos aspectos críticos se ejecuten en paralelo. La implementación puede ser un desafío cuando

87
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Es necesario que numerosas funciones funcionen juntas y es posible que sean necesarias muchas
pruebas y ajustes hasta que todo funcione sin problemas. Aquí debería intentar escribir código
flexible donde pueda ajustar dinámicamente la cantidad de subprocesos. Al realizar pruebas, puede
descubrir
cuántos subprocesos deben reservarse para cada función y qué diseño ofrece el mejor rendimiento
general. Python ofrece una gran variedad de herramientas y funciones adicionales para trabajar con
multiprocesamiento, así que asegúrese de consultar la documentación oficial.

3.4 • Paseo aleatorio

Un paseo aleatorio es un punto u objeto que se mueve aleatoriamente y, por tanto, de forma
impredecible desde su origen. Esto no es necesariamente útil para aplicaciones prácticas, pero es un
maravilloso ejercicio de trigonometría. Queremos simular tal movimiento aleatorio en el plano, es
decir, en dos dimensiones. Al hacerlo, determinamos que nuestro objeto se mueve en un sistema de
coordenadas cartesiano y comienza en el origen (0, 0). Puede moverse en cualquier dirección en
cada paso y siempre debe recorrer una distancia de exactamente 1. De lo contrario, no se aplican
restricciones, por lo que se puede llegar a prácticamente cualquier punto del avión. Por tanto, para
cada paso se debe seleccionar un ángulo en la dirección en la que se va a realizar el paso. Por ejemplo,
si en el sorteo se seleccionara un ángulo de 90 grados, se alcanzaría el punto (0, 1) después del primer paso.

Primero miremos el círculo unitario con radio 1. ¿Cómo encontramos el punto donde el ángulo α
cruza el círculo unitario? Para ello utilizamos el seno y el coseno. Como se muestra, el seno es la
distancia vertical desde el origen hasta el punto; el coseno es la distancia horizontal.
Dependiendo de qué tan lejos nos acerquemos en el círculo, estos valores serán positivos o negativos.
Basándonos en estas relaciones simples, podemos determinar el nuevo punto.

Figura 3.2: Seno y coseno en el círculo unitario. Creador: Martin Thoma (Wikimedia Commons)
Machine Translated by
Google 88
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

tiempo de importación

importar matematicas

importar aleatoriamente

def paseo aleatorio (pasos):


posición = (0, 0)
para i en rango (pasos):
ángulo = [Link]() * 360
xpos = posición[0] + [Link]([Link](ángulo))
ypos = posición[1] + [Link]([Link](ángulo))
posición = (xpos, ypos)
posición de retorno

La función solo toma un argumento: el número de pasos a seguir. En la función, primero definimos el
punto de partida como una tupla y luego iniciamos el bucle principal, que recorre tantos pasos como
lo configuramos. Determinamos un número aleatorio, que se toma del intervalo [0, 1[ como flotante.
Multiplicamos este número por 360 para recibir siempre un valor entre 0 y 360. Esto cubre todas las
posibilidades, siempre que calcules en grados. Este ángulo es ahora la base para la nueva posición.
Usamos dos funciones del módulo math. En primer lugar, hay que convertir el ángulo de grados a
radianes, ya que Python calcula con esta unidad por defecto. Luego podemos insertar el ángulo
convertido en la función trigonométrica deseada y obtener un número. Este número se suma a la
posición actual. Lo único a lo que debemos prestar atención es a que se utilice el eje correcto. Luego,
estas coordenadas se establecen como la nueva posición. Si ejecutamos la función con una secuencia
de pasos suficientemente larga, notaremos que nuestro destino final puede variar mucho.

Esta función sólo nos da un valor de retorno al final, lo cual no es muy espectacular.
¿No sería más interesante mostrar gráficamente el recorrido? Si sólo quieres limitarte a la salida de
la consola, esto ciertamente no es muy bonito, pero es posible. Para ello tenemos que cambiar un
poco la función. Además, son necesarias varias funciones de ayuda. La idea es la siguiente: en la
consola se reservan un cierto número de posiciones de la cuadrícula, divididas en filas y columnas, y
el recorrido se simula mediante un objeto que camina a través de esta cuadrícula. Podemos mapear
dicha cuadrícula usando una lista con sublistas. El número de sublistas en la lista principal es el
número de filas, la longitud de las sublistas es el número de columnas, es decir, el ancho de la
pantalla. Para empeorar las cosas, tenemos que realizar una conversión del sistema de
coordenadas cartesiano original. Por lo tanto, debería mostrarse un punto con las coordenadas (0, 0)
en el medio de la cuadrícula (observe cómo esta tarea es similar a la espiral de Ulam).
Entonces necesitamos una función que encuentre la posición correspondiente en la matriz de lista.
También es necesario considerar qué sucede con el punto si sale de la cuadrícula y ya no se puede
mostrar. O ha desaparecido y la visualización termina o se le impide salir de la cuadrícula, que es una
especie de "muro" que no se puede superar. Comencemos con la función modificada randomwalk().
Ahora que no especificamos el número de pasos al principio, la función puede ejecutarse todo el
tiempo que queramos.

89
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def random_pos(posición, nrows, ncolumns):


mientras que Verdadero:

ángulo = [Link]() * 360


xpos = posición[0] + [Link]([Link](ángulo))
ypos = posición[1] +
[Link]([Link](ángulo)) posición = (xpos,
ypos)
gridpos = postogrid(posición, nrows, ncolumns)
si 0 <= gridpos[1] <= nrows 1 y 0 <= gridpos[0] <= \ncolumns 1:

posición de retorno

La función acepta la posición actual de nuestro punto en movimiento como argumento y devuelve la
posición nueva y actualizada del objeto como valor de retorno. Además, especificamos el tamaño de la
cuadrícula. Aquí utilizamos un bucle que se ejecuta hasta que se encuentra una posición legal, es decir,
una que se encuentra dentro de los límites de la cuadrícula. Elegimos una versión en la que el objeto no puede salir.
En principio, la función es muy similar al primer borrador. La gran diferencia es la función postogrid(), que
aún está por definir y que sirve para convertir la posición del sistema de coordenadas cartesiano al sistema
matricial. A continuación se comprueba si la posición así creada se encuentra dentro de los límites de la
cuadrícula. Si este es el caso, se devuelve, si no, la función comienza de nuevo con otro sorteo aleatorio.
Esto significa que el ciclo se ejecutará hasta que finalmente se encuentre una posición legal. Esto garantiza
que, pase lo que pase, nuestro puntito se queda con nosotros dentro de la parrilla.

Repasemos esto usando un ejemplo simple. Elegimos una cuadrícula con cinco filas y nueve columnas.
Esto ahora está simbolizado en una lista con sublistas. El número de sublistas corresponde al número de
filas, la longitud de cada sublista corresponde al número de columnas.
Se vería así:

matriz = [
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0]]

Ahora queda más claro lo que se quiere decir, puesto que la disposición ya recuerda a una matriz. Un
punto, que en el sistema cartesiano estaría situado en el origen (0, 0), se encontraría por tanto exactamente
en el "centro" de la matriz, que correspondería a la fila 3 y a la columna 5.
Dado que en Python el primer elemento se direcciona con 0, la posición sería matriz[2][4].
El mayor obstáculo además de la conversión real es que esta disposición intercambia las coordenadas
xey, por así decirlo. El primer valor ([2]) especifica la fila, es decir, la coordenada y, el segundo valor ([4])
la posición de la columna, es decir, la coordenada x. Siempre debes tener esto en cuenta, de lo contrario
se producirán errores. La función real puede verse así:
Machine Translated by
Google 90
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

def postogrid(posición, nrows, ncolumns):


xpos, ypos = posición #tupla
descomprimiendo poscolumna = int(xpos +
ncolumnas / 2) posfila = int(ypos + nfilas / 2)
retorno (columnas, filas)

La entrada se proporciona como una tupla en el formato (x, y). Los valores respectivos se extraen y
convierten. Puedes comprobar con el ejemplo mostrado que esto es correcto. Dado que int()
simplemente trunca la parte fraccionaria de un número decimal, hace exactamente lo que tenemos
en mente y siempre redondea. En el caso de la posición de la fila, también hay que recordar invertir
el signo, ya que un valor negativo se ubica más "abajo" en el sistema de coordenadas cartesiano,
pero esto significa que es necesario un índice numérico mayor, porque se refiere a una fila . que se
ubica más al final de la lista en la matriz. Tomemos como ejemplo la posición cartesiana (0, 1). Este
punto se encuentra directamente en el eje y en el rango negativo. En una matriz con cinco filas y
cinco columnas, este punto se muestra en la fila ((1) +2,5) = 3,5, es decir, redondeado 3. Esto es
correcto. Las dos variables ncolumns y nrows se pasan explícitamente.
Ahora todavía falta una función de ayuda que muestre gráficamente la cuadrícula.

def display_grid(partículas, nrows, ncolumns):


pantalla = [[" "] * ncolumnas para i en rango(nrows)]
para elemento en partículas:
xgrid, ygrid = postogrid(elemento, nrows, ncolumns)
pantalla[ygrid][xgrid] = "*"
print("#" * (columnas + 2))
para fila en pantalla:

print(f"#{''.join(fila)}#")
print("#" * (columnas + 2))

Como entrada, la función acepta una lista de todos los objetos o partículas a simular. De este modo se
pueden visualizar varios objetos al mismo tiempo. Primero, se crea una cuadrícula vacía en la pantalla,
lo cual se realiza mediante una comprensión anidada. Luego iteramos sobre todos los elementos en las
partículas y usamos la función de ayuda para convertir correctamente la posición. Luego insertamos un
asterisco en la posición recién calculada en la cuadrícula para marcar el campo como ocupado. Una vez
completado el ciclo, se inserta un asterisco en la posición correcta en la cuadrícula para cada partícula y
se completa la matriz de datos. Ahora sólo falta mostrarlo.

Para hacer esto, primero tenemos que mostrar un borde en la parte superior e inferior de la
cuadrícula, lo cual se hace con el signo numérico. Luego iteramos sobre todas las filas en la pantalla
y usamos cadenas F para mostrar cada fila. Al principio y al final de cada línea, dibujamos otro signo
numérico como límite, seguido del contenido de cada línea, que se ensambla en una cadena usando
join(). Al final, dibujamos el límite inferior para completar la cuadrícula. Ahora podemos ensamblar
todas las piezas en la función principal.

91
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

FPS = 10
def principal(n, nfilas=18, ncolumnas=50):
partículas = [(0, 0)] * n #posible ya que las tuplas son inmutables
mientras que Verdadero:

partículas = [random_pos(p, nrows, ncolumns)]


para p en partículas:
display_grid(partículas, nrows, ncolumns)
[Link](1/FPS)

Definimos los FPS, es decir, los cuadros por segundo, como una constante fuera de la función principal que
tiene tres argumentos: el número de partículas, filas y columnas, que especificamos como predeterminados.
Creamos una lista de partículas, todas las cuales comienzan en el origen. A esto le sigue el bucle principal,
que se ejecuta hasta que finalizamos el programa desde fuera. Aquí iteramos sobre todas las partículas y
aplicamos el algoritmo aleatorio a cada una para que se genere una nueva posición aleatoria. Después de
esto, mostramos la cuadrícula y hacemos una pausa para ver una buena visualización en la consola. Luego
el ciclo se reinicia.

Cuando llames a la función, verás que todos los puntos comienzan cerca del origen y luego se distribuyen
de forma aleatoria y casi uniforme en el campo de juego. Esta es una buena visualización de cómo se
comportan las partículas en una solución (movimiento molecular browniano). La entropía aumenta
únicamente por casualidad y la distancia entre partículas aumenta en promedio. Sólo las fronteras que
fijamos impiden que este proceso continúe infinitamente.

Asignaciones

1. Reescribe la función original para que la conversión de grados a radianes


sea obsoleto y se calcula directamente con radianes.
2. Cambie la función de paseo aleatorio para que se dibujen múltiples partículas distintas. Limitar el
código en unas pocas partículas distintas para que la visualización no sea demasiado borrosa.

3. Cambie la función de paseo aleatorio para que no todas las partículas comiencen en el origen sino de forma aleatoria.
puntos elegidos dentro de la grilla.

3.5 • Juego de la Vida

Game of Life es una simulación sencilla en dos dimensiones inventada por John Conway en 1970. Se trata
de celdas que existen en un sistema de coordenadas cartesiano. Estas células pueden tener exactamente
dos estados (viva o muerta) y seguir unas cuantas reglas básicas. A pesar de estas sencillas reglas, a
veces se crean patrones o elementos complejos que se regeneran cíclicamente y que se mueven por la
pantalla y, por tanto, se parecen a la vida real. El juego es una ilustración de cómo se pueden crear
estructuras superiores a partir de elementos básicos.

El campo de juego o cuadrícula se basa en un plano, que idealmente es infinitamente grande y está
dividido en cajas o celdas. Una celda así puede estar vacía (muerta) o llena (viva). Al principio suele haber
una cuadrícula en la que se rellena aleatoriamente un determinado número de celdas. Cada celda de la
cuadrícula tiene exactamente ocho celdas vecinas. Se aplican las siguientes reglas:

92
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones


Si una célula viva tiene menos de dos células vecinas vivas, muere (soledad).

Si una célula viva tiene más de tres células vecinas vivas, muere (sobrepoblación).

Si una célula viva tiene exactamente dos o tres células vecinas vivas, sigue viva (sociedad).

Si una célula vacía tiene exactamente tres células vecinas vivas, se convierte en una célula viva
(reproducción).

Es bastante fácil implementar estas reglas. En este ejemplo comenzamos con la función principal y luego
creamos las funciones adicionales.

tiempo de importación

importar aleatoriamente

def juego_de_vida(rondas):
grid = [[[Link]() < 0.10 para x en rango(50)]para y en \ rango(18)]

para i en rango (rondas):


draw_grid(cuadrícula)
cuadrícula = actualizar_cuadrícula (cuadrícula)

[Link](0.6)

Primero importamos los módulos necesarios y luego creamos la función principal, en la que el único argumento
es el número de rondas a jugar. Generamos aleatoriamente el tablero al principio con una lista de comprensión
anidada. Cada fila está representada por una lista con 50 columnas. [Link]() genera un número
aleatorio en [0, 1[. Si este número es menor que 0,10, se escribe Verdadero en la lista; en caso contrario, Falso.
De este modo, una media del 10% de todas las células están llenas de True, que interpretamos como una célula
viva. Entonces comienza el bucle principal.
Primero, se muestra la cuadrícula actual. Luego, basándose en las reglas definidas anteriormente, se calcula la
cuadrícula de la siguiente ronda. Después de esto, la función se detiene brevemente y el bucle comienza de
nuevo. Sólo faltan las dos funciones auxiliares draw_grid() y update_grid() . Comenzamos aquí con la función que
actualiza la grilla.

def update_grid(cuadrícula):
nueva_cuadrícula = []
para y, fila en enumerar (cuadrícula):
nueva_fila = []
para x, celda en enumerar (fila):
vecinos = contar_vecinos((x,y), grilla)
si celular y vecinos == 2:
celda = Verdadero

vecinos elif == 3:
celda = Verdadero
demás:
celda = Falso
nueva_fila.append(celda)
new_grid.append(nueva_fila)
devolver nueva_cuadrícula

93
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Esta función acepta la cuadrícula anterior como argumento, es decir, la lista con sublistas. Creamos
la nueva grilla como una lista vacía y ahora la llenamos poco a poco. Para hacer esto, primero
iteramos sobre todas las filas de la matriz de datos. Como necesitamos tanto el contenido de la fila
como el índice de la fila respectiva, usamos enumerate(). Esta función devuelve una tupla de una
lista con el respectivo elemento de la lista y su índice. Veamos un ejemplo:

>>> datos = ["A", "B", "C"]


>>> para índice, elemento en enumerar(datos):
>>> imprimir (índice, elemento)
(0, 'A')
(1, 'B')
(2, 'C')

Esta es exactamente la función que necesitamos. Luego creamos una nueva fila que completamos paso a paso.
Para hacerlo tenemos que iterar sobre cada elemento de la fila, lo cual se hace de la misma manera.
En las variables x e y se muestra la posición de cada celda en la matriz de datos. Llamamos a una
función aún por definir count_neighbors(), que devuelve el número de vecinos vivos para cada
celda. Este número, que puede estar entre 0 y 8, se almacena en vecinos. Ahora entran en juego
las reglas del juego. Si una celda está viva (True) y tiene exactamente dos vecinos, permanece viva.
Pero si la celda está vacía y tiene exactamente tres vecinos, nace, es decir, se pone viva. Si no se
cumplen ambas condiciones, en cualquier caso estará vacío (dead) en la siguiente ronda. Si
trabajamos con una celda de una línea de este tipo, el resultado se escribe en new_row. Si hemos
recorrido toda la fila anterior de esta manera, la fila nueva completa se agrega al tablero. De esta
manera, trabajamos fila por fila y dentro de una fila celda por celda hasta generar completamente el
nuevo tablero. Todavía tenemos que crear la función de ayuda count_neighbors().

def count_neighbors(posición, cuadrícula):


vecinos = 0
para x en (1, 0, 1):
para y en (1, 0, 1):
si x == y == 0:
continuar
xpos, ypos = posición[0] + x, posición[1] + y
si 0 <= xpos < len(grid[0]) y 0 <= ypos < len(grid):
vecinos += grilla[ypos][xpos]

La función toma la posición a probar como una lista o tupla y la cuadrícula actual. Inicializamos el
contador y analizamos todas las posibilidades imaginables para las coordenadas xey.
Evidentemente, sólo hay ocho. Saltamos una posición, es decir, cuando tanto x como y son iguales a 0.
Luego definimos la posición que se va a probar. Si todavía está dentro de los límites de la cuadrícula,
agregamos el contenido del campo respectivo a los vecinos. Dado que Verdadero se evalúa como 1 y Falso como 0,

94
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

esta operación es válida. Finalmente, podemos devolver el número de vecinos. ¡Casi llegamos! Falta la función que
muestra el campo de la cuadrícula. La lógica es muy similar a la tarea anterior, donde se debería mostrar una matriz
de datos similar.

def draw_grid(cuadrícula):
para fila en la cuadrícula:
print("".join("#" si la celda es " " para la celda en la fila))
imprimir("#" * len(fila))

Repetimos todas las sublistas de la lista principal y usamos una cadena F para juntarlas. Si una celda está llena,
mostramos un signo numérico (#); de lo contrario, una cadena vacía. Al final, agregamos una línea separadora para
que obtengamos una buena visualización con cada actualización del campo. Hora de una prueba
correr.

>>> juego_de_vida(30)
##################################################
#
####
####
# #
#

#
## ##
### # ####
## ##
## #
#
#
### ##
## #

##################################################

Si tiene suerte, notará patrones en movimiento o que emergen cíclicamente en la pantalla. Pruébelo unas cuantas
veces más y establezca una semilla para el generador de números aleatorios si desea estudiar algunos patrones
en detalle.

95
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Asignaciones

1. Busque el artículo de Wikipedia sobre el juego de la vida e inspeccione el patrón de un planeador.3


Esta es una figura que parece volar por el campo. Crea una función que inserte este patrón en una
posición aleatoria antes de que comience el juego.

3.6 • Modelado de poblaciones

En este ejemplo queremos construir un modelo ecológico y simular una población. Utilizamos la técnica
del modelado basado en agentes. La idea básica es simular un gran número de agentes que actúan
más o menos independientemente unos de otros, pero que en suma afectan a su entorno. Se trata
principalmente de procesos aleatorios que pueden simularse con cualquier complejidad. Imaginamos un
rebaño de ovejas en un pasto, que al final sólo pueden realizar tres acciones: moverse, comer y
aparearse.

Las reglas básicas de la simulación se resumen a continuación.

• El pasto es un área cuadrada de cualquier tamaño con límites fijos. Los animales no pueden abandonar
el pasto ni la simulación (¡lo siento Neo!). El pasto se divide en celdas de un metro cuadrado. Cada
celda se identifica de forma única mediante un número, por ejemplo, mediante una coordenada xey.

• El pasto está cubierto de hierba que comen las ovejas. Una vez que se ha comido la hierba de una
celda, pasan dos días antes de que otra oveja pueda volver a comer de ella. Si una oveja se para
en una celda cubierta y come, entonces se comerá toda la hierba de esa celda. Cada oveja quiere
comer diariamente. Si no puede comer durante dos días, morirá de hambre.
• Cada oveja se mueve hasta dos metros en cada ronda (en coordenadas x e y; el
La distancia máxima recorrida en una ronda es, por tanto, 2 * sqrt(2).
• Si la distancia entre dos ovejas es inferior a un metro, pueden aparearse. Si se produce el
apareamiento, una de las dos parejas es seleccionada al azar como madre y queda embarazada
durante ocho días. Durante este período ya no puede aparearse. Al final del período "nace" una
nueva oveja. La pareja que no quede embarazada no podrá volver a aparearse en la misma ronda.
Las ovejas hambrientas tampoco pueden aparearse.
• Las ovejas tienen una vida útil de 20 días y mueren después.

Por primera vez hacemos uso de las clases. Las clases son una herramienta poderosa en la
programación orientada a objetos. Sin embargo, los ejemplos analizados en esta introducción suelen
ser tan breves que a menudo no resulta útil utilizar clases. Son ideales para programas más grandes o
más complejos. En este ejemplo, son de gran ayuda porque nos permiten crear un gran conjunto de
objetos que tienen propiedades similares. Por tanto, crearemos una clase de ovejas. Cada oveja es
entonces una instancia de esa clase. También utilizaremos varios métodos. Un método en Python es,
en términos generales, una función que pertenece a un objeto. Ya hemos utilizado varios métodos. Las
listas también son objetos en Python. Un método asociado es append(). Siempre que queramos agregar
un objeto a una lista, aplicamos este método a la lista concreta. Si creamos nuestras propias clases,
también podemos definir métodos asociados que solo se pueden usar con las respectivas instancias de
clase. Primero veamos un ejemplo simple para nuestra clase.
3 [Link]

96
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

clase oveja:
def init (yo):
[Link] = [[Link]() * TAMAÑO, [Link]() * \
TAMAÑO]

[Link] = 0
[Link] = 0
[Link] = 0

En principio, este ejemplo es una clase completa. Estamos utilizando la clase de palabra clave para la definición.
Los nombres de las clases suelen estar en mayúscula. Dentro de la clase definimos primero init () .
Cada vez que creamos una nueva instancia de una clase, esta función se ejecuta automáticamente.
Se utiliza para crear ciertos valores o propiedades básicas que siempre deben estar presentes. Todos los argumentos
que usamos en esta función deben pasarse si queremos crear una instancia. Los guiones bajos dobles antes y
después de init indican que es una función especial, que en Python está marcada de esta manera ("métodos dunder").

Entonces, ¿qué es exactamente el yo aquí? Para funciones que se definen dentro de una clase y, por lo tanto, son
métodos por definición, siempre se debe hacer referencia al objeto implícito al que se aplica la función. Cabe señalar
que este argumento siempre debe incluirse. De lo contrario, podemos programar funciones como antes, pero
debemos tener en cuenta que el efecto de esta función siempre está relacionado con el objeto para el que llamamos
a la función. Esto quedará más claro a continuación.

En el propio init () , definimos un conjunto de variables que caracterizan las "propiedades" de la oveja. Siempre
usamos [Link]. Cuando luego queremos consultar las propiedades de una oveja en particular, queda claro que la
variable no es local, sino que pertenece a una instancia específica. Estas propiedades son la posición de la oveja,
que se determina aleatoriamente cuando se crea, el nivel de hambre actual, si está preñada y su edad. Entonces,
¿cómo podemos crear una oveja concreta, es decir, una instancia de la clase oveja? Como esto:

>>> shaun = Oveja()


>>> [Link]
0
>>> [Link] = 1
>>> [Link]
1

Creamos una nueva instancia llamada shaun y llamamos a la clase para hacerlo. No necesitamos pasar más
argumentos. Incluso si usamos self en el método init como argumento, siempre podemos ignorarlo y no necesitamos
insertar un argumento. Una vez que creamos la instancia, automáticamente se crean las variables asociadas. Queda
claro cómo consultar o cambiar los valores de una instancia específica, de acuerdo con el esquema nombre de
[Link] de variable. Ha llegado el momento de definir varios métodos para modificar las propiedades de
una oveja. Las ovejas deberían poder moverse y comer.

97
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

[Dentro de la clase Oveja]


movimiento definido (yo):
mientras que Verdadero:

x = [Link]ón[0] + [Link]() * 4
2 y = [Link]ón[1] + [Link]() * 4
2 si 0 <= x < TAMAÑO y 0 <= y < TAMAÑO:
romper
[Link]ón = (x, y)

Definimos la función dentro de la clase Sheep, por lo que debemos usar el argumento implícito self nuevamente. No
se requiere ninguna otra entrada para esta función. Definimos un bucle infinito que se ejecuta hasta que se
encuentra un movimiento legal. Esto es necesario porque las ovejas no deben salir de los límites de pastoreo
definidos y algunos movimientos pueden ser ilegales. Se deben determinar un total de dos coordenadas, x e y. En
cada caso, el cambio puede ser positivo o negativo. Por lo tanto, extraemos un número aleatorio de [0, 1[ y lo
multiplicamos por 4 para obtener un valor dentro de [0, 4[. Luego le restamos 2 nuevamente, asegurando que se
puedan crear números tanto positivos como negativos.
Luego comprobamos si el movimiento seleccionado todavía está dentro de los límites. Si es así, salimos del bucle y
realizamos el movimiento; de lo contrario, el bucle comienza de nuevo y se prueban otros números aleatorios. Por lo
tanto, permaneceremos informados el tiempo que sea necesario. Dado que todos los valores se eligen
aleatoriamente, solo tenemos que llamar a la función o aplicar el método a una instancia y se cambia la posición de
esta instancia.
Después de esto, faltan dos métodos más de la clase: uno para comer y una función de ayuda para calcular la
distancia a otra instancia.

[Dentro de la clase Oveja]


Definitivamente comer (yo, hierba):
xpos, ypos = mapa(int, [Link])
si hierba[xpos, ypos] == 2:
[Link] = 0
hierba[xpos, ypos] = 0

Empezamos con la función de comer. Aquí se necesita un argumento, a saber, la información sobre el estado del
pasto. La idea es que el estado de la celda de pasto actual se almacene en el pasto. Primero tenemos que calcular
a partir de la posición actual en qué celda se encuentra la oveja. Para ello, tenemos que cortar la parte decimal del
número real. Si la hierba ha crecido lo suficiente, lo que se indica con el valor 2, las ovejas pueden comer y el
hambre baja a 0. Al mismo tiempo, la celda de hierba respectiva se marca como consumida y recibe el valor 0. Si la
celda actual tiene ya ha sido comido y la condición if no se cumple, no pasa nada. Aquí hemos utilizado map(). Esta
función toma una función y un iterable (como una lista), itera sobre todos sus elementos y aplica la función a cada
elemento. Después de esto, se muestran todos los elementos. Veamos un ejemplo para ilustrar cómo funciona:

98
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

>>> números = [5, 33, 1, 1, 9.22]


>>> lista(mapa(abs, números))
[5, 33, 1, 1, 9.22]

Aquí, una lista llamada números es iterable y la función que usamos es el valor absoluto de un número
(abs), que elimina cualquier signo negativo. A continuación, definimos la función que mide la distancia de
dos ovejas.

[Dentro de la clase Oveja]


distancia def(yo, otro):
xdiff = [Link]ón[0] [Link]ón[0]
ydiff = [Link]ón[1] [Link]ón[1]
retorno (xdiff ** 2 + ydiff ** 2) ** 0.5

Curiosamente, aquí utilizamos dos argumentos implícitos. Seguimos las convenciones y los
llamamos yo y otro. self nuevamente se refiere a la instancia a la que se aplica el método. other es
otra instancia de la misma clase, es decir, una oveja diferente. Usando el teorema de Pitágoras,
simplemente calculamos la distancia entre los dos objetos a partir de sus respectivas posiciones en
el sistema de coordenadas. Necesitaremos esta función para comprobar si dos ovejas pueden
aparearse. Crearemos dos métodos más para consultar rápidamente ciertos estados.

[Dentro de la clase Oveja]


def vivo (yo):
devolver [Link] < 20 y [Link] < 3

definitivamente cachondo(yo):

devolver [Link] == 0 y [Link] == 0

Estos dos métodos nos permiten probar directamente si una oveja todavía está viva o es una posible
pareja de apareamiento. Por ejemplo, horny() solo devolverá True si una oveja no está preñada ni tiene
hambre. Finalmente, agregamos una función de ayuda para mostrar estadísticas después de cada ronda
para que podamos seguir el desarrollo a lo largo del tiempo. Esta función no es un método. Normalmente
lo definimos fuera de la clase. Esto se debe a que esta función no debe aplicarse a una instancia
específica, sino que debe incluir información de todas las ovejas.

99
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def display_statistics(todas las ovejas, ronda):


hambre = preñada = pasto = edad = 0 para ovejas en
todas las ovejas:
hambre += [Link] hambre
+= [Link] si
[Link]ñada > 1: preñada += 1

print("Ronda: ", ronda)


print("Número total de ovejas: ", len(allsheep)) print(f"Hambre promedio:
{hambre / len(allsheep):.2f}") print(f"Edad promedio : {edad / len(allsheep):.2f}")
print("Embarazada: ", embarazada) print("#" * 40)

La función necesita dos argumentos: la lista en la que están almacenadas todas las ovejas (la crearemos a
continuación) y la ronda actual. Los valores se inicializan con 0 y luego se suman para poder calcular los valores
promedio. Luego se generan y se presentan de una forma que nos resulte bastante clara, de modo que podamos
tener una visión general de la población después de cada ronda: cuántas ovejas están vivas actualmente, qué
edad tienen en promedio y qué tan hambrientas están. Con estas herramientas, ahora podemos escribir la
función principal real.

importar tiempo
importar
aleatoriamente desde itertools importar combinaciones
TAMAÑO = 10

simulación def (rondas): hierba =


{(x, y): 2 para x en el rango (TAMAÑO) para y en el rango (TAMAÑO)} allsheep = [Oveja()
para i en el rango (10)] para r en el rango ( rondas): # La
hierba está creciendo para pos en
hierba: si hierba[pos] < 2:
hierba[pos] += 1
[Link](allsheep)

# Muévete y come
corderos = 0
para ovejas en todas las ovejas:
[Link] += 1
[Link] += 1
[Link]()
[Link](hierba) si
[Link] == 8: [Link]
= 0 corderos += 1

• 100
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

elif [Link] > 0:


[Link] += 1
[Link](Ovejas() para i en rango(corderos))
display_statistics(todas las ovejas, r)

# apareamiento

horny_sheep = [oveja por oveja en todas las ovejas si [Link]()]


oveja_cansada = establecer()
para ovejas, compañero en combinaciones(horny_sheep, 2):
si oveja en oveja_cansada o pareja en oveja_cansada:
aprobar

elif [Link](pareja) <= 1:


[Link] = 1
cansado_sheep.update([oveja, compañero])

# Muerte
allsheep = [oveja por oveja en allsheep si [Link]()]
si no todas las ovejas:
romper
[Link](0.7)

Primero importamos todos los módulos necesarios y luego definimos la función real, en la que el único argumento
es el número de rondas a simular. Luego definimos pasto como un diccionario de comprensión, que almacena el
estado del pasto para cada campo. Al principio, cada campo está completamente cubierto de hierba, por lo que
obtiene el valor 2. Luego creamos una lista de todas las ovejas. Entramos en el bucle principal, que se ejecuta
hasta que se calculan todas las rondas o hasta que todas las ovejas hayan muerto. Al inicio de cada ronda
dejamos crecer la hierba. Tenga en cuenta que puede tener un valor máximo de 2. Después de esto, el orden de
las ovejas en la lista se aleatoriza para que no haya efectos de posición en los siguientes cálculos.

Con los corderos creamos una variable en la que contamos cuántas ovejas nacen en la ronda actual. Luego
iteramos sobre todas las ovejas y las dejamos envejecer y pasar hambre, que son simplemente efectos del tiempo.
Después de esto, las ovejas se mueven. Aplicamos el método previamente definido a todas las ovejas. Luego, las
ovejas comen con el segundo método. Ahora comprobamos: si una oveja ha estado preñada durante 8 rondas, se
produce el parto. La oveja ya no está preñada y se incrementan los corderos . Sin embargo, si una oveja está
preñada, pero no lo suficiente, la variable preñada se incrementa en 1. Ahora que hemos tratado a todas las ovejas
de esta manera, podemos agregar oficialmente los corderos a la población. Primero, utilizamos una comprensión
para generar una lista de nuevas ovejas. Luego se agrega a la lista principal usando extend(). Después de eso,
tenemos las estadísticas mostradas.

Sigue la fase de apareamiento. Aquí primero creamos una lista temporal en la que almacenamos todas las ovejas
que están potencialmente disponibles para aparearse (horny_sheep). Esto significa que la oveja no debe tener
hambre ni estar preñada. También creamos un conjunto con cansado_sheep.
Aquí almacenamos todas las ovejas que ya se han apareado y por lo tanto no pueden estar activas ni un segundo.

101
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

tiempo en la ronda actual. Luego usamos combinaciones() para generar todos los emparejamientos
imaginables e iterar sobre ellos. Si uno de los socios potenciales aparece en cansado_sheep, este
emparejamiento se omite directamente con el pase. Si ambos compañeros no se encuentran en este
conjunto y su distancia es menor o igual a 1, se produce el apareamiento. Observe cómo se llama este
método, que tiene dos argumentos (uno mismo y otro). Dado que aplicamos el método a una oveja
específica, self ya se pasa implícitamente, por lo que solo necesitamos insertar el compañero como
argumento. Posteriormente, un animal queda preñado. Como ya asignamos aleatoriamente la lista al
principio, el orden es irrelevante. Finalmente, agregamos ambas ovejas al conjunto, usando update().
Debemos pasar las dos instancias agrupadas en una lista (otras opciones serían en tupla, dict o set).
Finalmente, sigue la fase de muerte. Hacemos una actualización dinámica de la lista de ovejas iterando
sobre la lista anterior con comprensión y seleccionando solo las ovejas que todavía están vivas (otras tienen
exceso de edad o están muertas de hambre). Entonces esta nueva lista se convierte en la lista de ovejas
real. Si esta lista está vacía, podemos salir de la simulación, porque no hay más animales presentes en la
siguiente ronda. En caso contrario, esperamos 0,7 segundos para poder leer la visualización del campo de
juego y luego comenzar la siguiente ronda.

Basándonos en la simulación, podemos rastrear cómo cambia la población cuando modificamos los
parámetros específicos. Si los pastos se vuelven demasiado pequeños y el número de ovejas demasiado
grande, morirán de hambre. A largo plazo, sólo hay dos escenarios: o la población desaparece o se establece
un equilibrio en el que el número de ovejas permanece aproximadamente constante. Esto es poco probable,
ya que la extinción es generalmente más fácil de lograr. Por ejemplo, si el pasto se vuelve demasiado
grande, las ovejas no morirán de hambre, sino que a la larga se separarán (sólo por casualidad), de modo
que los apareamientos serán menos frecuentes y la población eventualmente morirá de vejez. Esto muestra
cuán sensibles pueden ser incluso los sistemas ecológicos más simples.
Por supuesto, esta simulación no es muy realista, ya que no hemos modelado muchos aspectos.
Por ejemplo, en realidad las ovejas no se moverán al azar, sino que permanecerán juntas como rebaños en
grupos más grandes, lo que naturalmente aumenta las posibilidades de apareamiento. También podríamos
simular la aparición de una segunda especie, que podría diezmar la población como cazadores.

Asignaciones

1. Generar un modelo para simular la propagación de una enfermedad infecciosa en una población.
Determinar factores como la probabilidad de infección, movilidad de agentes y mortalidad.
¿Cuántos agentes están infectados? ¿Qué sucede si cambia el número de individuos inmunes en la
población?

3.7 • Dinero Rápido

¿Cuál es la estrategia más rápida para alcanzar tus objetivos? Esta pregunta es ciertamente de gran
relevancia para muchas tareas de la vida. Por lo general, sólo se dispone de una solución sistemática si el
problema es comparativamente simple y existen reglas estrictas. Este es el caso, por ejemplo, de muchos
juegos. Incluso si éstas suelen tener baja complejidad y son comprensibles para los niños, en muchos casos,
una estrategia óptima no es fácilmente reconocible. Python puede resultar útil para esto.
Por tanto, asumimos que no existe ningún algoritmo disponible que permita una solución perfecta. Un
enfoque de fuerza bruta pura tampoco es viable, ya que no podemos probar todas las opciones ni siquiera
con sistemas modernos debido al aumento exponencial de la complejidad. En estos casos se utilizan otros métodos.

102
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

necesario. En el siguiente ejemplo, utilizamos un algoritmo de optimización basado en pura casualidad que probablemente
no encuentre la mejor solución, pero quizás una bastante buena, que puede ser útil en muchas aplicaciones del mundo
real. Pasemos ahora a las reglas del
juego.

1. El jugador abre una sucursal bancaria y debe ganar una determinada cantidad de dinero lo más rápido posible. El
juego se basa en rondas y comienza con la ronda 1. En cada ronda, el jugador recibe una cantidad de 20 más un
bono que corresponde al número actual de rondas (por lo tanto, en la ronda 1 se le acredita un total de 21).

Esta cantidad se paga directamente al comienzo de la ronda.


2. El jugador puede comprar máquinas para imprimir dinero. Cada máquina gana un interés del 5% del saldo actual en
cada ronda. Por lo tanto, si tiene una máquina y un saldo de crédito de 100, la máquina obtendrá un crédito
adicional de 5. Las máquinas generan intereses inmediatamente después de recibir la suma redonda.

3. Diez rondas después de la compra de una máquina, el interés ganado por la máquina
cae. del 5% al 3% (por desgaste).
4. El jugador no posee una máquina al inicio del juego pero puede comprar hasta cinco. La primera máquina cuesta 50.
El precio luego se duplica por cada máquina adicional.

No es difícil implementar estas reglas. En nuestro primer borrador, asumimos que compraremos una máquina nueva tan
pronto como se alcance la cantidad requerida.

juego definido (meta):


ingresos = 20
r=0
saldo = 0
maquinas = []
mientras que el equilibrio <objetivo:
r += 1
saldo += ingreso + r

interés = suma(0,05 si r t <= 10 en caso contrario 0,03 para t en \ máquinas)

saldo += saldo * interés


precio = 50 * 2 ** len(máquinas)
si saldo >= precio y len(máquinas) < 5:
má[Link](r)
saldo = precio
retorno r, máquinas

La función tiene un solo argumento: la suma objetivo a alcanzar. Primero definimos algunas variables, como ingreso por
ronda, ronda actual (r), saldo y una lista vacía en la que almacenamos el momento de compra de las máquinas. A esto le
sigue un ciclo que se ejecuta hasta que se alcanza el total objetivo. Aumentamos el contador r en 1 y recibimos nuestros
ingresos, que también se basan en el número de rondas. Calculamos el interés en una lista por comprensión.

Repetimos todas las máquinas existentes y calculamos cuánto tiempo ha estado disponible una máquina.

103
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

En base a esto, se puede determinar la tasa de interés, que se resume. En el siguiente paso, aplicamos
la tasa de interés al saldo acreedor actual. Si la lista está vacía, el valor es 0.
Luego calculamos el precio de una máquina nueva. Se basa únicamente en la cantidad de máquinas
ya compradas. Luego hacemos una prueba: si nuestro crédito es mayor que el precio de compra y
todavía poseemos menos de 5 máquinas, compramos una. Esta información se agrega
posteriormente a la lista como la ronda en la que se realizó la compra. Tenemos que restar el precio
de nuestro saldo. De esta manera el juego continúa hasta alcanzar el objetivo. Supongamos que tenemos
un objetivo de
5.000. Si ejecutamos esta primera versión estrictamente determinista, obtenemos el siguiente resultado:

>>> game(5000)
(44, [3, 6, 12, 18, 27])

Entonces se necesitan 44 rondas para llegar a la meta. También está claro que compraremos las
máquinas lo antes posible. ¿Pero eso tiene sentido? Supongamos que comprarías la última máquina por
800 justo antes de alcanzar la meta de 5000. En este caso, las máquinas podrían tardar más en
compensar la cantidad faltante, como si simplemente se hubiera esperado y ahorrado.
Por lo tanto, nuestra solución es probar muchas versiones diferentes y probar cuál funciona mejor.
Necesitamos, por tanto, un elemento aleatorio que decida cuándo comprar una máquina. Todavía tenemos
la limitación de que sólo podemos comprar una máquina si nos lo podemos permitir. Procedemos de tal
manera que cada vez que teóricamente podríamos comprar una máquina, lanzamos una moneda y sólo la
golpeamos cuando la moneda muestra el lado correcto. Esto lo hacemos importando el módulo aleatorio y
modificando la línea correspondiente de la siguiente manera:

(...)
si saldo >= precio y len(máquinas) < 5 y [Link](0, 1) == 1:
(...)

Aquí Python "lanza" la moneda por nosotros y compra solo si se extrae 1. Ejecutemos esta versión una
vez y seguramente obtendremos un resultado diferente al determinista que se muestra arriba. En mi
caso, el resultado fue (41, [4, 11, 13, 18, 27]). Por pura casualidad, se produce una pequeña mejora de
44 a sólo 41 rondas. También vemos que las máquinas se compraron un poco más tarde.
Un solo intento no tiene sentido, por lo que deberíamos intentar muchos más. El procedimiento es simple:
ejecute repetidamente la función modificada, almacene los resultados y luego vea qué táctica funciona
mejor. Podemos agregar algunas optimizaciones para acelerar el cálculo. Por ejemplo, una simulación
puede abortarse si se alcanza el mejor resultado anterior porque está claro que no se puede lograr un
resultado mejor. Además, no necesitamos guardar todos los resultados, sino sólo el mejor. De esta forma
evitamos tener que almacenar en caché datos inútiles. La nueva función podría verse así:

104
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

importar aleatoriamente

def juego2(mejor, objetivo):


ingresos = 20
r=0
saldo = 0

maquinas = []
mientras que el equilibrio <objetivo:
si r >= mejor:
regresar Ninguno
r += 1
saldo += ingreso + r

interés = suma(0,05 si r t <= 10 en caso contrario 0,03 para t en \ máquinas)

saldo += saldo * interés

precio = 50 * 2 ** len(máquinas)
si saldo >= precio y len(máquinas) < 5 y \
[Link](0, 1) == 1:
má[Link](r)
saldo = precio
retorno r, máquinas

Solo hemos realizado algunos cambios en comparación con la primera función. Justo al comienzo del ciclo while comprobamos si
ya se ha excedido nuestro mejor valor anterior, luego podemos salir inmediatamente y devolver Ninguno. A continuación hemos
modificado la opción de compra por lo que también se debe extraer el número aleatorio correcto para realizar una compra. Ahora
sólo falta el programa de simulación real.

simulación def(n):
mejor = 999

para i en el rango (n):


salida = juego2(mejor, 5000)
si salida:
mejor, máquinas = producción
volver mejor, máquinas

Establecemos un valor máximo alto al principio para garantizar que se rebaje. Luego iniciamos un bucle en
el que se juegan los juegos reales. Almacenamos el resultado de un juego en la salida y comprobamos si
no es igual a Ninguno. Si este es el caso, el mejor anterior se rebaja y actualizamos el mejor y los datos de
información de compra. Para ello utilizamos el desempaquetado de tuplas. Una vez que se hayan ejecutado
todas las simulaciones, podemos obtener el mejor resultado general. De esta manera somos eficientes y
solo completamos aquellas simulaciones que tienen posibilidades de superar el antiguo récord.

>>> simulación(10 ** 6)
35, [6, 7, 16, 27])

105
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Como puedes ver, después de un millón de partidas jugadas, tenemos el mejor de 35 rondas. Curiosamente,
sólo se compraron cuatro máquinas en total para alcanzar este objetivo, lo que significa que probablemente
no sea una buena idea comprar cinco máquinas si se quiere llegar a 5.000 lo más rápido posible.

Asignaciones

1. Un robot especial produce una placa base por hora. La probabilidad de falla del robot es del 5% por hora
(línea de base). Si falla, no se produce ninguna placa base en esa hora y la reparación demora 6
horas (después de eso, la probabilidad de falla vuelve al 5%). En general, la probabilidad de fallo
aumenta 0,2 puntos porcentuales cada hora.
¿Cuántas placas base produce el robot en promedio por semana (168 horas)?
2. ¿Cuál es la tasa de fracaso máxima de referencia, que es del 5% en la primera tarea, para que
¿Se pueden producir en promedio al menos 120 placas base por semana?

3.8 • Muchos círculos

Se da un número arbitrario de círculos en el plano, que pueden superponerse total o parcialmente. Ahora se
calculará el área total de todos los círculos. Las superposiciones no deben contarse dos veces, por lo que
sumar todas las áreas del círculo no produce el resultado deseado. ¿Cómo se puede solucionar esto?
Tómate un tiempo para pensarlo, porque no es un problema baladí. Una representación gráfica de la tarea
sirve como ayuda.

Figura 3.3: ¿Cuál es el área total de la forma gris? Creador: Bearophile ([Link])

106
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Como suele ocurrir, existen muchos enfoques diferentes. Aunque también existe una solución analítica, es
bastante compleja y requiere muchas matemáticas, por lo que probablemente sea mejor guardarla en un libro de
matemáticas.4 Muchos lectores habrán notado que hemos resuelto una tarea similar antes, concretamente
cuando fue sobre el cálculo de Pi usando estadísticas. ¿Podemos aplicar este método aquí también? Sí, pero
parece lógico que serán necesarios muchos más sorteos aleatorios para obtener un resultado preciso, por lo que
estamos modificando el procedimiento. Podemos resumir la estrategia de la siguiente manera:

1. Primero, se eliminan todos los círculos que se encuentran completamente dentro de otro círculo, lo que acelera
el cálculo más tarde.
2. Luego se recorta el área para que quede el menor espacio en blanco posible. Por lo tanto, los bordes se
acercan lo más posible a la figura, lo que reduce el área total.
3. A continuación, el campo se divide en un número de rectángulos que se puede definir libremente. Por ejemplo,
si definimos que los ejes x e y se dividirán en 20 secciones cada uno, nuestra cuadrícula contendrá 400
rectángulos al final.
4. Por separado para cada uno de los rectángulos generados en el paso anterior, comprobamos si las cuatro
esquinas se encuentran dentro de un círculo. Si este es el caso, se garantiza que toda el área del rectángulo
se encuentra dentro del círculo. En este caso, podemos sumar automáticamente el área completa del
rectángulo al área total y no necesitamos ejecutar una simulación.
5. Si esta condición no se aplica a un rectángulo (de modo que al menos una esquina no está dentro de un círculo
específico), sabemos que se encuentra completamente fuera de un círculo o al menos lo intersecta. En este
caso, iniciamos una simulación para el rectángulo. Dibujamos muchos puntos al azar y comprobamos
cuántos de ellos caen dentro de un círculo. Si aproximadamente el 25% de los puntos terminan dentro de
un círculo, sabemos que aproximadamente el 25% del rectángulo está dentro de un círculo. En este caso,
sumamos el 25% del área rectangular al área total de la figura.
6. Si todos los rectángulos se tratan de esta manera, hemos aproximado el área total. La precisión del resultado
se basa en la cantidad de rectángulos definidos y la cantidad de puntos aleatorios extraídos para cada
rectángulo.
7. El algoritmo es más eficiente que el algoritmo ingenuo porque los rectángulos que se encuentran completamente
dentro de un círculo no requieren simulación, lo que ahorra tiempo de cálculo.

Después de recortar los bordes y crear un patrón de cuadrícula, la figura se ve así:

Para aquellos interesados, consulte las siguientes páginas, que presentan ideas para 4
soluciones: [Link]/a/1667789; [Link]

107
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Figura 3.4: La figura después de recortar los bordes, eliminar todos los círculos que se encuentran completamente dentro de otro
círculo y crear el patrón de cuadrícula.

Para el cálculo numérico, utilizamos los siguientes valores que fueron tomados de Rosettacode.
org.5 Cada fila representa un círculo. Los dos primeros valores son las coordenadas xey del centro del
círculo. El tercer valor es el radio del círculo. Almacenamos estos valores en una lista con sublistas para
representarlos en Python (datos). Para comenzar con el paso 1, codificamos una función auxiliar que elimina
todos los círculos que se encuentran completamente dentro de un círculo más grande. Sin embargo, tenga
en cuenta que esta función no puede eliminar círculos que estén completamente cubiertos por varios círculos diferentes.

coordenada x
Coordenada y Radio
1.6417233788
1.6121789534 0.0848270516
1.4944608174 1.2077959613 1.1039549836
0.6110294452 0,6907087527 0,9089162485
0.3844862411
0.2923344616 0.2375743054
0,2495892950 0,3832854473 1,0845181219
1.7813504266
1.6178237031 0.8162655711
0,1985249206 0,8343333301 0,0538864941
5 [Link]

108
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

1.7952608455 0.6281269104 0.2727652452


1.4168575317 1.0683357171 1.1016025378
1.4637371396 0.9463877418 1.1846214562
0,5263668798 1,7315156631 1.4428514068
1.2197352481 0.9144146579 1.0727263474
0,1389358881 0,1092805780 0.7350208828

1.5293954595 0.0030278255 1.2472867347


0,5258728625 1,3782633069 1.3495508831
0,1403562064 0,2437382535 1.3804956588
0.8055826339 0,0482092025 0,3327165165
0,6311979224 0,7184578971 0.2491045282
1.4685857879 0,8347049536 1,3670667538
0,6855727502 1,6465021616 1.0593087096
0.0152957411 0.0638919221 0.9771215985

desde itertools importar combinaciones


def buscar_distancia(p1, p2):
retorno ((p1[0] p2[0]) ** 2 + (p1[1] p2[1]) ** 2) ** 0,5

def remove_circles(círculos):
eliminar = establecer()
para pares en combinaciones(círculos, 2):
círculo_pequeño, círculo_grande = ordenado(par, clave=lambda c: c[2])
distancia_centros = encontrar_distancia(círculo_pequeño,
círculo_grande) si círculo_grande[2] >= centros_distancia +
círculo_pequeño[2]:
# círculo pequeño se encuentra dentro del círculo grande
[Link](small_circle)

Nuevamente, primero necesitamos una función auxiliar para calcular la distancia entre dos puntos. La función real remove_circles()
sigue. Esta función tiene sólo un argumento, es decir, una lista de todos los círculos. Después de esto, creamos un conjunto en el
que marcamos todos los círculos que deberían eliminarse más adelante. Luego usamos combinaciones() para generar todos los
pares de círculos. El orden en que aparecen los círculos es irrelevante. Para cada emparejamiento, clasificamos los dos círculos
por su radio. Para esto usamos sorted() con una función lambda como clave. Ordenamos por el tercer elemento, es decir, el radio,
como lo definimos en la tabla anterior. Luego calculamos la distancia entre los centros del círculo, usando nuestra función de ayuda
inicialmente definida. Ahora comprobamos si el círculo más pequeño está completamente dentro del más grande. La idea es la
siguiente: si el radio del círculo más grande es mayor que la suma de la distancia entre los centros y el radio del círculo más
pequeño, se demuestra que el círculo más pequeño debe estar completamente dentro del círculo más grande. Para comprender
esto, dibuje algunos ejemplos en papel para aclarar este principio. Si es el caso, agregamos el círculo más pequeño al conjunto y lo
marcamos para eliminarlo. Una vez que hayamos realizado todos los emparejamientos de esta manera, terminaremos devolviendo
solo los círculos que no están en el conjunto. Esto completa este primer paso.

109
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Dado que la función será bastante larga, la dividiremos en otras funciones de ayuda.
Primero, presentamos la función principal real que reúne todo. De esta manera, el principio básico queda claro
desde el principio. Otras funciones se definen posteriormente.

def calcular_total_area(círculos, n, iteraciones):


total_simulations = 0 # Área encontrada usando simulaciones
cajas_total = 0
# Área encontrada usando cajas
cajas_omitidas = 0
puntos_total = 0
círculos = eliminar_circulos(círculos)
xmin, xmax, ymin, ymax = find_circumscribing_rectangle(círculos)
área de caja = ((xmax xmin) * (ymax ymin)) / (n ** 2)
para box_part en iter_parts(xmin, xmax, ymin, ymax, n):
si box_inside(box_part, círculos):
cajas_omitidas += 1
total_boxes += área de caja
demás:
total_points += iteraciones
tasa de acierto = find_hitrate(box_part, círculos, iteraciones)
simulaciones_totales += área de caja * tasa de aciertos
print(f"Proporción de cuadros omitidos: {skipped_boxes / n**2}")
print(f"Número total de todos los puntos extraídos (en miles): {total_points // 10**3}")

Nuestra función tiene tres argumentos: una lista de todos los círculos, el número de secciones en las que
dividiremos cada lado y el número de iteraciones para la parte de simulación. Cuanto mayores sean n y las
iteraciones , más precisa debería ser nuestra estimación. Primero, definimos las variables que se utilizan con fines
contables. En total_simulaciones almacenamos el área total calculada por las simulaciones. De manera similar, en
total_boxes almacenamos el área total que se calcula de forma puramente analítica. Por lo tanto, el área total de la
figura es la suma de estas dos variables. En skipped_boxes contamos cuántos cuadros o rectángulos se calcularon
puramente analíticamente y no se entregaron a la simulación. En total_points almacenamos cuántos puntos
aleatorios simulamos en total. Luego aplicamos la función de ayuda ya creada a la lista de todos los círculos para
eliminar aquellos que están completamente cubiertos y, por lo tanto, pueden eliminarse sin afectar el resultado.

A continuación, recortamos la cuadrícula, para lo cual calculamos las coordenadas xey más extremas. Esto se
hace usando la función find_circumscribing_rectangle() . Esto nos da cuatro nuevas variables que almacenan los
valores más extremos. Usando estos, ahora podemos calcular el área de cada cuadro. Esta es el área total
restante, que dividimos por el número de cajas. Como generamos un número idéntico de secciones para ambos
ejes, este número es el cuadrado de las secciones. Si el área total del rectángulo recortado (¡no la figura!) fuera
100 y tuviéramos un n de 20, el área de cada cuadro sería 0,25 (100/20^2). Como se muestra en la figura anterior,

110
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Ahora necesitamos colocar una cuadrícula en el rectángulo. Para ello, tenemos que calcular los cuatro
puntos de las esquinas de cada una de las cajas resultantes. Esto se hace en la función iter_parts().
Repetimos todos los cuadros así creados. Ahora comprueba: ¿los cuatro puntos de las esquinas se
encuentran dentro de un círculo? Si es así, se confirma que toda la superficie de la caja se encuentra
dentro de un círculo y podemos omitir la parte de simulación para esta caja específica. Esta verificación se
realiza en la función box_inside(). En este caso, aumentamos el contador de casillas omitidas en 1 y
agregamos el área de una casilla al área total de todas las casillas. Si este no es el caso, es decir, al
menos un vértice se encuentra fuera de un círculo, iniciamos una simulación. Para hacer esto, sumamos
el número de nuevas iteraciones al número total y calculamos la tasa de aciertos usando find_hitrate(). El valor
devuelto
La proporción del área dentro de un círculo finalmente se calcula como el producto de la tasa de aciertos
y el área de una caja. Sumamos este resultado al área total de todas las simulaciones.

Casi terminamos. Tenemos dos estadísticas más que podrían resultarnos interesantes. Al final,
calculamos el área final de la figura como la suma de las áreas de simulación y del cuadro. Ahora el
principio está claro: podemos crear las funciones de ayuda que faltan. Empecemos por recortar, es decir,
determinar las posiciones más extremas de la parrilla.

def find_circumscribing_rectangle(círculos):
xmin = min(c[0] c[2] para c en círculos)
xmax = max(c[0] + c[2] para c en círculos)
ymin = min(c[1] c[2] para c en círculos)
ymax = max(c[1] + c[2] para c en círculos)
devolver xmin, xmax, ymin, ymax

El único argumento que necesita esta función es la lista de círculos. Luego encontramos los valores de
xey más extremos usando comprensiones. Para valores de x, por ejemplo, esta es la coordenada x del
centro de un círculo menos el radio. Esto se calcula para los valores xey para mínimo y máximo
respectivamente. Luego devolvemos estos valores como una tupla. El orden en el que se pasan los
valores en la tupla siempre debe ser el mismo, para que las funciones posteriores reciban la asignación
correcta. Luego pasamos a la función que calcula las esquinas de todos los cuadros de la cuadrícula.

def iter_parts(xmin, xmax, ymin, ymax, n):


xtamaño = (xmax xmin) / n
ytamaño = (ymax ymin) / n para
xstep en el rango (n):
para paso y en rango(n):
xmin_part = xmin + xpaso * xtamaño
ymin_part = ymin + ystep * ysize
rendimiento xmin_part, xmin_part + xsize, ymin_part, \
ymin_part + ysize

Esta función acepta los límites calculados previamente, así como el número de secciones en las que se
dividen los ejes x e y. Todas las cajas deben tener el mismo tamaño. El tamaño resulta

111
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

de la diferencia entre el valor máximo y mínimo, que se divide por el número de secciones. Esto determina las longitudes de
los lados de cada caja. Ahora iteramos sobre todas las secciones en las direcciones x e y y las llamamos xstep e ystep
respectivamente. Las esquinas se calculan como el valor mínimo al que se suma el producto del paso por la longitud del
lado. De esta forma, revisamos todas las casillas una por una. Luego devolvemos las cuatro coordenadas de los vértices
como una tupla usando rendimiento en lugar de retorno, por lo que hemos creado un generador. Una vez que hayamos
calculado los cuatro vértices de un cuadro de esta manera, podemos probar si los cuatro se encuentran dentro de un círculo
específico. Si lo hacen, se confirma que el área total de la caja respectiva está dentro de ese círculo.

def box_inside(cuadro, círculos):


xmín, xmáx, ymín, ymáx = caja
para círculo en círculos:

if (find_distance([xmin, ymin], círculo) < círculo[2] y \


find_distance([xmin, ymax], círculo) <círculo[2] y \
find_distance([xmax, ymin], círculo) < círculo[2] y \
find_distance([xmax, ymax], círculo) <círculo[2]): \
devolver verdadero
falso retorno

Esta función toma las coordenadas del cuadro como una tupla y la lista de círculos. Desempaquetamos
la tupla en las cuatro esquinas de un cuadro y luego iteramos sobre todos los círculos. Si la distancia
entre el centro del círculo y una coordenada de esquina es menor que el radio del círculo respectivo, es
seguro que el punto se encuentra dentro del círculo. Si esto se aplica a los cuatro puntos, generamos
Verdadero o Falso en caso contrario. Es importante tener en cuenta la lógica aquí: si la condición si
sólo es verdadera una vez (es decir, para al menos un círculo), podemos detenernos inmediatamente y
devolver Verdadero, ya que se demuestra que las cuatro esquinas se encuentran en una círculo. Si se
viola esta condición para un círculo específico, no terminamos inmediatamente, sino que iteramos sobre
todos los círculos restantes, ya que el cuadro aún podría estar dentro de otro círculo.

Finalmente, tenemos que crear la pieza que ejecuta la simulación. Si al menos un punto de la
esquina no está en un círculo, utilizamos el método aleatorio y determinamos la parte de la caja que
se
encuentra en un círculo. El principio es muy similar a la tarea anterior cuando calculamos estadísticamente Pi.

112
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

importar aleatoriamente

def find_hitrate(cuadro, círculos, iteraciones):


xmín, xmáx, ymín, ymáx = caja
aciertos = 0
para i en rango (iteraciones):
zx = xmin + (xmax xmin) * [Link]()
zy = ymin + (ymax ymin) * [Link]()
para círculo en círculos:

si find_distance((zx, zy), círculo) <círculo[2]:


visitas += 1
romper
devolver visitas / iteraciones

Como argumentos volvemos a utilizar la tupla, que contiene las esquinas del cuadro, la lista de círculos y el
número de puntos a dibujar. Primero, descomprimimos la tupla en las cuatro esquinas. Luego establecemos el
número de aciertos en 0. Ahora comienza la simulación, que se ejecuta hasta que se extraen todos los puntos.
La coordenada x del punto aleatorio es el valor aleatorio de [0, 1[, que se multiplica por la longitud del cuadro.
Sumamos este valor al valor x mínimo.
El procedimiento para el valor y es similar. De esta forma obtenemos un punto aleatorio que se encuentra dentro
del cuadro actualmente en consideración. Ahora comprobamos en todos los círculos de la lista si el punto creado
se encuentra en al menos un círculo, lo que se puede determinar por la distancia al punto central respectivo. Si
este es el caso, para un solo círculo, podemos detenernos inmediatamente y contar el impacto. Una vez que
dibujamos todos los puntos de esta manera, podemos determinar la proporción de puntos que se encuentran
dentro de un círculo. Si este valor es 0,5, por ejemplo, sabemos que, en promedio, la mitad de la caja considerada
se encuentra dentro de un círculo. Luego devolvemos este valor a la función consumidora. Esto completaría
todas las funciones auxiliares y podremos iniciar una ejecución de prueba.

>>> calcular_total_area(datos, 100, 2000)


Proporción de casillas omitidas: 0,7145
Número total de todos los puntos sorteados (en miles): 5710
21.565288978106558

Dado que encontramos la solución analítica en línea para el ejemplo dado, 21.56503660..., podemos concluir
que nuestra aproximación es bastante buena. Además, el tiempo de ejecución del programa es inferior a un
minuto, por lo que podríamos, si fuera necesario, generar un resultado más preciso.

Asignaciones

1. La precisión de nuestra función está determinada por dos parámetros: la cantidad de rectángulos que se
generarán y la cantidad de puntos aleatorios extraídos de cada rectángulo. ¿Qué pasa si solo variamos el
número de rectángulos? ¿Y si sólo variamos los puntos? Analice algunos ejemplos extremos y piense en
lo que determinan estas variables y cómo esto puede afectar el resultado. ¿Cómo se relaciona esto con la
influencia del azar?

113
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

2. ¿Qué es más importante para un resultado preciso, muchos rectángulos o muchos sorteos aleatorios?
Escriba un programa para variar sistemáticamente estas variables y registrar los resultados.
También asegúrese de tener en cuenta la influencia del azar para que los resultados no se
distorsionen demasiado por los valores atípicos.

3.9 • Cerdo

Reglas muy simples pueden crear situaciones endiabladamente complejas, como lo demuestra este
juego de dados para dos o más jugadores, donde el objetivo es sumar 100 puntos. El juego se juega
alternativamente en rondas. En cada ronda, un jugador puede tirar un dado o hacer que su puntuación
de la ronda actual se acredite a su puntuación total. Si tira el dado y recibe un número entre 2 y 6, este
número se suma a su puntuación de la ronda. Si obtiene un 1, pierde todos los puntos que obtuvo en
esa ronda y solo se le acredita un punto. Esto significa que cada jugador recibe al menos un punto en
cada ronda.6 Según estas reglas, cada jugador puede decidir cuánto riesgo quiere correr. Por supuesto,
querrás tirar al menos una vez al comienzo de cada ronda, porque no tienes nada que perder en la
primera tirada. Después de esto, deberías considerar si prefieres mantener la puntuación actual y
guardarla, o apostar y esperar no sacar un 1. Si asumimos que juegan exactamente dos jugadores,
también hay que considerar la puntuación del otro jugador. Si está lejos de la meta de 100 puntos,
puedes jugar de forma más conservadora.

Surge la pregunta de qué estrategia promete los mejores resultados. En este sentido, el juego es sencillo,
ya que el jugador sólo puede elegir entre dos opciones: seguir jugando o guardar. Esta decisión, a su
vez, sólo depende de tres variables: la puntuación del jugador (i), la puntuación del oponente (j) y el total
de la ronda actual (k). Una regla general bastante simple establece que debes jugar cada ronda hasta
haber alcanzado al menos 20 puntos.
El razonamiento es el siguiente: Un dado tiene seis caras, cada número tiene la misma probabilidad.
Entonces sabemos que, en promedio, aparecerá un 1 cada seis lanzamientos. Por lo tanto, en promedio
puedes tirar cinco veces hasta que ocurra este evento. El valor esperado de una tirada, suponiendo que
no se obtenga un 1, es igual a 4 ((2+3+4+5+6)/5). Como cuatro por cinco es 20, en promedio obtendrás
esta puntuación. Por lo tanto, si te detienes antes, regalas puntos. Pero esta regla general llega a sus
límites cuando ocurren algunas situaciones de juego. Supongamos que su oponente tiene 99 puntos,
está claro que tiene garantizado ganar la siguiente ronda, pase lo que pase. Por lo tanto, es sensato tirar
el dado con la mayor frecuencia posible, incluso si tu puntuación aún está lejos de 100.
En teoría, un jugador puede ganar en la primera ronda desde el comienzo del juego ("sólo" tiene que
sacar el número 6 17 veces seguidas, lo cual es extremadamente improbable, pero posible).
Una estrategia óptima de juego debe tener en cuenta estos factores. Desarrollaremos un total de cinco
estrategias diferentes y las probaremos entre sí en un torneo. Dado que podemos simular fácilmente
tiradas de dados en Python, finalmente sabremos qué estrategia maximiza las posibilidades de ganar.

Comencemos con un enfoque muy simple y posiblemente sin sentido: un jugador podría ignorar todos

6 Aquí es donde Progressive Pig se diferencia del original, porque no recibes ningún punto cuando tiras
un uno. Dado que esta versión genera dependencias cíclicas complicadas, discutiremos aquí la versión
del juego ligeramente modificada. Para obtener una solución del original, consulte
[Link]
~tneller/papers/[Link]

114
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

la información que tienes a mano y juega de forma completamente aleatoria. Ella lanzaba una moneda antes de
cada tirada. Si sale cara, continúa jugando. Si muestra cruz, se detiene. Esta norma no parece muy eficaz, pero
nos gustaría incluirla de todos modos. Sirve como límite inferior, por así decirlo. Cualquier otra estrategia que
pierda parece contener graves errores de razonamiento.

importar aleatoriamente

def reproducción aleatoria(mitotal, tutotal):


total redondo = 0
mientras que Verdadero:

si [Link](0, 1) == 1:
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z
demás:

Incluso si esta forma de jugar no utiliza la información sobre los totales de ambos jugadores, los pasamos aquí
como argumentos para luego poder llamar a todas las funciones de la misma manera en el programa del torneo.
Inicializamos el total de la ronda con 0 y comenzamos un bucle que se ejecuta hasta que el jugador saca un 1 o
detiene la ronda. Se extrae un número aleatorio: 0 o 1. Cuando el valor es 1, se tira el dado y el resultado se
suma al total de la ronda. Si el programa obtiene un 1, este valor se devuelve directamente como resultado final.
En resumen, este programa continúa tirando el dado hasta que se saca un 0 o un 1. La siguiente estrategia que
implementaremos parece un poco más sensata pero es bastante arriesgada: un jugador seguirá tirando el dado,
pase lo que pase.
Esto significa que jugará hasta que reciba un 1 o alcance los 100 puntos.

def codicioso(mitotal,tutotal):
total redondo = 0
mientras que total redondo + mitotal < 100:
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z
devolver total redondo

La implementación de esta forma de jugar es aún más sencilla porque sólo existen dos condiciones de salida:
sacar un 1 o ganar el juego. La tercera forma de jugar es la versión más elaborada, explicada anteriormente, en
la que se espera conseguir al menos 20 puntos por ronda a largo plazo.

115
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

se ejecuta y sólo se detiene cuando se alcanza este valor.

def get20(mitotal,tutotal):
total redondo = 0
mientras que roundtotal < 20 y mytotal + roundtotal < 100:
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z
devolver total redondo

Aquí el bucle principal simplemente se ejecuta hasta llegar a 20 o hasta tener más de 100 puntos en
total. Entonces deberías parar en cualquier caso. Aquí hay un breve recordatorio sobre los valores
booleanos: el bucle solo se ejecuta si ambas condiciones son verdaderas. Tan pronto como una de ellas
es Falsa, por ejemplo, Falsa y Verdadera, toda la condición es Falsa y se sale del bucle. Puedes fallar y
sacar un 1 antes de esto, pero si no sucede, solo pararás con los puntos mínimos. Como se discutió
anteriormente, hay una ligera modificación a esta idea. Siempre juegas por al menos 20 puntos, a menos
que tu oponente esté cerca del límite de la victoria. Entonces tienes que jugar más arriesgado. Como
argumentamos, hay un 50% de posibilidades de ganar una vez que alcanzas los 80 puntos. Por lo tanto
establecemos este valor como límite. Entonces, si un jugador tiene esta puntuación o más, el programa
jugará hasta que gane o obtenga un 1.

def arriesgado(mitotal,tutotal):
total redondo = 0
si 80 <= tu total < 100:
mientras que mytotal + roundtotal < 100:
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z
devolver total redondo
demás:
mientras que roundtotal < 20 y mytotal + roundtotal < 100:
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z
devolver total redondo

116
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

En primer lugar, se comprueba qué táctica se debe utilizar. Si el oponente tiene entre 80 y 99 puntos,
el ciclo se ejecuta hasta que se logra una victoria, es decir, al menos 100 puntos (o se obtiene un 1).
De lo contrario, se utilizan las tácticas normales y se deben anotar al menos 20 puntos.
Queda la cuestión de cuál es la estrategia óptima que maximiza las posibilidades de ganar. Se
pueden hacer algunas consideraciones básicas para resolver este desafío. Una vez que un jugador
tiene una puntuación de 99 y le llega el turno, automáticamente gana, ya que consigue al menos un
punto por ronda, por lo que no es necesario analizar la situación en detalle. ¿Qué pasa cuando se
inicia una nueva ronda y te toca jugar con un total de 98 puntos, pero tu oponente tiene 99 puntos,
que denotamos de la siguiente manera: (98, 99, 0)? En este caso, sabemos que ella ganará la
siguiente ronda a menos que usted gane esta ronda. Por tanto, nuestras posibilidades de ganar son
5/6. Si sacamos un 1, terminamos la ronda con 99 puntos y perdemos. Cualquier otra tirada del
dado, sin embargo, nos lleva a la victoria. Entonces podemos deducir: En el estado de (98, 99, 0),
ganamos si lanzamos el dado con p = 5/6. Si ahorramos y dejamos de jugar, ganamos con un 0%
de probabilidad. En este estado, sería mejor tirar el dado. Dado que los resultados son siempre
simétricos, estas consideraciones se aplican al oponente si se encuentra en la misma situación. ¿Qué pasa con
la Ganamos automáticamente si sacamos al menos un dos. Si sacamos un uno, terminamos la ronda y
el oponente encuentra, desde su punto de vista, la situación como (98, 99, 0). Como acabamos de
ver, su probabilidad de ganar es p = 5/6. Como sabemos esto, nuestra probabilidad de ganar en la
ronda anterior, cuando lanzamos el uno, es la probabilidad contraria, es decir, 1 – (5/6) = (1/6). Si
sumamos todas estas probabilidades, obtenemos la siguiente solución:

Sin embargo, si guardamos nuestra puntuación en el mismo estado, tenemos la siguiente probabilidad
de ganar el juego:

Como 1/6 (alrededor del 17%) es menor que 86%, debemos continuar tirando en lugar de quedarnos
en este estado. Dedujimos esto utilizando algunas consideraciones lógicas simples. Si las ampliamos,
podemos desarrollar una regla de decisión para cada estado concebible del juego, que luego servirá
como base para nuestra estrategia de juego óptima. Por lo tanto, nos gustaría tener una
recomendación para cada situación de juego imaginable (i, j, k) para saber si estamos maximizando
nuestras posibilidades al continuar jugando o al detenernos. Podemos crear una base de datos de
este tipo de forma recursiva utilizando las fórmulas que se muestran arriba.7 Con base en estas
consideraciones,
7 Ahora queda más claro por qué esto no es posible con el Pig normal. Si esto es

117
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Ahora podemos formular las ecuaciones que necesitamos implementar y resolver de forma recursiva.

Dado es (i, j, k), es decir, la puntuación actual. Ahora podemos calcular las probabilidades de
ambas opciones y luego elegir la mejor. El carácter recursivo de esta tarea se hace evidente aquí.
Si empezamos desde el principio con el primer movimiento, es decir (0, 0, 0), tenemos que calcular
P(0, 0, 6), por ejemplo (si sacamos un 6 en el primer lanzamiento). Dado que este valor tampoco
existe en la base de datos, primero debe calcularse, lo que significa que debe calcularse otro
valor. Entonces tenemos que buscar recursivamente hasta llegar al caso base, es decir, cuando
el juego termina y se gana o se pierde. Veamos una posible implementación en Python y luego
expliquemos el procedimiento paso a paso.

También es posible ganar cero puntos en una ronda determinada, a veces caemos en dependencias cíclicas.
Las probabilidades necesarias ya no se pueden calcular recursivamente, puesto que se abre una
regresión infinita. Aquí son necesarias otras técnicas, que requieren más matemáticas.

118
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

def buscador de estrategias(wintotal=100):


def wincheck(yo, j, k):
si (i, j, k) en probabilidad:
# La probabilidad ya está disponible
probabilidad de retorno [i, j, k]

si i + k >= wintotal:
# ganar es seguro
regresar 1

elif j >= wintotal:


# pérdida es segura
regresar 0

# Probabilidad al lanzar el dado


p_roll = 1 wincheck(j, i + 1, 0)
para puntos en el rango (2, 7):
p_roll += wincheck(i, j, k + puntos)
p_roll /= 6
# Al guardar la probabilidad de que j
gane p_hold = 1 wincheck(j, i + máx(k,
1), 0) # cual opción es mejor
p_best = máx(p_roll, p_hold)
si p_roll > p_hold:
recomendación[i, j, k] = "rollo"
demás:
recomendación[i, j, k] = "mantener"
probabilidad[i, j, k] = p_best
devolver p_best
probabilidad = {}
recomendación = {}
wincheck(0, 0, 0)
retorno (probabilidad, recomendación)

Creamos la función Strategyfinder(), que al final genera todos los datos que necesitamos para encontrar la
decisión óptima. Suponemos que el total de ganancias es 100. Sin embargo, esto se puede ajustar si
deseas cambiar las reglas. Creamos dos dictados, probabilidad y recomendación. El primero almacena
para cada estado posible (i, j, k) la probabilidad de que el jugador actual gane el juego, el segundo indica si
tirar el dado o esperar para un estado dado (i, j, k) . Para i y j hay lógicamente 100 posibilidades cada uno,
de 0 a 99 inclusive. Para k, es decir, el estado actual de la ronda, hay menos posibilidades. Por ejemplo, si
ya tenemos 95 puntos, no necesitamos considerar una posible puntuación de diez rondas, porque con esta
puntuación ya habrías ganado y no tienes que decidir más. Los valores que se generarán para k, por lo
tanto, dependen de i, por lo que desde el principio no podemos predecir fácilmente cuántos valores se
deben generar. La recursividad que vamos a utilizar se encargará automáticamente de esto.

119
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Repasemos esta función paso a paso. Al principio, se crean los dos dictados vacíos. Luego llamamos a la
función interna wincheck() con nuestro estado inicial, es decir, el valor inicial del juego (0, 0, 0). Esto se
hace "desde fuera" exactamente una vez. Todas las demás llamadas las realiza la propia función y, por
tanto, son recursivas. Ahora ingresamos wincheck().

Las variables i, j y k representan la puntuación total propia, la puntuación total del oponente y la puntuación
propia de la ronda (suma de la ronda). Si ya hay un valor disponible en probabilidad , podemos devolverlo
directamente. Si la suma de i y k es mayor que la suma ganadora necesaria, el juego ya ha terminado y
podemos devolver el valor 1. Por el contrario, si j, es decir, la puntuación del oponente está por encima de
la suma de la ronda, entonces es imposible ganar y el el retorno es 0. Si ninguno de estos dos casos se
aplica, tenemos que calcular la nueva puntuación. Comencemos con la probabilidad de una tirada, que
vamos aumentando paso a paso. Sólo utilizamos las fórmulas que se muestran arriba. Primero calculamos
la probabilidad de que ganemos si sacamos un 1, que es la probabilidad de que el oponente no gane en la
siguiente ronda. Luego calculamos las probabilidades de los otros resultados posibles, es decir, si sacamos
un número entre 2 y 6. Sumamos todos estos resultados y finalmente los dividimos entre 6. Esto calcula la
probabilidad.

Luego llegamos a la probabilidad de ganar si el jugador no continúa jugando sino que aguanta. Como
recibimos al menos un punto, tomamos el valor máximo de 1 o k. Entonces obtenemos el valor 1 o uno
mayor si k es mayor que 1. En principio, esto nuevamente es solo la probabilidad de que el oponente no
gane en la siguiente ronda. Ahora elegimos la mayor de ambas probabilidades y la guardamos en p_best.
Dependiendo de qué opción sea mejor, ingresamos escribir "roll" o "hold" para registrar el resultado. De
manera similar, ponemos el valor numérico en probabilidad. Al final, devolvemos la probabilidad. Dado
que la función se llama a sí misma cuando es necesario, tenemos una función recursiva. Repasemos esto
con algunos ejemplos. Al principio llamamos a la función con (0, 0, 0). Como al principio no hay ningún
valor, se debe calcular este valor. Si ahora pasamos por wincheck() vemos que la primera autollamada
ocurre en p_roll = 1 wincheck(j , i + 1, 0). La autollamada obviamente se realiza con (0, 1, 0), pero este
valor tampoco está presente todavía. Entonces comenzamos una cascada de recursividad, que finaliza
solo cuando la función más interna devuelve un valor. Esto ocurre por primera vez cuando un jugador
alcanza los 100 puntos, es decir, aproximadamente (100, j, k). En algún momento llegamos a esta
situación y obtenemos un valor de retorno para la última recursión creada.

Llegamos a la entrada (99, 100, 0). Este es el valor más temprano con el que un jugador ha ganado. Aquí
el oponente gana, el retorno a la función anterior (99, 99, 0) es por tanto 0, pero ya sabemos que tenemos
garantizado ganar si nos toca. El rendimiento de esta función vuelve a ser 1, por lo que aquí las
probabilidades de ganar se suman para cada número del dado y se dividen por seis al final. Como k es
cero, la primera condición es verdadera y la probabilidad de ganar al mantener también es 1, ya que
recibimos exactamente un punto y llegamos a 100. Al final, escribimos este resultado en el dict para que
se
almacene permanentemente. Generamos el retorno y a partir de ahí se deconstruye la "torre" de
recursividad de abajo hacia abajo. Suena paradójico que comencemos en (0, 0, 0) y cuentemos hasta (99,
100, 0) primero, pero no nos molesta, ya que sólo hay un final de la recursividad y se alcanza el caso base.

Para entender esto, puede resultar útil colocar directamente una declaración impresa como la primera línea
de wincheck() y mostrar (i, j, k). De esta manera podrá ver que el programa primero cuenta hacia arriba y
luego regresa desde arriba. Finalmente, ha llenado los dos dicts, que aquí actúan como bases de datos, con
Machine Translated by
Google 120
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

toda la informacion. Entonces sabrás para cada situación posible si debes tirar el dado o esperar. Ahora solo
nos queda implementar la función como estrategia:

def óptimo(mitotal,tutotal):
total redondo = 0
mientras que Verdadero:

si mitotal + totalredondeado >= 100:


devolver total redondo
res = (mitotal, tutotal, totalredondeado)
si base de datos[1][res] == "mantener":
devolver total redondo
z = [Link](1, 6)
si z == 1:
regresar 1

demás:
total redondeado += z

La estructura básica es similar a las otras estrategias implementadas anteriormente. Una vez que alcancemos
100 puntos o más, podremos salir. De lo contrario, primero verificamos la base de datos para ver si debemos
rodar o mantener. Si la vuelta es parada, damos por finalizada la ronda. En caso contrario se realizará la tirada.
Dependiendo del número que obtengamos, tendremos que salir o aumentar el total de nuestra ronda. Es
importante que la función pueda acceder posteriormente a la base de datos, para lo cual usaremos una variable global.

Con esta información podremos comenzar nuestro torneo. Entonces necesitamos una función que configure
pares de duelos para todos los programas de juego y luego los reproduzca repetidamente para obtener un
promedio de muchos juegos. Para evitar efectos posicionales, siempre jugamos ambas parejas, es decir, A vs.
B y además B vs. A. El jugador que comienza la ronda tiene ventaja porque el oponente no puede alcanzarlo si
el primer jugador ya ha ganado.

121
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

desde itertools import product def torneo


(estrategias, rondas):
base de datos global
base de datos = buscador de
estrategias()
historia = {} para uno mismo
en estrategias:
historia[yo] = {} para el oponente en
estrategias: si uno mismo!
= oponente: historia[yo][oponente] = 0
para estrat0, estrato1 en producto (estrategias, estrategias): si estrat0! =
estrato1:
para r en rango (rondas): p0,
p1 = 0, 0 mientras
que Verdadero:
p0 += estrat0(p0, p1) si p0
>= 100:
historial[strat0][strat1] += 1 descanso

p1 += estrato1(p1, p0) si
p1 >= 100:
historial[strat1][strat0] += 1 descanso

para uno mismo en la historia:


print(self. name ) para
el oponente en la historia[self]: winchance
= 100 * historial[self][oponente] / (rondas \ * 2) print(oponent. name ,

round(winchance, 1)) print("_" * 15)

El torneo sólo tiene dos argumentos: una lista de todas las estrategias de juego y el número de partidos
a jugar por pareja. Precisamos que las bases de datos generadas se consideran variables disponibles
globalmente. De lo contrario, siempre tendríamos que pasar explícitamente esta información. Para
almacenar todos los resultados creamos un dictado, que contendrá varios otros dictados. Para cada
estrategia, generamos un dictado separado, en el que se recopilan todos los oponentes. Aquí sólo
tenemos que cuidar que los juegos se clasifican entre sí porque podemos derivar estos resultados de
manera lógica. Contamos las victorias con respecto a todos los demás programas de esta base de datos
para luego poder calcular la probabilidad de ganar. Así generamos la siguiente estructura de datos:

122
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

>>> historia
{<function randomplay en 0x7fe865008268>: {<function greedy en 0x7fe8650082f0>: 0,
<function get20 en 0x7fe865008378>: 0, <function riesgoso en 0x7fe865008400>: 0, <función óptima en
0x7fe865008488>: 0}, 0}, ... ... ...

Esto parece un poco extraño porque Python proporciona el nombre de cada función así como la dirección
en la memoria, lo cual es irrelevante en este momento. Luego comenzaremos la simulación real. Aquí
usamos product() de itertools, que corresponde a un bucle anidado. De esta manera dejamos que todas
las estrategias compitan contra todas las demás. Nuevamente, clasificamos pares en los que la función
jugaría contra sí misma. Luego repetimos todas las rondas e inicializamos la puntuación de ambos
jugadores con 0, seguido de la simulación real, que se ejecuta hasta que un jugador gana y se alcanza el
descanso . A la hora de almacenar los resultados sólo debemos prestar atención al orden. En el dictado
creado anteriormente, solo guardamos las victorias y, por lo tanto, siempre tenemos que nombrar primero
la función ganadora. Una vez hemos jugado todos los emparejamientos de esta forma, pasamos al análisis.
Para ello, iteramos sobre todos los elementos de la base de datos y solo mostramos el nombre. La dirección
de almacenamiento se puede eliminar con FUNC. name . Después de esto, iteramos sobre todos los
oponentes y producimos una visualización clara al final. Al calcular las probabilidades de victoria, debemos
multiplicar el número de rondas por 2 en el denominador, ya que jugamos todas las parejas dos veces
(para compensar los efectos de posición). Finalmente dejamos que se muestre el resultado.

>>> [Link](1234) >>>


torneo((juego aleatorio, codicioso, get20, arriesgado, óptimo), 5000)
Reproducción aleatoria

codicioso 46.4
obtener20 0.3
arriesgado 0.4
óptimo 0,8

(...)

Así vemos que después de 5.000 rondas, la estrategia randomplay ganó contra los codiciosos en el 46,4%
de todos los juegos, pero sólo en el 0,8% contra la estrategia óptima. A partir de estos números, creamos
una tabla.

Reproducción aleatoria Avaro Obtener20 Arriesgado Óptimo

Reproducción aleatoria 53,6 99,7 99,6 99,2

Avaro 46,4 86.0 85.2 85,8

Obtener20 0.3 14.0 57.2 55.3

Arriesgado 0,4 14.8 42,8 54,7

Óptimo 0,8 14.2 44,7 45.3

Vemos que el óptimo ganó contra todos los demás programas y, por lo tanto, es el ganador de la

123
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

torneo. Lo interesante es que ganó contra el riesgo por un margen relativamente estrecho, lo que
significa que esta estrategia bastante simple no es mucho peor que una jugada prácticamente
perfecta. En este sentido, ni siquiera con un estilo de juego óptimo se debe subestimar el factor
suerte. En el 0,8% de todos los juegos, incluso el azar puro obtuvo resultados mejores que los óptimos. El
segundo lugar lo ocupa el arriesgado, que ganó contra todos los programas excepto el óptimo. El tercer lugar es
para get20, el cuarto lugar para el jugador codicioso y el último lugar para el programa aleatorio, que no lo hizo
mucho peor que el jugador codicioso. En general, estos resultados están en línea con las expectativas iniciales.

Asignaciones

1. Piensa en otra estrategia y agrégala al torneo. ¿Qué tan bien le va


en comparación con los demás?

3.10 • Arranque

Una de las aplicaciones más importantes de la estadística es obtener información sobre una población
mucho mayor a partir de una muestra limitada. Supongamos que desea saber cuánto ganan los
habitantes de una determinada región, lo que podría resultar útil para fines de investigación de mercado.
Sabemos por estadísticas oficiales que en la región viven 85.000 personas. Tienen un ingreso
independiente y por lo tanto forman la población para el análisis. Nuestra pregunta podría responderse
simplemente preguntando a cada persona sobre sus ingresos y luego tomando el promedio de todas
las respuestas. Desafortunadamente, a menudo no es posible preguntar esto a todos los individuos
de la población, principalmente debido a los costos extremos, y muchas personas se negarían a
participar o no estarían disponibles de otra manera. En este sentido, las estadísticas utilizan un truco:
se selecciona aleatoriamente un cierto número de encuestados de la población y se entrevista.
Mediante el proceso aleatorio, se espera que la muestra forme una versión representativa de la
población, pero en una escala más pequeña. Por ejemplo, se podrían seleccionar aleatoriamente
unos 1.000 números de teléfono (suponiendo que cada persona tenga exactamente un número de
teléfono), llamarlos y agregar estas respuestas. Esto reduce considerablemente el esfuerzo. Sin
embargo, ahora tenemos un problema. Como no hemos encuestado a toda la población, debemos
suponer que el valor medio calculado de la muestra pro_bablemente diferirá del valor medio de la
población. Nos referimos a la media poblacional como μ o como el valor "verdadero". Nos referimos
a la media muestral como μ bar, que es la mejor estimación de la población media. La diferencia
entre los dos valores se llama error de muestreo. Este es el error que cometemos porque no
entrevistamos a toda la población. Desafortunadamente, no podemos calcular este error en
aplicaciones reales, porque tendríamos que conocer el valor real, lo que haría que extraer una
muestra no tuviera sentido. Pero ya podemos deducir algunas propiedades obvias: cuantas más
personas se dibujen, menor debería ser el error. Por lo tanto, si pudiéramos entrevistar
aleatoriamente a 5.000 personas en lugar de 1.000, nuestra estimación sería mejor. En otras
palabras, el error de muestreo converge a 0 cuando el tamaño de la muestra converge al tamaño de
la población.

Hasta aquí la media, que es nuestra principal preocupación. Queda un segundo problema. Como ya
podemos prever que la media muestral se desviará del valor real, sería bueno si pudiéramos estimar
aproximadamente qué tan grande será esta desviación. Como hemos visto, la relación con el tamaño
de la muestra es un elemento central de esta estimación. El segundo factor determinante tiene que
ver con la dispersión de los valores en la población. Si nosotros

124
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Si nos atenemos a los ingresos, algunas personas pueden tener ingresos extremadamente altos,
otras extremadamente bajos. Por ejemplo, suponemos que el ingreso mensual más bajo es de unos
400 €, mientras que el límite más alto es difícil de determinar. Si por casualidad un multimillonario
viviera en la región, podría ganar más de 100.000 euros, lo que superaría enormemente el ingreso
medio. En este sentido, ya podemos estimar que los ingresos no se distribuyen normalmente. A partir
de datos oficiales, sabemos bastante bien que estas variables casi siempre están sesgadas hacia la
izquierda. Esto significa que la mayoría de las personas tienen ingresos bastante bajos y hay muchas
menos personas con ingresos muy altos. Sin embargo, no necesitamos profundizar más en esto, ya
que el ingreso es sólo un ejemplo aquí y la distribución real es irrelevante para la tarea. En última
instancia, podemos suponer que la distribución de la variable es bastante desigual y el rango es
amplio. Nos gustaría visualizar esto. En estadística, los histogramas se utilizan a menudo para ilustrar
la distribución empírica
de valores (ver figura 3.5). También podemos hacer esto en Python, aunque nuevamente nos
limitaremos a la salida de la consola. A diferencia de los histogramas normales, los mostraremos con
ejes
intercambiados, es decir, rotarlos, lo que tiene varias ventajas para la visualización y no influye en el
contenido.

Figura 3.5: Se utiliza un histograma para visualizar la distribución de una variable numérica. Los valores de la
variable se trazan en el eje x y la frecuencia relativa de cada contenedor en el eje y.

La idea básica es la siguiente. Primero tomamos los datos y los clasificamos numéricamente. Luego
tenemos que decidir cuántas barras o contenedores queremos usar. Este número depende de las
opciones de visualización en la consola. Dado que cada barra debe tener el mismo ancho, es decir,
mostrarse exactamente con un carácter, en este punto no tenemos ninguna opción de ajuste, a
diferencia de una salida gráfica, donde podemos ajustar el ancho de visualización mediante píxeles.
Por
tanto, decidimos generar un máximo de 20 barras para que todo sea visible sin necesidad de
desplazarse en la consola. Si tenemos muy pocos puntos de datos, es posible que necesitemos menos
barras. No existe un estándar fijo para esto, pero existen numerosos algoritmos disponibles. En este
punto decidimos utilizar la fórmula según Rice, que es la siguiente: k = 2 * n^(1/3)

Aquí k es el número de contenedores a generar y n es el número de puntos de datos. Por


ejemplo, si hay 100 puntos de datos, generaríamos 9,28 barras, que redondeamos a 9.
Machine Translated by
Google

125
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

La altura de las barras depende del número de elementos que se asignan a cada barra. Cuantos más
elementos, mayor será el contenedor. Para determinar esto, primero debemos definir el ancho de cada
barra, que es el rango numérico que debe representar un contenedor. Para hacer esto primero calculamos
el ancho de la variable, que es simplemente la diferencia entre el valor máximo y mínimo.
El ancho de cada barra es entonces el ancho total dividido por el número de barras. Por ejemplo, si
tenemos un ancho numérico de 100 y se van a crear 20 barras, cada barra tendrá un ancho de 5. Si el
valor mínimo fuera 0, la primera barra contendría todos los casos con un valor entre 0 (inclusive) y 5
(exclusivo). De esta forma determinamos cuántos casos se asignan a un bar. A partir del número relativo
de casos por barra, podemos derivar la altura de una barra y mostrarla en la consola.

importar aleatoriamente

de estadísticas importar media, mediana, stdev

def histograma(datos, bins=Ninguno):


"""Dibuja un histograma a partir de datos numéricos"""
valor máximo = máximo (datos)
valor mínimo = mínimo (datos)
ancho total = abs(valor máximo valor mínimo)
ndatos = len(datos)
si no son contenedores: #sin valor proporcionado por el usuario
#Regla del arroz o 20 contenedores máximo

contenedores = int(min((2*datos**(1/3)), 20))


ancho de contenedor = ancho total / contenedores
datos de enlace = []
elementos máximos = 0
para i en el rango (contenedores):
límite inferior = valor mínimo + i * ancho de bin
límite superior = valor mínimo + (i + 1) * ancho de contenedor
elementos = suma(1 para elemento en datos si límite inferior <= \ elemento <límite
superior)
si elementos > maxelementos:
maxelementos = elementos
valor medio = límite inferior + (límite superior límite inferior) / 2
[Link]([elementos, valor medio])

altura máxima = 25
imprimir("" * 40)
para fila en bindata:

binheight = int((maxheight / maxelements) * fila[0])


print(f"{fila[1]: 4.2f} {'#' * altura bin}")
imprimir("" * 40)
imprimir(f"N: {ndata}")
print(f"Media: {media(datos):4.02f}")
print(f"Mediana: {mediana(datos):4.02f}")
print(f"Desviación estándar: {desvestándar(datos):4.02f}")

126
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Importamos algunos módulos y creamos nuestra función que tiene dos argumentos, a saber, los datos
numéricos como una lista y la opción de especificar el número de contenedores. Si el usuario no sobrescribe el
valor predeterminado, estableceremos automáticamente este valor a continuación utilizando el algoritmo.
Encontramos el valor numérico más pequeño y más grande de los datos y el ancho total. Si no se proporciona
ningún valor
predeterminado, utilizamos la fórmula, pero nos aseguramos de que se genere un máximo de 20
contenedores, para los cuales filtramos aquí por el mínimo. También debemos recordar transformar el flotante
nuevamente a un
número entero. Luego se puede calcular fácilmente el ancho de un contenedor. Ahora guardaremos la
información en bindata. Creamos una variable en la que almacenamos el contenedor con más elementos porque
necesitamos
este valor para escalar el histograma más adelante. Luego iteramos sobre la cantidad de contenedores para
crear y establecemos los límites superior e inferior respectivos. Luego calculamos de forma comprensiva cuántos
elementos de los datos caen en el contenedor respectivo. Si este valor es mayor que el valor más grande
conocido hasta el momento, realizamos una actualización de esta variable. También calculamos el centro de un
contenedor, porque usaremos esta información en la pantalla. Al final, colocamos los tres objetos en una lista y
los agregamos a bindata.

Ahora podemos centrarnos en la pantalla. La altura máxima de un contenedor se establece en 25, por lo que
puede haber un máximo de 25 caracteres en una línea. Creamos una línea separadora para una visualización
más clara. Ahora iteramos sobre todos los contenedores en bindata. Escalamos la altura, que es el cociente de la
altura máxima permitida y la altura del contenedor más alto. Esto garantiza que el contenedor más alto siempre
tenga exactamente 25 caracteres de alto y que todos los demás se muestren correctamente en relación con él.
De esta manera utilizamos el área de manera óptima. Una vez calculado este valor, primero mostramos el valor
numérico medio y en la misma línea el contenedor, que ensamblamos con el símbolo numérico (#). Aquí utilizamos
cuerdas F. Al final, volvemos a insertar una línea separadora.
Finalmente, generamos una serie de estadísticas descriptivas y listo. Es hora de realizar una prueba.
Machine Translated by
Google

127
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

>>> [Link](1234)

>>> datos = [ronda([Link](0, 1), 3) para i en el rango(300)]


>>> histograma(datos)

2,64

2.20 ##

1,76 ####

1,32 ########
0,88 ###############

0,44 ###################

0.00 #########################

0,44 ################

0,88 #################

1.32 #####

1.76 ######

2.20 ##

2.64 #

número: 300

Media: 0,01

Mediana: 0,05

Desviación estándar: 0,98

Especificamos que queremos generar una variable distribuida normalmente con un valor medio de 0 y una desviación estándar de 1
con un total de 300 puntos de datos. La semilla hace que el resultado sea reproducible. El carácter de la distribución normal se vuelve
reconocible en el histograma, incluso si existen desviaciones evidentes. Dado que solo generamos 300 casos, esto no es sorprendente.
También queda claro por qué rotamos el histograma. De lo contrario, no podríamos mostrar los valores numéricos directamente
debajo de un contenedor, ya que cada valor ocuparía varios caracteres.

Como ahora tenemos una herramienta para mostrar datos numéricos gráficamente, podemos volver al concepto de arranque. La idea
básica es simple: siempre que queramos cuantificar la incertidumbre de un estimador (media, mediana, desviación estándar, etc...),
pero el error estándar del estimador es desconocido o difícil de calcular, generamos el error estándar mediante extraer repetidamente
nuevas muestras (nuevas muestras) de la muestra existente. Este proceso se denomina entonces arranque. Suponiendo que
queremos calcular el error estándar de la mediana, hacemos lo siguiente: tomamos la muestra y calculamos la mediana real; esta es
nuestra estimación puntual. Luego extraemos nuevas muestras repetidamente con reemplazo de la muestra, siendo el tamaño de las
nuevas muestras idéntico al original. Queda claro que esto significa que algunos elementos se pueden dibujar varias veces y otros
ninguna. Hacemos esto unas 500 veces y calculamos la mediana para cada nueva muestra. Almacenamos estas nuevas medianas
en una lista y calculamos la desviación estándar de esta lista usando stdev() de las estadísticas del módulo. Como han demostrado
los estadísticos, podemos considerar este valor como el error estándar de la mediana muestral y generar otras estadísticas derivadas,

por ejemplo,

128
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

un intervalo de confianza.8 El programa que lleva a cabo estos cálculos se puede escribir de forma muy
compacta.

def arranque(func,datos,n):
valoremp = func(datos)
remuestreos = [func([Link](data, k=len(data))) for i in \ range(n)]

stderr = stdev(nuevas muestras)


ci = (redondo(valoremp 1,96 * stderr, 2), redondo(valoremp + 1,96 * \
estándar, 2))
histograma (nuevas muestras)
print(f"Valor empírico: {empvalue:4.02f} | Bootstrap Stderr: \ {stderr:4.02f} | 95%CI: {ci}")

La función acepta tres argumentos: la función para la cual se va a calcular el error estándar, los datos y
el número de remuestreo que queremos generar. Luego calculamos el valor empírico a partir de los datos
de la muestra. El arranque real se genera en la siguiente línea usando [Link](), que genera
aleatoriamente nuevas muestras con reemplazo. Aplicamos la función de interés a estos remuestreos y
almacenamos los valores generados en una lista.
El error estándar es entonces simplemente la desviación estándar de estos resultados. Después de esto,
también generamos un intervalo de confianza del 95%. También se muestra la distribución empírica de los
resultados del remuestreo, ya que nos permite juzgar la calidad del resultado. Sería deseable una
distribución normal aproximada. Finalmente, se muestran los resultados. Veamos esto en acción.

Supongamos que tenemos datos de pruebas disponibles para 14 personas (pensemos en una prueba de
competencia estandarizada o en los resultados de un examen). Podemos suponer que estas 14 personas fueron
seleccionadas al azar de una universidad y nos gustaría hacer inferencias sobre la competencia del estudiante promedio.
Quienes hayan trabajado anteriormente con estadística recordarán que para muestras tan pequeñas no se
deben utilizar la mayoría de los métodos estadísticos inferenciales y que 30 casos suele ser el límite inferior.
Bootstrapping es más adecuado en tal situación y se recomienda especialmente para muestras pequeñas.
Aplicamos nuestro programa a estos datos y especificamos que estamos interesados en la mediana.

8 La teoría subyacente no se puede explicar en este momento; para este propósito es adecuado el
trabajo estándar, que es fácil de entender incluso con conocimientos estadísticos básicos: Efron,
Bradley; Tibshirani, Robert J (1994): Introducción al Bootstrap. Prensa CRC

129
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

>>> resultados de la prueba = [4, 5, 7, 7, 9, 10, 11, 13, 15, 18, 19, 19, 22, 23] >>> histograma(resultados de la prueba,
5)

5.90 #########################
9.70 ###################
13.50 ############
17.30 ###################
21.10 ######

N: 14
Media: 13.00
Mediana: 12.00
Desviación estándar: 6,37

>>> bootstrap(mediana, resultados de prueba, 2000)

7.38###
8.12 ####
8.88 ########
9.62 ########
10.38 #########################
11.12 ############
11.88 ################
12.62 ##
13.38 ###############
14.12 ##############
14.88##########
15.62###
16.38 ##########
17.12##
17.88 ######
18.62####
19.38 ###
20.12
20,88
21,62

norte: 2000
Media: 12,58
Mediana: 12.00
Desviación estándar: 3,00

Valor empírico: 12.00 | Estándar Bootstrap: 3.00 | IC del 95%: (6,13, 17,87)

En primer lugar, como tenemos la distribución empírica de los datos mostrados, resulta evidente

130
Machine Translated by
Google
Capítulo 3 • Estadísticas y simulaciones

Es evidente que estos no están distribuidos normalmente y los valores más altos ocurren con mucha
menos frecuencia. Ahora vamos a arrancar. Después de extraer 2.000 nuevas muestras, resulta
evidente que la distribución de las medianas generadas corresponde aproximadamente a una
distribución normal, que puede calificarse como aceptable. Más abajo encontramos los resultados.
La mediana empírica es 12, nuestro error estándar estimado es 3. El intervalo de confianza del 95%
oscila entre 6,13 y 17,87. Por lo tanto, suponemos (en términos generales) que la media verdadera,
es decir, la de la población, probablemente estará en este rango.9 Aquí se ilustró cómo podemos
hacer inferencias a partir de una muestra pequeña para una población mucho más grande. En
resumen, el
bootstrapping es una técnica estadística versátil y poderosa que se puede aplicar a muchas áreas diferentes del
aná

Asignaciones

1. Hace unos años un gran comercio tenía la siguiente oferta: por cada 10€ gastados en la tienda,
el cliente recibía un juguete de Pitufo de colección. En total, había 36 figuras diferentes. La
pregunta es, ¿cuánto dinero se debe gastar en promedio en la tienda para terminar con una
colección completa? Escriba una simulación y visualice la distribución resultante usando un
histograma.

El estadístico profesional notará inmediatamente que esta interpretación de 9


intervalos de confianza es discutible. Si quieres saberlo exactamente, debes consultar un libro de
texto de estadística.
Machine Translated by
Google 131
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Capítulo 4 • Datos de texto y cadenas

4.1 • Diccionario

En este ejemplo, se utiliza una lista de todas las palabras en inglés que se encuentran en un diccionario
como fuente para todas las aplicaciones. Existen numerosas fuentes en Internet que ofrecen listas de
palabras gratuitas y legibles por máquina en muchos idiomas. Usamos una lista que se puede descargar
como un archivo de texto.1 Ahora esta lista se guarda en nuestro disco duro, pero primero debe cargarse
en Python. Para ello utilizamos un administrador de contexto, que hoy en día es la mejor opción para
leer datos. La ventaja es que Python gestiona todo el objeto por nosotros y cierra el archivo correctamente
en caso de errores o abortos. Esto nos ahorra tener que cerrar el archivo manualmente al final. Esto
garantiza un manejo más limpio y mejor de archivos y código. El uso es simple:

con open("[Link]", encoding="utf8") como nuevo archivo: datos =


[Link]()
imprimir(len(datos))
imprimir(datos[:20])

En la primera línea especificamos la ruta absoluta o relativa al archivo deseado. Además,


especificamos que el archivo solo debe leerse. No queremos hacer ningún cambio. Especificamos la
codificación del archivo, en este caso UTF8. Con el método readlines() ahora podemos leer todas las
líneas del archivo. Esto es posible porque los datos están estructurados, es decir, una entrada por
línea. Estos
se escriben en una lista y se pueden utilizar como base para análisis posteriores. Obtenemos el
número de elementos de la lista y sus primeros 20 elementos. Cuando ejecutamos el código anterior,
obtenemos el siguiente resultado:

>>> lista(datos[5])
['3', 'r', 'd', '\n']

Para eliminar el salto de línea, debemos eliminar el último carácter de cada elemento de la lista. Además,
utilizamos el método de cadena lower() para convertir todas las palabras a minúsculas en caso de que
se incluyan letras mayúsculas. Podemos hacer esto directamente en una sola expresión. Aquí usamos
dos funciones de cadena: rstrip(), que elimina espacios al final de una cadena, y lower(). Podemos
aplicarlos a cada cadena de forma secuencial y escribir todo de forma muy compacta en una lista por
comprensión. En la segunda ronda, también eliminamos el carácter "'" (apóstrofe) de las palabras, ya
que también puede perturbar nuestros análisis posteriores.
Seguramente la primera opción es el MobyProject gratuito, que proporciona listas de palabras para diferentes
1 idiomas. Desafortunadamente, la página principal está fuera de línea en el momento de la impresión. Las listas de
palabras todavía están disponibles, en parte de otras fuentes. Por lo tanto, parece mejor buscar las fuentes actuales en
Wikipedia: [Link] . También tenga en cuenta que después de la descarga, la lista
puede estar codificada incorrectamente y Python generará un mensaje de error. En este caso, puede ser útil abrir la lista
en un editor de texto y guardarla nuevamente con la codificación UTF8.

132
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

>>> palabras = [[Link]().lower() para línea de datos]


>>> palabras = [[Link]("'", "") para palabra en palabras]
>>> palabras[:5]
['1080', 'décimo', '1º', '2', '2º']

Como vemos, ahora tenemos el resultado deseado, es decir, todas las palabras sin caracteres molestos, en
minúsculas, y todo recogido en una lista. Esta lista de palabras ahora se puede utilizar para todo tipo de
análisis.
¿Cuál es la entrada más corta y más larga de la lista? Pronto descubriremos que hay muchas abreviaturas en
la lista que no recuerdan mucho a palabras. Para eliminarlas, simplemente cree una nueva lista utilizando una
lista por comprensión y elimine todas las palabras de menos de tres letras:

palabras largas = [palabra por palabra en palabras si len(palabra) > 2]

¿Qué pasa si ya no queremos que la lista esté ordenada alfabéticamente, sino por longitud de palabra? Tenemos
que asegurarnos de que Python utilice la clave de clasificación correcta. [Link]() ordenaría los elementos por
tamaño numérico (para números) o alfanuméricamente, es decir, según el diccionario.
Pero ese ya es el caso, por lo que solicitamos la longitud como clave aquí.

>>> palabras [Link](clave=len)


>>> palabras largas[0]
1er
>>> palabras largas[1]
diclorodifeniltricloroetano

Mostramos la palabra más corta y la más larga. Python proporciona una poderosa función de clasificación que
podemos personalizar como queramos. Por ejemplo, ¿qué podemos hacer si queremos una clasificación que
ordene las palabras alfabéticamente desde su final? No se trata simplemente de invertir la clasificación para que
las palabras con Z aparezcan primero, sino de ordenar las palabras que terminan con la letra A, por ejemplo. Para
ello, utilizamos una función lambda anónima que invierte las palabras. Luego estos deben ser ordenados.

>>> palabras [Link](clave=palabra lambda: palabra[::1])


>>> palabras largas[:20]
['1080', 'n/a', 'aaa', 'baa', 'cabaa', 'assbaa', 'chaa', 'mushaa', 'markaa', 'ijmaa', 'naa', 'compaa' , 'saa',
'taa', 'humuhumunukunukuapuaa', 'padre', 'caaba', 'caaba', 'padre', 'caaba']

La función anónima se define directamente dentro del método de clasificación. Por esta razón, estas funciones no
pueden contener la misma complejidad que las funciones regulares, ya que sólo pueden

133
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

constan de una expresión. Sin embargo, pueden resultar extremadamente útiles. Para invertir las palabras
utilizamos un truco de Python con cortes, es decir, la inteligente deconstrucción de cadenas. Tenga en cuenta
que esta clasificación no invierte las palabras de la lista; esto sólo se realiza internamente durante la
clasificación. Los elementos de la lista permanecen intactos.

Para arrojar algo de luz sobre esta función, echemos un vistazo a la siguiente tarea: ¿qué palabras de la lista contienen
con mayor frecuencia la letra "g"? Para saberlo, sólo tenemos que contar con qué frecuencia aparece esta letra en una
palabra y ordenar la lista en consecuencia. Hasta aquí la teoría. Para que la implementación sea más clara, dividiremos
esta tarea en varios subpasos. Primero, una función que cuenta el número de letras:

contador def (cadena, carácter):


suma de retorno (1 para elemento en cadena si elemento == carácter)

f = palabra lambda: suma(1 para el carácter de la palabra si el carácter == "g"])

Dos funciones que proporcionan resultados idénticos. Primero la versión clásica con def(), luego la función anónima,
que todavía podemos abordar aquí con el nombre f. No importa cuál queramos usar para ordenar. Sin embargo, solo
podemos crear la función lambda "sobre la marcha" directamente, por lo que no es necesario haber definido la función
explícitamente antes. Si juntamos todas las partes obtenemos el siguiente código:

>>> palabras [Link](clave=palabra lambda: suma(1 para el carácter en palabra si \


carácter == "g"), reverso = Verdadero)
>>> palabras largas[:3]
['regateo', 'keggmiengg', 'algas']

Ahora está claro exactamente lo que hace la función: cuenta las "g" y devuelve este valor como un número.
Ahora también debería quedar claro cómo funciona la clave . Las computadoras solo funcionan con números,
por lo que todos los demás símbolos deben representar números. Para ordenar por el número de "g", primero
debemos ver cuántas de ellas aparecen en una palabra y usar ese número para ordenar la lista. Por lo tanto,
las cadenas con valores más pequeños se encuentran en la parte superior. Otros con valores mayores están
más abajo. Como queremos saber qué palabras tienen el número más grande , también especificamos
revertir para que la lista aparezca al revés (ordenada de mayor a menor). Tenga en cuenta que esta tarea ya
se puede resolver con una función predefinida, que es más conveniente en la práctica ([Link]("character")).

Asignaciones

1. Escribe una función que reconozca palíndromos, es decir, palabras que tienen la misma secuencia de letras cuando
se leen hacia adelante y hacia atrás (por ejemplo, OTTO). ¿Cuántos palíndromos hay en la lista de palabras en
inglés? ¿Cuál es el palíndromo más largo y más corto?

134
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

2. La lista de palabras se puede utilizar como fuente para un generador de contraseñas. Defina una función
que seleccione aleatoriamente un número determinado de palabras de la lista y las genere. El usuario
también debería poder especificar la longitud mínima y máxima de cada palabra. También debería ser
posible establecer una longitud máxima de la contraseña generada. De este modo, se pueden
generar muchas contraseñas, algunas de las cuales ciertamente son fáciles de recordar.2
3. Definimos la diversidad de una palabra como el número de letras diferentes que contiene. Por ejemplo,
la tolerancia tiene una mayor diversidad que el plátano. ¿Qué diez palabras con al menos seis letras
tienen la mayor (menor) diversidad?
4. Un anagrama está presente cuando las letras de una palabra se reorganizan para formar otra palabra.
Por ejemplo, ESCUCHAR es un anagrama de SILENCIO. Escriba una función que tome una palabra
como argumento y busque en la lista de palabras anagramas coincidentes. Consejo: limite la entrada
a palabras cortas; de lo contrario, la búsqueda puede llevar mucho tiempo.

4.2 • LPS

En este ejemplo, LPS significa subcadena palindrómica más larga, un término de bioinformática.
Se trata del análisis digital de genes en los que los palíndromos desempeñan un papel especial. En nuestro
ADN, los genes están representados por las cuatro letras ATCG, un idioma con un alfabeto de sólo cuatro
letras. Además, entendemos por gen una palabra extremadamente larga, por ejemplo,
CCCTCACTGATCATGGGGCTTTGGGTTAAGTGTA. En este encontramos diferentes subcadenas que son
palíndromos, por ejemplo: CCC o TTGGTT. El objetivo es encontrar el palíndromo más largo dentro de la
secuencia dada. Esta tarea es perfecta para practicar los cortes de listas, es decir, la hábil deconstrucción
de cadenas. Se debe prestar especial atención a los errores uno por uno, que ocurren cuando el índice se
desplaza una posición, es decir, la cadena deseada es demasiado larga o demasiado corta. Como
recordatorio, consideremos brevemente cómo obtener trozos de cuerdas. Es importante recordar que
Python comienza a contar con el índice 0. El último elemento de una cadena, es decir, contado desde atrás,
se selecciona con el índice 1, independientemente de la longitud de la cadena. Para la siguiente tarea,
primero necesitamos una función auxiliar que examine una cadena determinada y pruebe si es un
palíndromo. Aquí
definimos que solo reconocemos subcadenas con al menos dos caracteres como palíndromos, de lo contrario
cada carácter sería un palíndromo en sí mismo.

def is_palindrome(cadena):
si len(cadena) < 2:
elevar AssertionError("La cadena debe tener al menos 2 \ caracteres")

devolver cadena == cadena[::1]

La lógica es simple. Comprobamos si la cadena de entrada es idéntica a su versión invertida. Si es así,


devolvemos Verdadero, en caso contrario devolvemos Falso. Basándose en esta función, ahora se puede
diseñar el programa real. La idea es la siguiente: se corta una subcadena de la cadena. Comenzamos con
toda la cadena y cortamos carácter por carácter desde el final y probamos el corte resultante en busca de
un palíndromo. Una vez hecho esto volvemos a tomar toda la cadena, cortamos el primer carácter del
principio y continuamos con esta cadena como se describe.
2 [Link]

135
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

def lps(cadena):
pal_start = Ninguno
longitud_amigo = 1
longitud = longitud (cadena)
para startpos en rango (longitud):
para posiciones finales en el rango (longitud, posición inicial + longitud_pal, 1):
subcadena = cadena[startpos:endpos]
imprimir (subcadena)
si es_palindrome(subcadena):
pal_start = posición de inicio
pal_length = len(subcadena)
romper
devolver pal_start, pal_length

Primero, creamos la variable pal_start para almacenar el índice inicial del palíndromo más largo. Al
principio, esta variable es Ninguna ya que no sabemos si se encontrará algún palíndromo en la cadena. La
longitud del palíndromo más largo se inicializa con 1 en pal_length.
Como acabamos de definir que un palíndromo debe tener al menos dos caracteres, establecemos el valor
en 1 para que cualquier palíndromo real en la cadena dada anule este valor predeterminado. La longitud
total de la cadena también se calcula y almacena.

Comenzamos con el bucle exterior que recorre todos los caracteres de la cadena de adelante hacia
atrás. Cada posición inicial (startpos) también necesita un índice final (endpos), que se resuelve con un
segundo bucle (interno). Este bucle tiene que ir de atrás hacia adelante, por lo que comienza con la
longitud de la cuerda y corre hasta la suma de la posición inicial y la longitud del palíndromo más largo
encontrado.
Especificamos 1 para indicar que realizamos una cuenta regresiva. De esta forma se forman todas las
subcadenas posibles. Los imprimimos para que podamos rastrear el programa más tarde.
¿Cuál es la idea aquí? Por ejemplo, si el palíndromo más largo encontrado tiene cinco caracteres, podemos
omitir las subcadenas restantes con cinco o menos caracteres, ya que cualquier palíndromo que se
encuentre no puede ser más largo que el mejor actual, por lo que descontamos todos los índices
adicionales si se produce dicha constelación. Después de generar cada subcadena, la probamos para
detectar un palíndromo. Si esta prueba es positiva, realizamos una actualización y configuramos el nuevo
índice de inicio al índice de inicio actual. También podemos actualizar la longitud del palíndromo. Luego
salimos directamente del bucle interno y continuamos con el siguiente índice inicial. Al final, devolvemos el
índice inicial del palíndromo y la longitud del palíndromo, que define el palíndromo de forma inequívoca en
la cadena dada.

Para mostrar la función con más detalle, probamos la cadena (algo inútil) TOTABBA. Es obvio que toda la
cadena no es un palíndromo, sino que contiene dos. Si observa las subcadenas probadas, surge el siguiente
patrón:
Machine Translated by
Google 136
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

>>> lps("TOTABBA")
TOTABBA
TOTABB
TOTAB
TODA
HASTA

OTABA
OTABB
OTAB
SEGURO

TAB
ABBA
(3, 4)

Primero, se prueba toda la cadena y de aquí en adelante, el índice final siempre se desplaza un lugar hacia la izquierda.
Esto continúa hasta encontrar el palíndromo TOT. Internamente, las actualizaciones se realizan y se cierra el bucle
interno. El índice inicial ahora se desplaza una posición hacia la derecha y el algoritmo continúa. Nuevamente, se prueba
toda la cadena que queda y luego el índice final se desplaza hacia la izquierda. Este proceso continúa hasta llegar a
OTAB, que no es un palíndromo.
Dado que a la siguiente cadena solo le quedan tres lugares (OTA), no puede superar el palíndromo TOT encontrado
anteriormente, por lo que el algoritmo se detiene y vuelve a la siguiente opción para el bucle externo, razón por la cual
TABBA es el siguiente candidato probado. Posteriormente se encuentra ABBA, que vence a TOT y se convierte en
ganador. Se devuelven el índice inicial (3) y la longitud del palíndromo (4) y la tarea se resuelve con éxito.

Asignaciones

1. Escribe una función que genere un código genético aleatorio a partir de las letras A, T, C y G.
Cree una cadena de este tipo de 5000 caracteres e introdúzcala en lps(). ¿Cuánto mide el palíndromo más largo
que se encuentra?
2. La subcadena creciente más larga es la sección de una cadena o lista que aumenta continuamente (estrictamente
monótona). Por ejemplo, en la cadena 741249223, la subcadena 1249 es una de esas subcadenas. Supongamos
una lista de n elementos, siendo cada elemento un número natural entre 0 y 999. Escriba una función que
encuentre la cadena ascendente más larga en esta lista y genere el comienzo de esta cadena y su longitud.

4.3 • ECL

Sigamos con la genética y analicemos una tarea relacionada. Nuevamente, las diferentes secuencias de genes se dan
como cadenas que constan únicamente de las cuatro letras A, T, C y G, lo que refleja el código genético.
Una tarea típica es encontrar la subcadena común más larga. Veamos un ejemplo con cinco
genes:

137
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

TAGGCGTCGA
TGCCGATCCC
ACGGATGATA
ACCGATACTC
GACATCCGTC

Cada gen consta de diez letras. ¿Cuánto mide la secuencia más larga común a los cinco genes? La respuesta es, por
ejemplo, CC o CG, es decir, un máximo de dos caracteres comunes.
No habrá una cadena de tres caracteres o más común a todos los genes. Existen diferentes soluciones para este
ejemplo específico, pero tienen la misma longitud. Para la solución del problema, asumimos que todos los genes tienen
la misma longitud y sólo nos interesa la longitud de la cadena común más larga. Si existe más de una solución, se
puede devolver cualquiera de ellas. Cuanto más largos sean los genes, más larga será en promedio la cadena común
más larga. Sin embargo, a medida que aumenta el número de genes, su longitud vuelve a disminuir, ya que la cadena
común debe aparecer en todos los genes simultáneamente. Primero comencemos con una función que genera
aleatoriamente información genética que podemos usar más adelante.

importar aleatoriamente

def create_genes(número, longitud):


alfabeto = "ACGT"
return ["".join([Link](alfabeto, k=longitud)) \
para i en el rango (número)]

La función acepta dos argumentos, el número de genes a generar y la longitud de cada gen. Especificamos el alfabeto
que se utilizará en una cadena. Al final, todo lo que necesitamos es una lista por comprensión, y el trabajo lo realiza
[Link](). Esta función extrae aleatoriamente letras del alfabeto con reemplazo que luego se unen en una
cadena usando join(). Todos los "genes" se recopilan en una lista y se pueden evaluar con la siguiente función.

La idea de solución real en la búsqueda de la secuencia común más larga es seleccionar uno de los genes como
referencia (si la longitud de los genes es idéntica, esto es irrelevante; si las longitudes difieren, se debe seleccionar el
gen más largo). Luego, este gen se deconstruye en todas las subcadenas posibles. Por ejemplo, la cadena LION se
puede dividir en un total de diez subcadenas: L, I, O, N, LI, IO, ON, LIO, ION y el propio LION. Clasificamos estas
subcadenas de largas a cortas, porque podemos detenernos inmediatamente después de encontrar la primera
coincidencia correcta, ya que todas las demás no pueden ser más largas.

138
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

def lcs(todas las cadenas):


referencia = todas las cadenas [0]
probado = establecer()
para longitud en rango (len (referencia), 0, 1):
para pos en rango (0, len (referencia) + 1 longitud):
subsecuencia = referencia[pos:pos + longitud]
si la subsecuencia no está probada:
si todo(subsecuencia en secuencia para secuencia \
en todas las cadenas [1:]):
subsecuencia de retorno
[Link](subsecuencia)
devolver ""

Como asumimos que todos los genes de la selección tienen la misma longitud, elegimos el primero al azar. Creamos un
conjunto en el que almacenamos todas las subcadenas que ya han sido probadas. Creamos un bucle exterior que
determina la longitud de la cuerda. Tomamos la longitud más larga posible, el gen de referencia, y lo reducimos en 1 en
cada ejecución, es decir, contamos hacia atrás.
El bucle interior recorre todas las posiciones imaginables de la cuerda, trabajando de adelante hacia atrás. También
tenemos en cuenta la longitud de la subcadena para no exceder la longitud de la cadena al final. Por ejemplo, si la
cadena a probar es SHIP, se producen las siguientes cadenas: SHIP, SHI, HIP, SH, etc.

Si una cadena es nueva para nosotros, es decir, aún no está disponible en el conjunto, es un candidato potencial. Luego
utilizamos una comprensión y comprobamos si la subcadena está presente en todas las demás secuencias de genes.
Utilizamos all() para probar si todos los valores booleanos generados son verdaderos. Si este es el caso, la función
general devuelve Verdadero y hemos encontrado la solución. Si solo hay un Falso, obtenemos Falso. Luego, la
subcadena se agrega al conjunto como no coincidente y continuamos. Si no hemos encontrado ninguna coincidencia al
final, se devuelve una cadena vacía. Es hora de una prueba.

definición principal():

datos = crear_genes(3, 14)


imprimir (datos)
imprimir(lcs(datos))

Definimos una semilla para el generador de números aleatorios y así garantizamos que siempre se utilicen los mismos
números aleatorios para llamadas repetidas a funciones. Esto suele ser muy útil para la depuración.
El resultado es entonces el siguiente:

>>> [Link](12345)
>>> principal()
['CATCCAGAACGAGC', 'TATCGAGACACTTG', 'ATTTAACTGGAGGT']
MORDAZA

139
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Apéndice: Controlar el flujo de un programa

En general, la programación no es necesariamente matemática sino más bien el arte del pensamiento
lógico y estructurado. Por ejemplo, para una tarea se deben realizar determinadas operaciones, pero en
función del resultado se debe adaptar el flujo del programa. El flujo del programa determina si una
determinada operación se realiza o se omite. Un tipo fundamental de control es si...
otras construcciones que utilizamos todo el tiempo. Las cosas se vuelven más complejas cuando
aparecen construcciones anidadas. Una tarea frecuente es utilizar bucles anidados para encontrar un
resultado determinado. Una vez que lo tenga, querrá salir de todos los bucles directamente y continuar
con el programa principal. Esto a veces es complicado. A continuación se presentan tres ideas básicas
que le permitirán manejar estas situaciones. Los veteranos también recordarán la declaración GOTO
para poder realizar saltos arbitrarios en el programa. Sin embargo, estos ya no están actualizados y
deben evitarse a toda costa. Mientras tanto, hay suficientes alternativas.

Nuestro ejemplo es el siguiente: tenemos tres bucles anidados y estamos buscando un resultado. La
solución se encuentra cuando la suma de tres números es divisible por 31. La primera arquitectura del
programa que discutiremos es subcontratar esta parte del código a una función adicional.
Esto tiene varias ventajas: hace que el código sea más claro, puede usar la nueva función en otros
lugares y, a veces, la depuración es más fácil. La ventaja de las funciones es que, no importa cuántos
bucles se estén ejecutando, una declaración de retorno o rendimiento hace que la función salga
inmediatamente y devuelva un resultado. Un ejemplo puede verse así:

def buscador numérico():


para x en (200, 201, 220):
para y en (77, 88, 99):
para z en (1, 5):
imprimir(x,y,z)
resultado = x + y + z
si resultado % 31 == 0:
resultado de retorno
imprimir(buscador de números())
print("Todos los bucles salieron")

La segunda opción es utilizar una variable de bandera. Este es un booleano, que puede ser Verdadero
o Falso. Una vez que se encuentra el resultado, el valor cambia y los bucles principales saldrán. No
necesitamos una nueva función para esto, pero la desventaja es que el código se vuelve más largo y
hay que verificar cada bucle.

salir = falso #Crear variable booleana


para x en (200, 201, 220):
si se va:
romper
para y en (77, 88, 99):
si se va:

140
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

romper
para z en (1, 5):
imprimir(x,y,z)
resultado = x + y + z
si resultado % 31 == 0:
salir = Verdadero
romper
imprimir (resultado)

Primero configuramos la variable de bandera dejar en Falso y luego ingresamos los bucles. En cada nivel
introducimos una verificación que sale del ciclo respectivo tan pronto como el valor cambia a Verdadero. Tan
pronto como el resultado esté disponible dentro del bucle, la variable se establece en Verdadero y el bucle
más interno se deja con interrupción. Después de esto, la verificación de adentro hacia afuera sale de cada
bucle y regresa al nivel superior del programa.

La última posibilidad es una solución con excepciones. Dado que en realidad están destinados a mostrar o
procesar mensajes de error, algunas personas consideran que controlar el programa con ellos es un mal
uso. La aplicación es la siguiente:

intentar:

para x en (200, 201, 220):


para y en (77, 88, 99):
para z en (1, 5):
imprimir(x,y,z)
resultado = x + y + z
si resultado % 31 == 0:
generar error de aserción

excepto AssertionError:
aprobar

imprimir (resultado)
print("Todos los bucles salieron")

La idea es poner todos los bucles en un bloque de prueba y generar una excepción predefinida cuando se
encuentre la solución (aquí un AssertionError). Tan pronto como esto suceda, será capturado por el bloque
de excepción y podrá especificar cómo debe proceder el programa. Una excepción a continuación es lo
mínimo que debe incluir porque, de lo contrario, se genera otra excepción, que generalmente no es lo que
tenía en mente .

4.4 • Cifrado

Nuestro mundo moderno ya no es concebible sin cifrado digital. No importa si pedimos algo por Internet,
hacemos una transferencia bancaria o nos conectamos a una red inalámbrica.

141
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

hotspot, siempre existen métodos de cifrado complejos que tienen como objetivo evitar que personas no autorizadas obtengan
acceso a nuestros datos o secretos. Por qué el desarrollo y la implementación del cifrado deberían reservarse a absolutos
especialistas se puede ver en el hecho de que casi a diario circulan en los medios nuevos informes sobre el cifrado
defectuoso. Python incluye inherentemente varias formas de cifrar datos y crear hashes. En este ejemplo, queremos ilustrar
cómo se pueden cifrar y descifrar textos de forma primitiva. La idea es cifrar texto plano con una contraseña y así crear un
texto secreto que no es directamente legible y no tiene sentido (el código). Este texto se puede volver a convertir a texto sin
formato, pero sólo si conoce la contraseña correcta. Para ello necesitaremos varias funciones de ayuda.

Primero, debemos explicar el principio de una función hash. Dicha función genera una salida (numérica) para una entrada
arbitraria. Hay varios aspectos de interés: primero, la función debe ser estrictamente determinista: entradas idénticas siempre
deben producir las mismas salidas. No debería haber ninguna similitud obvia entre la entrada y la salida, es decir, no se
puede inferir fácilmente la salida a partir de la entrada y viceversa. Además, la salida siempre debe tener la misma longitud,
independientemente de la longitud de la entrada. Por lo tanto, una función hash también es adecuada para comprimir
información con pérdidas. De manera similar, pequeños cambios en la entrada deberían producir grandes cambios en la
salida. Por último, se considera deseable evitar colisiones.

Esto significa que diferentes entradas producen diferentes salidas y no hay dos entradas diferentes que se correspondan con
la misma salida. Por supuesto, esto no siempre es posible, ya que teóricamente son posibles entradas de cualquier longitud,
pero las salidas tienen una longitud fija. Si el número de entradas teóricas excede el número de salidas, las colisiones son
inevitables. Nuestra propia función hash, muy primitiva, difícilmente cumplirá con estos requisitos. Sirve aquí como
ilustración. Adaptamos el algoritmo de suma de comprobación según John Fletcher.

def generar_hash(cadena):
datos = [ord(elemento) para elemento en cadena]
suma1, suma2 = 0, 0
para elemento en datos:

suma1 = (suma1 + elemento * 11111) % (10 ** 6)


suma2 = (suma2 + suma1) % (10 ** 6)
devolver cadena(suma1) + cadena(suma2)

Primero, convertimos cada carácter de la cadena en un número. Esto es fácilmente posible, porque debido al estándar
Unicode, a cada carácter ya se le asigna un número único, que se puede recuperar en Python mediante ord(). Aquí hay unos
ejemplos:

>>> orden("t")
116

>>> [ord(i) para i en "HOLA"]


[72, 69, 76, 76, 79]

142
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

De esta manera, creamos una lista mediante comprensión, en la que se almacenan todos los valores
numéricos respectivos de la cadena. Inicializamos dos sumas con 0, luego iteramos sobre los números en la
lista y sumamos los valores como se muestra. Como pequeña modificación, también multiplicamos cada
número de la lista por una constante para producir números más grandes y, por lo tanto, resultados más
largos, lo que será útil más adelante. También queremos evitar números que sean demasiado largos y usen
módulo. Finalmente, tenemos dos números que convertimos en cadenas y los devolvemos. Ahora podemos
probar algunos ejemplos.

>>> a = ["Hola", "Hola", "12345678", "12334567", \


"una cadena muy larga se reduce mediante el hash"]
>>> para elemento en a:
>>>
generar_hash(elemento)
511056544289
555500722065
666620533128
611065366463
66236758629

Como puede ver, incluso pequeños cambios en los insumos conducen a grandes cambios en la
producción. Las entradas muy largas se comprimen. Luego usaremos esta función para generar un código
numérico aparentemente aleatorio, pero que se puede producir de manera confiable a partir de la
contraseña. En la práctica se utilizan funciones hash criptográficas como SHA2 o anteriormente MD5. Sin
embargo, su modo de funcionamiento es mucho más complejo que el de nuestro ejemplo, ya que los datos
se procesan directamente a nivel de bits, lo que es más rápido y produce resultados mucho mejores. Hoy
en día, MD5 se considera inseguro, ya que la potencia informática disponible actualmente puede generar
colisiones de forma fiable, que pueden utilizarse para manipular datos. La función hash de Python se
puede llamar con hash().

Ahora que la función hash está implementada, podemos empezar a pensar en la técnica real de cifrado. Aquí
hay que poner en práctica una idea muy sencilla: los caracteres individuales del texto se intercambian varias
veces y aparentemente de forma aleatoria, lo que da lugar a un código sin sentido.
Como ejemplo, se puede mencionar un simple cambio de caracteres: la secuencia de letras HOLA da
como resultado OLLEH cuando se invierte. Esto se puede ver muy rápidamente. Sin embargo, si se
combinan y ejecutan diferentes métodos de transformación uno tras otro, resulta mucho más difícil
reconocer el patrón. Para que este proceso sea estrictamente determinista, se utiliza la contraseña.
Determina qué método se utiliza y cuándo, de modo que sea posible una reversión. Si este proceso no
fuera determinista, los datos ya no podrían descifrarse. Queremos limitarnos a tres funciones diferentes,
que se muestran a continuación directamente como código.

def cambio (cadena de entrada):


devolver cadena de entrada[::1]

def tornado(cadena de entrada):


afirmar len (cadena de entrada) % 2 == 0
Machine Translated by
Google 143
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

""
salida =
para i en el rango (0, len (cadena de entrada) 1, 2):
salida += cadena de entrada[i + 1]
salida += cadena de entrada[i]
salida de retorno

def cremallera (cadena de entrada, reversa = Falso):


afirmar len (cadena de entrada) % 2 == 0
""
salida = si
no es inverso:
para i en rango(0, len(cadena de entrada) //
2): salida += cadena de entrada[i]
salida += cadena de entrada[i 1]
demás:
a = [cadena de entrada[i] para i en el rango(0, len(cadena de entrada), 2)]
b = [cadena de entrada[i] para i en rango(1, len(cadena de entrada), 2)] \
[::1]
para i en rango (len (cadena de entrada) // 2):
salida += a[i]
para i en rango (len (cadena de entrada) // 2):
salida += b[i]
salida de retorno

La primera función simplemente gira una cuerda, como se muestra arriba en un ejemplo. La segunda función
invierte la posición de dos caracteres consecutivos. La palabra SECRETO se convierte así en ESRCTE.

La tercera función es un poco más complicada y tiene el objetivo de sustituir siempre la primera por la última
letra y la segunda por la penúltima… y así sucesivamente. SECRETO se convierte en STEECR. Tenga en
cuenta que la traducción inversa requiere una función especial y no es suficiente para aplicar la misma
función a la cadena nuevamente. Por lo tanto, se debe utilizar un argumento para especificar explícitamente
si se desea lo contrario. Además, sólo se pueden introducir cadenas con un número par de caracteres en
zip() y twister(); de lo contrario, los emparejamientos no funcionarán. Si se crean estas tres funciones, ahora
se puede programar el cifrado real.

importar aleatoriamente

def cifrar (mensaje, contraseña):


mensaje = [Link]()
alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
si len(mensaje) % 2 == 0:
mensaje += "".join([Link](alfabeto, k=20)) + "ZZ"
demás:
mensaje += "".join([Link](alfabeto, k=20)) + "AAA"

144
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

valor hash = generar_hash (contraseña)


funclist = [cambio, cremallera, tornado]

para elemento en valor hash:

resto = int(elemento) % 3
mensaje = listafuncional[resto](mensaje)
mensaje de respuesta

La función toma dos argumentos: el mensaje y la contraseña. Al principio, el texto plano se traduce completamente a letras
mayúsculas. Luego generamos un código aleatorio que se adjunta al mensaje. Esto tiene dos propósitos: primero, aumenta
la longitud efectiva del código para textos muy cortos, lo que aumenta la seguridad. En segundo lugar, garantiza que el texto
a cifrar contenga un número par de caracteres. Para un cifrado más seguro, esta adición probablemente debería ser mucho
mayor, pero en este caso aumentaría innecesariamente la longitud del código impreso. Para hacer esto, primero definimos
un alfabeto y luego usamos [Link]() para generar una selección aleatoria de caracteres. Este apéndice tiene
entonces 22 ó 23 caracteres. Siempre que cortemos el número correcto de caracteres al final, no importa que el código sea
aleatorio y, por lo tanto, no necesariamente reproducible. Al observar los caracteres exactos al final, que son ZZ o AAA,
podemos ver cuánto se debe cortar.

Luego, el valor hash se genera a partir de la contraseña, que se proporciona como una cadena. Ahora tiene lugar el
cifrado real. A cada valor numérico en hashvalue se le asigna una función.
Como sólo hay tres funciones pero diez dígitos, se realiza una reducción utilizando el módulo. Así, al final en reposo sólo son
posibles 0, 1 o 2. Estos se asignan a las funciones definidas en funclist. El orden en el que se aplican las funciones al texto
plano se basa, por tanto, en el hash y, por tanto, en la contraseña. El código generado finalmente se genera. El descifrado es
sólo lo contrario del cifrado. Las respectivas operaciones de contador ahora deben realizarse en orden inverso, basándose
en la misma contraseña.

desde functools importar parcial


def descifrar(código, contraseña):
valor hash = generar_hash(contraseña)[::1]
funclist = [cambio, parcial (cremallera, reverso = Verdadero), tornado]
para elemento en valor hash:

resto = int(elemento) % 3
código = listafunc[resto](código)
si [Link]("ZZ"):
código de retorno[:22]
demás:
código de retorno[:23]

Nuevamente, el hash se genera pero se guarda directamente en orden inverso. El funclista también debe tener

145
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

la misma estructura. También es importante que llamemos a zip() con el argumento inverso para garantizar un
descifrado correcto. Pero como tratamos las funciones como objetos y no queremos pasar más argumentos a
continuación, usamos parcial() del módulo functools para asegurarnos de que la función zip() siempre se llame con
el argumento especial, que es lo que el otro Faltan funciones. Esta forma es más elegante, de lo contrario
crearemos una construcción if...else para pasar (o no pasar) ciertos argumentos dependiendo de la función.

Luego se repiten los números enteros del hash. Finalmente, el apéndice adicional agregado al principio debe
truncarse para que se genere exactamente el mismo texto sin formato. Los caracteres al final, ya sea ZZ o AAA, nos
indican cuántos caracteres se deben eliminar.

Ahora se puede realizar una prueba.

>>> mensaje = "CONOCE A MEATTHEOLDBRIDGEATSEVEN"


>>> contraseña = "misecreto"

>>> código = cifrar (mensaje, contraseña)


>>> código

RMTHOBTYDEKYTSUBIAAEYNMYQEWFTEHDDAEOKEEQTMSWAVPLGA
>>> decodificar = descifrar(código, contraseña)
>>> decodificar
CONOCE A MEATTHHEOLDBRIDGEATSEVEN

>>> afirmar mensaje == decodificar

Obviamente el mensaje cifrado es más largo que el mensaje original, lo que se debe a los caracteres adicionales
insertados. Debido al elemento aleatorio, la cadena cifrada probablemente no sea idéntica cuando se vuelva a
llamar a la función, pero esto es irrelevante para la funcionalidad. Ahora podríamos transmitir esta cadena, por
ejemplo a través de un canal inseguro como una carta, que suponemos será abierta y espiada. Sin la contraseña
asociada, esta
información finalmente no tiene mucho sentido. Si luego ingresamos este texto nuevamente con la contraseña
correcta en decrypt(), obtenemos el mensaje original. Dado que afirmar no causa un error, sabemos que el cifrado y
descifrado se realizaron correctamente. Sin duda, se trata de un cifrado muy primitivo, cuyo propósito es únicamente
ilustrativo.
Sin embargo, es superior a otras técnicas utilizadas en la antigüedad, como el cifrado César.

Asignaciones

1. Siga los pasos individuales del cifrado y considere las debilidades


o puntos de ataque.
2. Seguridad a través de la oscuridad significa que el cifrado es seguro si los algoritmos generadores o las
implementaciones del código se mantienen en secreto. Considere por qué es una mala idea y por qué todos
los métodos de cifrado comunes revelan sus códigos y algoritmos. ¿Hasta qué punto sería inseguro el
algoritmo de cifrado que se muestra arriba si los atacantes lo supieran?
3. Cree al menos una función más que mezcle letras de forma determinista (como
cremallera()). Modifique el código existente para que esta función también se utilice en
el cifrado.
Machine Translated by
Google

146
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

4.5 • Números romanos

Los números romanos no sólo impresionan en documentos o monumentos, sino que son un símbolo de
una de las civilizaciones más famosas de todos los tiempos. El sistema es primitivo y apenas permite
matemáticas superiores. No es un sistema posicional como los números arábigos, sino más bien un
alfabeto numérico aditivo. Por ejemplo, los números arábigos 91 y 19 son completamente diferentes, ya
que la posición de los dígitos es diferente aunque el número de los respectivos dígitos sea el mismo. En
números romanos esto es menos importante, ya que XV y VX, por ejemplo, significan lo mismo (aunque
aquí también existen reglas, aunque solo sea por razones estéticas). Además, existe la particularidad de
que ya en la antigua Roma se debían evitar cuatro carteles idénticos uno al lado del otro. Por esta razón,
el número 4 no se escribió IIII, sino que se utilizó una regla de resta, es decir, se restó del siguiente
carácter superior para que el resultado fuera IV.

En total distinguimos los siguientes caracteres: M (1000), D (500), C (100), L (50), X (10), V (5) e I (1), no
aceptamos números superiores a 3999. para evitar el problema de que necesitamos más caracteres
numéricos. Sólo debemos prestar especial atención a la regla de la resta para no cometer errores.
Existen diferentes enfoques del algoritmo. Puedes integrar un contador que comprueba con qué
frecuencia el carácter a configurar ya está presente en una fila. Si llega a cuatro, puedes borrar los
caracteres anteriores y aplicar la resta.
Esto es muy flexible, pero puede que no sea necesario. Si se examina más de cerca, queda claro que
sólo hay seis casos en los que la regla es necesaria, a saber, para los números 4, 9, 40, 90, 400 y 900.
Estos pueden abordarse por separado, ahorrando así el desarrollo de un algoritmo de verificación.
La idea general de la solución es la siguiente: tome el número arábigo que desea convertir y verifique
cada número romano, comenzando por el más grande. Cuántas veces puedes restarlo del número sin
obtener un resultado negativo. Si tal resta es posible, se realiza el paso y se suma el número romano
correspondiente. Luego se continúa con el resto de la primera resta y el siguiente número romano más
pequeño. De esta forma, el número al final será cero y el número romano se irá acumulando
sucesivamente. Tomando el número 1005 como ejemplo, esto significaría que puedes restar 1000 (el
resto sigue siendo 5), lo que significa que puedes sumar M al resultado. Ahora intentas restar todos los
demás números del resto, lo que sólo funciona para V, es decir, el cinco. Agrega V al resultado y listo
porque el número arábigo se redujo exitosamente a cero. Entonces obtienes el número romano correcto
MV. Si incluye los dígitos mencionados con regla de resta en esta lista, se considerarán igualmente. El
código se ve así:

números romanos = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100,
"C"), (90, "XC"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V "),
(4, "IV"), (1, "Yo")]

def to_roman(entero):
si no es instancia(entero, int) o no 0 <entero <4000:
aumentar ValueError()
""
salida =
para valor, símbolo en números romanos:
mientras que entero >= valor:

147
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

salida += símbolo
entero = valor
salida de retorno

Al principio, definimos el mapeo entre valores numéricos y símbolos, lo cual hacemos como tuplas y
empaquetamos todas las tuplas en una lista que hemos ordenado por valores numéricos en orden
descendente. La función real sigue. Primero desinfectamos la entrada y solo después continuamos.
Inicializamos la salida como una cadena vacía. Ahora iteramos sobre la lista recién creada e iniciamos
un bucle. Este bucle se ejecuta siempre que el número entero de entrada sea mayor o igual que el valor
numérico actual. Si este es el caso, sumamos el signo numérico actual a la salida y restamos el valor
numérico de la entrada. De esta manera, el número romano se construye sucesivamente y el valor de
entrada se reduce a 0. Entonces terminamos. Podemos probar el procedimiento usando el ejemplo 2039.
Comencemos en 1000, que es más pequeño que la entrada, por lo que se reduce a 1039 y se agrega M
a la salida. 1039 sigue siendo mayor que 1000, por lo que terminamos con MM y 39. 1000 ya no cabe en
39 y revisamos la lista hasta que encontramos 10, que cabe en 39 tres veces, lo que nos lleva a MMXXX.
Falta el 9, que se procesa con IX. El resultado final es MMXXXIX. La retransformación sigue un
procedimiento muy similar.

def de_roman(romano):
si no es instancia (romana, str):
aumentar ValueError()
salida = 0
para valor, símbolo en números romanos:
mientras que [Link](símbolo):
salida += valor
romano = romano[len(símbolo):]
salida de retorno

La entrada ahora debe ser un número romano como una cadena. La salida será un número entero, que
inicializamos con 0. Nuevamente, iteramos sobre las tuplas de valores y símbolos numéricos definidos al
principio e iniciamos un bucle. Usando comienza con (), verificamos si la cadena dada comienza con un
carácter numérico determinado. Si este es el caso, agregamos el valor respectivo y eliminamos los
caracteres. Hay que tener cuidado porque un personaje puede estar formado por uno o dos personajes
(como IX por 9). Entonces cortamos uno o dos caracteres al principio de la cadena, lo cual hacemos con
un segmento. Tomemos MMXXXIX nuevamente como ejemplo. Como M está presente, agregamos 1000
a la salida y eliminamos M. Esto sucede un total de dos veces, lo que nos lleva al número 2000 y al
carácter restante XXXIX. Luego pasamos por los personajes hasta llegar a la X, aquí sucede lo mismo
tres veces, lo que nos lleva a 2030 y IX. Encontramos IX en la lista, sumamos 9 al resultado y obtenemos
la cadena vacía, así terminamos. Así que gradualmente hemos ido acumulando el número 2039.

Dado que el ejemplo muestra una inversión perfecta en cada caso, podemos probar nuestras funciones para

148
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

consistencia. Si convertimos un número arábigo en un número romano y viceversa, debe surgir el número original.
Esto no prueba que nuestro procedimiento sea siempre correcto, pero muestra que la lógica es consistente y que
efectivamente se produce una retransformación correcta.
Como sólo son posibles los números entre 1 y 3999, podemos probarlos todos.

para i en el rango (1, 4000):


afirmar i == from_roman(to_roman(i))

4.6 • Aritmética de coincidencias

Un tipo popular de rompecabezas tiene que ver con coincidencias y aritmética. Se da una ecuación pero es
incorrecta, por lo que las matemáticas no funcionan. Todos los números y caracteres de esta ecuación están
representados por coincidencias. El lector tiene que mover un determinado número de cerillas y así corregir la
ecuación. Como base utilizamos lo siguiente:

185 + 15 = 270

Las matemáticas son claramente defectuosas. La tarea consiste en girar exactamente una coincidencia para que
la ecuación al final sea correcta. El número de coincidencias utilizadas debe seguir siendo el mismo. No podemos
eliminar ninguno. Los números y los operadores aritméticos pueden cambiar igualmente; por ejemplo, el signo
más podría cambiar a un signo menos. Suponemos que se permiten todos los dígitos del 0 al 9, así como los
signos más, menos e igual. Como reemplazo "digital" de los partidos, utilizamos una pantalla de siete segmentos
(ver figura 4.1).

Figura 4.1: Los diez dígitos se representan digitalmente mediante una pantalla de siete segmentos.
Fuente: [Link]

Por ejemplo, el número 1 consta de dos coincidencias, el número 2 de cinco, y así sucesivamente. La idea es la
siguiente: utilizamos un enfoque de fuerza bruta para probar sistemáticamente todas las opciones posibles. Por
supuesto, no queremos probar todas las ecuaciones, pero tenemos que limitar nuestro rango de búsqueda. El original

149
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

La ecuación sirve como ayuda. Según las directrices sólo podemos mover una pieza de madera. Al final sólo
hay dos posibilidades: movemos un trozo de madera sólo dentro de un dígito (por ejemplo, cuando un 2 se
convierte en un 3), o cogemos un trozo de madera de un dígito y lo sumamos a otro dígito (por ejemplo , cuando
le robamos un trozo de madera al 8 para que se convierta en un 0 y luego ponemos la madera en un 1 que se
convierte en un 7). Podemos recopilar manualmente todas esas transformaciones posibles y dividirlas en dos
categorías.

n_same = ["1+", "+=", "23", "35", "90", "60", "69"]


n_diff = ["+", "=", "17", "39", "56", "59", "68", "98", "08"]

La explicación es la siguiente: podemos convertir el 1 en un signo más volteando una de las dos coincidencias,
de modo que el número de coincidencias en el dígito original permanezca constante. Si queremos convertir un
signo menos en un signo más, tenemos que añadir un trozo de madera (o restarlo si la dirección es al revés).
En n_diff es importante que el personaje que necesita menos coincidencias siempre se coloque primero y luego
el que tiene exactamente una coincidencia más. Después de esto, necesitamos diferentes funciones de ayuda.
Por ejemplo, queremos realizar reemplazos en cadenas.

def reemplazar_en_index(cadena, índice, carácter):


devolver cadena[:índice] + carácter + cadena[índice+1:]

Tiene tres argumentos: la cadena en la que se va a reemplazar algo, la posición en la que se quiere reemplazar
y el carácter a usar como reemplazo. La aplicación es la siguiente:

>>> reemplazar_at_index("Casa", 0, "M")


Ratón

Entonces necesitamos una función que realice sistemáticamente todas las sustituciones. Lo dividimos en dos
partes: una reemplaza configuraciones en las que el número de sticks por personaje se mantiene constante y
la otra realiza reemplazos en las que cambia el número de personajes.
Podemos ver por qué esto tiene sentido cuando juntamos todo. Ahora primero la función con diferente número
de caracteres.

def add_match(cadena):
para i, char en enumerar (cadena):
por menos, más en n_diff:
si char == menos:
rendimiento replace_at_index(cadena, i, más)

150
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

La entrada es sólo la cadena, es decir, la ecuación que queremos resolver. Luego iteramos sobre todos los
caracteres de la cadena y usamos enumerate() para obtener simultáneamente el carácter y su índice como una
tupla. Para cada carácter, luego repetimos las opciones en n_diff y las probamos sistemáticamente. Por ejemplo,
si aparece un 1 en nuestra ecuación original, se reemplaza por un 7 y el resultado se devuelve usando
rendimiento (así creamos la función como generador).
Nuestra función principal comprobará entonces si se ha creado una ecuación correcta de esta manera. De esta
manera añadimos una coincidencia a la ecuación general, lo que significa que primero hay que eliminarla en
otro lugar; de lo contrario, el total ya no es constante. Esto se integra en la siguiente función, que se estructura
de la siguiente manera:

def change_match(cadena):
para i, char en enumerar (cadena):
para char1, char2 en n_same:
si char == char1:
rendimiento replace_at_index(cadena, i, char2)
si char == char2:
rendimiento replace_at_index(cadena, i, char1)
por menos, más en n_diff:
si char == más:
one_match_less = reemplazar_en_index(cadena, i, \
menos)
rendimiento de add_match(one_match_less)

Nuevamente usamos la cadena original como único argumento. Luego iteramos sobre todos los caracteres de la
cadena y aplicamos enumerate() nuevamente. Primero probamos reemplazos con un recuento constante de
coincidencias. Para obtener todas las combinaciones tenemos que probar la posición de los caracteres en cada
combinación almacenada en n_same, es decir, en ambas direcciones. Luego pasamos a las sustituciones con
distinto número de palos. Evidentemente tenemos que tener cuidado de quitar uno primero y añadir otro después.
Luego iteramos sobre todos los elementos en n_diff y, si es posible, reemplazamos una coincidencia eliminando
una. Guardamos esta nueva ecuación en one_match_less y la introducimos en add_match() para que ahora
todas las adiciones se prueben sistemáticamente y el número de maderas permanezca constante. Aquí utilizamos
el rendimiento de. Esto nos permite acceder a otro generador directamente desde nuestro generador actual y
solicitar sus valores de retorno (consulte también el apéndice de este capítulo). Esto garantiza que nuestra
función finalmente realizará todos los reemplazos posibles.
Veamos un ejemplo de cómo funcionaría esta función con nuestra ecuación original.

>>> testgen = cambio_coincidencia("185+15=270")


>>> para i en el rango(10):
>>> imprimir (siguiente (gen de prueba))
+85+15=270
765+15=270
185+15=270
166+15=270

151
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

165+75=270
165+16=270
165+19=270
165+15=278
795+15=270

Para hacer esto, llamaremos a la función que acabamos de definir como prueba y observaremos los primeros diez elementos de
este generador. Primero, el 1 se reemplaza por un +, luego el 1 se reemplaza por un 7. Esto significa que el 8 se convierte en un
6, por lo que el número de palos permanece constante. Ya casi hemos terminado con esto. De esta lista de todas las soluciones

posibles, ahora tenemos que encontrar aquellas que sean sintácticamente correctas (es decir, que contengan exactamente un
signo igual) y que también proporcionen la solución matemática correcta.

solucionador def (ecuación):


para candidato en change_match(ecuación):
si [Link]("=") == 1:
intentar:

si eval(candidato .replace('=','==')):
candidato de regreso

excepto error de sintaxis:


aprobar

generar RuntimeError ("No se encontró ninguna solución")

La función acepta la ecuación original como entrada. Luego iteramos sobre toda la salida del generador. Por lo tanto, tenemos la
garantía de obtener de esta función todas las combinaciones posibles de personajes que se ajusten a las reglas. Luego, primero
comprobamos si la ecuación resultante contiene exactamente un signo igual. Sólo entonces podrá ser una ecuación sintácticamente
correcta. Luego tenemos que verificar las matemáticas, para lo cual usamos eval(). Esto nos permite ejecutar código directamente
en Python o verificar su corrección. Tenga en cuenta que el signo igual debe reemplazarse por uno doble porque tenemos que
probar la equivalencia usando este operador en Python (por ejemplo, 1==1).

Si esta evaluación es exitosa, se devuelve True y hemos encontrado una solución. Es bastante probable que se genere un error,
por ejemplo en una ecuación como 7==+, porque no tiene sentido en Python. Para detectar tales errores, usamos una construcción
try...except para que el script no falle. Si al final hemos probado todas las combinaciones posibles, pero no hemos encontrado
una solución, la ecuación es irresoluble. Luego generamos un mensaje de error. Es hora de realizar una prueba.

>>> solucionador("185+15=270")
195+75=270

Es fácil verificar que las matemáticas son correctas. La solución es quitar una cerilla de los 8.

152
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

y sacamos un 75 de los 15. Al final, movimos exactamente una coincidencia y así cambiamos dos dígitos o caracteres.

Asignaciones

1. En teoría, ¿podrían existir múltiples soluciones? Cambie la función para que todo lo posible,
Se generan las soluciones correctas.
2. Cree una función que genere ecuaciones coincidentes similares, es decir, primero una ecuación
que corregido y además la solución correcta.

Apéndice: rendimiento de

En el ejemplo anterior utilizamos el rendimiento de, que era nuevo. Siempre que una función necesite devolver algo,
podemos usar return o rendimiento, donde rendimiento, como se explicó anteriormente, define un generador y almacena
el estado de la función. ¿ Pero de qué se obtiene el rendimiento? La idea básica es que un generador puede suministrar
elementos directamente desde otro generador sin tener que inicializarlo explícitamente. En este sentido, ceder es algo
que nos hace la vida más fácil pero no es necesario. Como ejemplo, podemos mirar una lista anidada que queremos
aplanar para terminar con una lista de todos los elementos pero sin sublistas. La programación es sencilla:

def aplanar(lista de entrada):


"""Aplana una lista"""

para elemento en lista de entrada:


si no es instancia (elemento, lista):
elemento de rendimiento
demás:
rendimiento de aplanar (elemento)

Supongamos que solo se incluyen listas (no tuplas anidadas). La función toma la lista original como argumento y luego
itera sobre cada elemento de la lista. Si el elemento no es una lista, se puede devolver directamente. Si es una lista,
ahora hay que descomprimirla. Por lo tanto, llamamos a nuestra función de forma recursiva, el nuevo argumento es la
lista que acabamos de encontrar. Esta lista puede contener más sublistas, pero esto está cubierto por la recursividad
anidada arbitrariamente. Aquí es donde entra en juego el rendimiento . La autollamada de la función generadora crea un
nuevo generador. Si usáramos solo rendimiento, obtendríamos un objeto generador como retorno, que no nos sirve de
nada. Sin embargo, utilizando el rendimiento de , el objeto generador recién creado se inicializa directamente y se realizan
salidas individuales. Probémoslo.

>>> a = [1, 2, 3, [8, 77, [3, 4], 7], 5, [34, [], 43]]
>>> lista(aplanar(a))
[1, 2, 3, 8, 77, 3, 4, 7, 5, 34, 43]

153
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

No importa cuántos niveles tengan las sublistas, al final todos los elementos (aquí sólo los números) se combinan
en una lista sin más niveles.

4.7 • Superpalíndromos

En tareas anteriores, tratamos con palíndromos, es decir, cadenas que son idénticas cuando se leen hacia adelante
y hacia atrás. Entre los candidatos más longevos del idioma inglés se encuentran RACECAR o TACOCAT. Si
pasamos a oraciones , podemos encontrar construcciones aún más largas como "¿Fue un murciélago lo que
vi?" ¿Puedes programar cosas así? Sí, siempre y cuando no esperes una frase significativa o gramaticalmente
correcta, sino simplemente una serie de palabras. Para ello utilizamos el diccionario que presentamos en la primera
tarea de este capítulo. Contiene una gran lista de sustantivos que podemos utilizar.

El algoritmo de solución se basa en un programa de Peter Norvig, quien creó el palíndromo más largo de Inglaterra.3
La idea en sí es la siguiente: determinar el principio y los finales del palíndromo para definir los límites. Así,
trasladado a una frase escrita, tenemos una parte inicial (la parte izquierda) y una parte final (la parte derecha).
Después se determina qué parte de la frase completa impide hasta el momento el palíndromo, es decir, no
encuentra un par de caracteres adecuado. Veamos un ejemplo. Como encuadre utilizamos:

UN HOMBRE UN PLAN… UN CANAL PANAMÁ

Como podrás comprobar fácilmente, esta frase es un palíndromo, excepto la parte ACA del lado derecho. Por lo
tanto, necesitamos encontrar una palabra que comience con ACA y que se agregue a la parte izquierda de la
construcción. Un ejemplo sería ACAPULCO, que crea esta oración.

UN HOMBRE UN PLAN ACAPULCO ... UN CANAL PANAMA

El ACA, que anteriormente era el "cabo suelto", ahora está cubierto por ACAPULCO; sin embargo, la cadena actual
todavía no es un palíndromo. El nuevo extremo suelto es PULCO, ubicado en el lado izquierdo de la construcción.
Ahora, para cubrir esta parte, necesitamos una nueva palabra que termine en el reverso de PULCO, es decir,
OCLUP. Sin embargo, si buscamos en el diccionario veremos que no existen tales palabras. Por tanto, nos
quedaremos sin palabras. Tenemos que confiar en dar marcha atrás y encontrar otra solución. Eliminamos
ACAPULCO del lado izquierdo del constructo y buscamos otra palabra o constructo, tal vez ACADEMIA. esto nos
da

UN HOMBRE UN PLAN ACADEMIA ...UN CANAL PANAMA

El cabo suelto ahora es DEMIA. ¿Hay alguna palabra que termine en el reverso, es decir, DIRIGIDO? Sí,
RECLAMADO por ejemplo. Espero que el principio esté ahora más claro. Buscamos coincidencias adecuadas, las
agregamos y vemos hasta dónde avanzamos. Si no se encuentran coincidencias en un punto determinado, tenemos
que eliminar palabras y probar con otras. Esto continúa hasta que la cuerda total se vuelve palindrómica y se
alcanza una cierta longitud mínima. Para hacer esto en Python, necesitaremos algunas funciones adicionales.

3 [Link]

154
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

def is_palindrome(cadena):
devolver cadena == cadena[::1]

def descanso(izquierda, derecha):


"""Encuentra la parte de la construcción que prohíbe la formación de
\ un palíndromo"""
izquierda = "".unirse(izquierda)
derecha = "".unirse(derecha)
regresar izquierda[len(derecha):] o derecha[:len(izquierda)]

def buscador de palabras (lista de palabras, cadena, inicio, bloqueado):


"""Encuentra una coincidencia adecuada"""
si comienza:
para palabra en lista de palabras:

si [Link](cadena) y la palabra no están bloqueadas:


palabra de retorno
demás:
para palabra en lista de palabras:

si [Link](string) y la palabra no están bloqueadas:


palabra de retorno
regresar Ninguno #no se encontró ninguna coincidencia adecuada

Primero, creamos una prueba simple con is_palindrome(), para indicar si una cadena a probar es un palíndromo. La segunda
función rest() toma dos listas y comprueba qué parte impide que se cree un palíndromo. Ya debemos definir nuestros tipos de
datos en este punto. Almacenamos las palabras respectivas en listas y las combinamos dinámicamente en cadenas para
realizar pruebas. Esto nos facilita mucho agregar o eliminar palabras completas más adelante (retroceder).

Luego definimos un subconjunto izquierdo y derecho como se muestra arriba y convertimos las listas en cadenas.
El resto se hace con un solo comando. Para ello utilizamos rodajas y la longitud de la otra cuerda.
Si ambas cadenas tienen la misma longitud, obtenemos dos cadenas vacías y generamos una de ellas.
De lo contrario, o garantiza que se devuelva la cadena con más caracteres.

La última función de ayuda busca nuevas palabras coincidentes en el diccionario. Aquí tenemos cuatro argumentos: la lista de
todas las palabras, la cadena que tiene que encontrar una coincidencia, inicio (un valor que indica si nuestra cadena debe
estar al principio o al final), y bloqueado, una colección de palabras ya utilizadas que No se nos permite asignar. Sólo nos
queda distinguir si la cadena debe estar al principio o al final de la palabra. Luego simplemente iteramos sobre la lista de
palabras y encontramos una coincidencia adecuada. Si resulta que no hay ninguna palabra coincidente, como se explica en el
ejemplo anterior, la función debe tenerlo en cuenta. En este caso, generará Ninguno. Ahora podemos juntar todo en la función
principal.

155
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

importar def aleatoria


principal(minlength): con
open("[Link]", encoding="utf8") como datos:
lista de palabras = [[Link]().upper() para la fila de datos]
izquierda = ["A", "MAN", "A", "PLAN"] derecha =
["A", "CANAL", "PANAMA"]

total = "".join(izquierda) + "".join(derecha) bloqueado = set()


last_right = False
mientras len(total) <
minlength o no is_palindrome(total): Loose_end = resto(izquierda, derecha) if Loose_end
= = "": mientras que Verdadero:

nueva palabra = [Link](lista de palabras) si


nueva palabra no está bloqueada:
romper
demás:
nueva palabra = buscador de palabras(lista de palabras, extremo_suelto[::1],
último_derecho, bloqueado)

#Retractarse
si no es una palabra nueva:

si último_derecho:
[Link](0)
último_derecho = Falso
demás:
[Link](1)
last_right = Verdadero
demás:
[Link](nueva palabra)
si última_derecha:
[Link](nueva palabra)
última_derecha = False
demás:

[Link](0, nueva palabra) last_right


= Verdadero

total = "".join(izquierda) + "".join(derecha)

print("Extremo suelto: ", extremo_suelto)


print("Nueva palabra: ", nueva palabra)
print(izquierda, derecha)
afirmar is_palindrome(total) devuelve total

156
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

Repasemos ahora esta función algo más larga paso a paso y veamos un ejemplo más detallado al final. La función
utiliza el módulo aleatorio y tiene un argumento, la longitud mínima que finalmente debería alcanzar el palíndromo.
Primero, leemos en la lista de palabras. Este debe estar en la misma carpeta que nuestro script. Convertimos todas
las palabras a mayúsculas y nos atenemos constantemente a este formato. Definimos las palabras iniciales como
se describe arriba. Luego construimos toda la cadena desde las oraciones iniciales en total. Creamos un conjunto
vacío en el que anotamos todas las palabras que ya hemos utilizado. También debemos asegurarnos de añadir
nuevas palabras de forma alternada en la parte izquierda y derecha y nunca dos veces seguidas en el mismo lado.
La variable bool last_right se utiliza para memorizar esto. Si agregamos la última palabra al lado derecho, esta
variable es Verdadera; en caso contrario , Falsa.

Iniciamos el bucle principal, que se ejecuta hasta que se cumplan dos condiciones. Por un lado, nuestro palíndromo
total debe haber alcanzado una longitud mínima, que especificamos en minlength. Por otro lado, la cuerda completa
debe ser un palíndromo. Usamos la función de ayuda rest() para determinar el cabo suelto de la construcción
actual. Aquí hay dos posibilidades: obtenemos una cadena vacía, lo que indica que no hay cabos sueltos, y la
cadena actual ya es palindrómica. Como todavía terminamos en el bucle, sabemos que la longitud total es
demasiado corta. En este caso, elegimos una palabra aleatoria de la lista de palabras (que no debe aparecer en
bloqueado), que seguiremos usando después. Si, por el contrario, nos quedamos con un cabo suelto, ahora
debemos encontrar una contraparte adecuada. Repasemos esto usando un ejemplo.

El valor residual puede ser OT. Lógicamente, el resto debe venir del último lado que se añadió. Suponiendo que este
fuera el lado derecho, la situación se vería así:

... | ANTIGUO TESTAMENTO...

Por lo tanto, ahora debemos encontrar una cadena para el lado izquierdo de la oración que coincida con el reverso
del final dado, que es TO. Un ejemplo podría ser TOLERANTE.

...TOLERANTE | ANTIGUO TESTAMENTO…

El nuevo cabo suelto ahora es LERANTE. Pero ¿y si el final está al otro lado de la frase, así?

...OT | …

Ahora debemos encontrar una palabra para el otro lado que termine con el extremo suelto invertido, por lo tanto TO.
Un ejemplo podría ser QUITO.

...OT | QUITO…

El nuevo cabo suelto en el lado derecho ahora es QUI. El lado al que tenemos que agregar una palabra se controla
usando last_right. Aquí sólo debes tener cuidado de encontrar la cadena correcta en el lugar correcto ya sea al
principio o al final de la nueva palabra. Pero ¿qué pasa si no se encuentra ninguna palabra, p. ej. porque ya existe
y, por tanto, está bloqueada, o no existe ninguna? En este caso, el retorno de wordfinder() es Ninguno y tenemos
que iniciar un retroceso.
Dependiendo de si la última palabra se insertó hacia la izquierda o hacia la derecha, se eliminará en el

157
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

posición correcta, al principio o al final de la lista. Para esto usamos [Link](index), que elimina un elemento de una lista en la
posición deseada. Una vez que se ha eliminado el elemento, solo necesitamos invertir el índice de posición actual (en last_right).
La palabra antigua permanece en la lista de bloqueados y, por lo tanto, ya no podrá utilizarse la próxima vez. De esta manera se
evitan ciclos interminables.

Si el resultado no es Ninguno, es decir, una palabra válida, se agregará al conjunto bloqueado. Como esto no es una lista sino un
conjunto, usamos [Link](element). Nuevamente, debemos asegurarnos de agregar la palabra recién seleccionada a la izquierda
o a la derecha, y allí al principio o al final de la lista. Para colocar un elemento al principio de una lista usamos [Link](index,
element), en caso contrario (para insertar al final de una lista) usamos el conocido [Link](element). Si esto está terminado,
casi hemos terminado y ahora podemos generar la cadena total y calcular la longitud. También tenemos algunos resultados
intermedios mostrados en la consola, para que podamos seguir la construcción del superpalíndromo. Después de esto, el bucle
comienza de nuevo. Si en algún momento se cumplen todas las condiciones y se sale del bucle, se realiza una prueba para
asegurarse de que el palíndromo final sea realmente un palíndromo. Después se muestra el resultado. Ahora visualicemos el
proceso de forma interactiva con los parámetros conocidos.

Extremo suelto: ACA


Nueva palabra: ABACA

['A', 'MAN', 'A', 'PLAN'] ['ABACA', 'A', 'CANAL', 'PANAMÁ']


Cabo suelto: ABACAACA
Nueva palabra: Ninguna

['A', 'MAN', 'A', 'PLAN'] ['A', 'CANAL', 'PANAMÁ']

Dadas las palabras iniciales, el programa identificó correctamente el cabo suelto ACA y encontró una posible palabra, ABACA.
Sin embargo, el nuevo cabo suelto ahora es ABACAACA y la nueva palabra es Ninguna , por lo que Python no puede encontrar
una palabra coincidente para este fin. En la siguiente línea, se elimina la palabra agregada ABACA y volvemos al principio.
Comienza la siguiente ronda y el proceso continúa. Lleva algo de tiempo, pero finalmente terminamos con esta solución.

['A', 'MAN', 'APPAIR', 'BA', 'DEL'] ['LED', 'ABRI', 'AP', 'PANAMA']

Como podemos comprobar, efectivamente se trata de un palíndromo con más de 30 caracteres en total. Algunas de
estas "palabras" son bastante extrañas, por lo que es posible que deseemos desinfectar un poco más los datos de entrada.
Siéntete libre de jugar con los datos. Quizás puedas encontrar un súper palíndromo interesante de esta manera.

4.8 • 2048

2048 es un juego popular para teléfonos móviles y computadoras. El objetivo es combinar inteligentemente potencias de dos en
un campo de juego de 16 cuadrados para que finalmente se alcance el número 2048.

158
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

(ver la siguiente figura). La regla principal es que sólo se pueden combinar números idénticos, por ejemplo, los
números 2 y 2 con 4, pero no 2 y 4. Queremos recrear este principio bastante simple en la consola.

Figura 4.2: Una versión de 2048 con una bonita interfaz gráfica. Creador: TheQ Editor (Wikimedia Commons, CC
BYSA 3.0)

Las reglas exactas son las siguientes: comienzas con un campo de juego en el que dos campos están
ocupados por el número 2. Después de esto, el jugador puede mover el campo de juego en cualquier
dirección con cada movimiento, es decir, arriba, abajo, izquierda o derecha. . Esto mueve los números en la
dirección deseada y, si es posible, los suma. Además, después de cada movimiento se inserta un nuevo 2
en una posición libre aleatoria. Veamos algunos ejemplos:

2202
<
4200

2222
<
4400

8442
<
8820

El segundo ejemplo muestra cómo sumar de izquierda a derecha. El primero y el segundo 2 se suman hasta un
4, luego el tercer y el cuarto número se suman nuevamente hasta un 4. Esto completa la ronda. Los campos
vacíos en los bordes se rellenan con ceros. El juego se gana cuando el jugador llega a 2048. En nuestro ejemplo,
nos limitaremos a 512 por una razón trivial, como se explicará más adelante. El campo en sí está representado
por una lista de sublistas (cuatro listas con cuatro elementos cada una). Comencemos con la funcionalidad
central del juego, la

159
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

implementación de los movimientos. Hay cuatro posibilidades como se mencionó anteriormente.


Consideraremos las cuatro posibilidades por separado, ya que como resultado nuestra funcionalidad
cambiará ligeramente. Comencemos primero con esta función de ayuda.

def combinar(números):
números = [z para z en números si z != 0]
para z en el rango(0, len(números) 1):
si números[z] == números[z + 1]:
números[z] = números[z] * 2
números[z + 1] = 0
números = [z para z en números si z != 0]
devolver números + [0 para z en el rango(4 len(números))]

Primero, usamos una lista por comprensión para eliminar todos los ceros de la lista, ya que
desaparecen de todos modos si hay otro número en la misma lista. Obtenemos una nueva lista que
solo contiene números de 2 o más. Ahora iteramos sobre todos los elementos de la nueva lista y
comprobamos, de izquierda a derecha: si dos números adyacentes son iguales, el valor del número
de la izquierda se duplica y el número de la derecha se elimina. Por tanto, es posible que se pueda
volver a crear una lista que incluya ceros. Estos se eliminan en el último paso y, si es necesario, se
insertan nuevos ceros en el extremo derecho de la lista. De esta manera se implementó la pura
función de movimiento hacia la izquierda. ¿Qué pasa si juegas hacia la derecha o incluso hacia
arriba o hacia abajo? Aún podemos usar esta función, solo tenemos que transformar las respectivas
entradas. Veamos la siguiente fila en el campo de juego:

0202
>
0004

Por lo tanto debemos sumar de derecha a izquierda e insertar los espacios en el lado izquierdo . Esto
se hace automáticamente si simplemente volteamos la lista, la pasamos a la función y luego volteamos
el resultado nuevamente. Alimentamos la función con [2, 0, 2, 0] y obtenemos [4, 0, 0, 0]. Le volvemos
a dar la vuelta y obtenemos el resultado final. El segundo paso es extraer las respectivas filas o
columnas de tal forma que las pasemos en la orientación correcta y al final, volver a encajar
correctamente el resultado en el campo general del juego.

160
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

def update_grid(cuadrícula, dirección):


si dirección == "izquierda":

grilla = [combinar(fila) para fila en la grilla]


dirección elif == "derecha":
grilla = [combinar(fila[::1])[::1] para fila en la grilla] dirección
elif == "arriba":
grid = [combinar(fila) para fila en zip(*grid)]
grid = [lista(fila) para fila en zip(*grid)]
dirección elif == "abajo":

grid = [combinar(fila[::1])[::1] para fila en zip(*grid)]


grid = [lista(fila) para fila en zip(*grid)]
rejilla de retorno

La primera variante, el movimiento hacia la izquierda, es clara. Solo necesitamos iterar sobre todas las
líneas de la cuadrícula, aplicar la función y generar el resultado. Para ello utilizamos una lista de
comprensión. La segunda variante, el movimiento hacia la derecha, es sólo un poco más compleja.
Repetimos todas las filas de la cuadrícula y alimentamos la función con la fila invertida . Luego, el resultado
se invierte nuevamente y se escribe en la cuadrícula.

Tenemos que prestar un poco más de atención cuando ascendemos. No podemos simplemente tomar
una fila completa del campo de juego, sino que tenemos que intercambiar filas y columnas
(transponer). Para hacer esto, usamos zip() y recibimos las filas transpuestas que ingresamos en la
función. En el segundo paso, después de que la función haya procesado la fila, invertimos el proceso,
es decir, transponemos nuevamente y nos aseguramos de que los resultados se escriban como listas
(y no como tuplas) en la cuadrícula final. Si bajamos, el procedimiento es casi el mismo, pero aquí
primero tenemos que invertir las filas transpuestas y luego invertir el resultado nuevamente. Para una
mejor comprensión, el siguiente ejemplo puede ayudar.

#Transponer una lista anidada (matriz)


>>> a = [[1,2,3], [4,5,6], [7,8,9]]
>>> b = zip(*a)
>>> para fila en a:
>>> fila

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
>>> para la fila en b:
>>>
lista (fila)
[1, 4, 7]
[2, 5, 8]

Queda claro cómo los números que inicialmente son adyacentes en una fila son adyacentes en una

161
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

columna después de la transposición. Después de esto, necesitamos una función que inserte un nuevo 2
en una posición aleatoria y vacía en la cuadrícula en cada ronda.

def nuevonúmero(cuadrícula):
salida = cuadrícula[:] #Copia de la grilla original
poscolumna = [0, 1, 2, 3]
[Link] = [0, 1, 2, 3]
[Link](columnas)
[Link](rowpos)
para fila en rowpos:
para col en
columnpos:
si salida[fila][col] == 0:
salida[fila][col] = 2
salida de retorno
salida de retorno

Para ello aleatorizamos los índices y buscamos hasta encontrar un número. Si el campo está lleno, esto
daría como resultado que no se ejecute ninguna declaración de devolución explícita al final y Ninguno sería
el resultado. Sin embargo, esto no debe suceder, por lo que en este caso generaremos la cuadrícula intacta.
Después de esto necesitamos una función que pruebe si se ganó el juego, es decir, si se alcanzó 512.

def game_won(cuadrícula):
devolver cualquiera (512 en fila por fila en la cuadrícula)

Tan pronto como se encuentra 512 en una fila de la cuadrícula, se devuelve True , lo que podemos lograr
mediante any(). Todavía falta una función que muestre gráficamente la grilla.

visualización def (cuadrícula, movimiento, puntuación):

mapeo = {0: "[ ]", 2: "[2^1]", 4: "[2^2]", 8: "[2^3]", 16: "[2^4]", 32: "[2^5]", 64: "[2^6]",
128: "[2^7]", 256: "[2^8]", \
512:"[2^9]"}
para fila en la cuadrícula:
para columna en fila:

imprimir(mapeo[col], fin= "")


imprimir("")
imprimir("=================")
print("Movimiento actual:", movimiento)

162
Machine Translated by
Google
Capítulo 4 • Datos de texto y cadenas

Aquí también vemos por qué solo vamos a 512. En la consola, tenemos que pensar en caracteres y para una visualización
agradable y uniforme, todos los campos deben tener el mismo tamaño. En lugar de números (que pueden tener entre uno y
cuatro dígitos), utilizamos una representación en potencias de dos, que tienen exactamente dos caracteres, es decir, 2 y la
potencia. Podemos llegar hasta 29, que corresponde a 512.4 Si tuviéramos números más grandes, necesitaríamos tres
dígitos. Para la salida de la consola, esto parece un buen compromiso. Para que se vea así, utilizamos un dict que contiene
la información necesaria. Luego iteramos sobre filas y columnas y nos aseguramos de que todos los caracteres de una lista
aparezcan en una línea. También mostramos información sobre el número actual de movimientos y la puntuación. La
puntuación es simplemente la suma de todos los números del tablero actual. Finalmente, reunimos todo en la función
principal y estructuramos el curso del juego.

importar aleatoriamente
importar itertools
TECLAS = {
"\x1b[D": "izquierda",
"\x1b[C": "derecho",
"\x1b[A": "arriba",
"\x1b[B": "abajo",
}

definición principal():

cuadrícula = [[0] * 4 para i en el rango


(4)] cuadrícula[3][0] = 2
cuadrícula[3][1] = 2
para moverse en [Link](1):
puntuación = suma(suma(fila) para la fila en la cuadrícula)
mostrar (cuadrícula, mover, puntuación)
si game_won (cuadrícula):
romper
mientras que Verdadero:

entrada de usuario = entrada()


si el usuario ingresa en TECLAS:
romper

print("¡La entrada no es válida! ¡Utilice sólo las teclas de flecha!")


cuadrícula = update_grid (cuadrícula, LLAVES [entrada de usuario])
cuadrícula = nuevo número (cuadrícula)
para i en el rango (40):
imprimir()
print("¡Juego ganado!")

4 Por motivos de impresión, en el código aquí mostrado las potencias sólo pueden representarse con
el carácter "^". En Python hay mejores caracteres, así que asegúrese de consultar la documentación
en línea para esta tarea.

163
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

Comenzamos con una cuadrícula vacía en la que insertamos dos números. Usamos [Link]() para
contar desde 1. Usando comprensión, calculamos la puntuación actual sumando los números en todas las
filas. Luego se muestra la cuadrícula. A esto le sigue un control de victoria. Si este es el caso, salimos del
juego. De lo contrario, se inicia un bucle que sirve para capturar la entrada actual. El bucle se ejecuta hasta
que recibimos una entrada válida. Movimos la asignación entre la entrada (teclas de flecha) y el comando
respectivo a KEYS, porque estos nombres son un poco crípticos. Cuando recibimos una entrada válida por
parte del usuario, pasamos la información a move() y se recalcula la cuadrícula. Luego insertamos un 2 en
una posición aleatoria.
A esto le siguen 40 líneas vacías, que se utilizan para simular una actualización dinámica del campo de
juego en la consola. Esto completa la función.

Asignaciones

1. Agregue una función que pruebe si el juego está perdido, lo que significa que todos los campos de la cuadrícula están
se llena con un número y no es posible realizar más movimientos.

4.9 • Los próximos pasos

¡Felicidades! Después de realizar todas las tareas, podrá estar orgulloso de lo que ha aprendido hasta
ahora. Ya no eres un principiante y puedes utilizar Python de forma productiva en escenarios reales en el
trabajo y en la vida cotidiana. Sabe cómo dividir tareas complejas en distintos pasos, implementar
algoritmos y aproximar problemas complejos con simulaciones. Ha utilizado las diversas posibilidades de
Python y ha conocido muchos módulos.
Dependiendo de cómo quieras desarrollarte, hay muchas formas de profundizar en Python.
Por ejemplo, si busca tareas o acertijos más prácticos, encontrará numerosas plataformas en línea que
recopilan y compilan tareas típicas de manera sistemática y exhaustiva.
Especialmente dignos de mención son [Link] y el Proyecto Rosalind ([Link]), que siempre
son una valiosa fuente de inspiración para mí. Encontrará muchos más desafíos en varios grados de
dificultad en estos sitios.

También puede resultar útil explorar temas especiales de interés con más detalle, ya sea programación
numérica, simulaciones estadísticas, programación GUI, aplicaciones web o software clásico. En línea y en
librerías encontrará abundante material sobre todos los temas. Finalmente, se recomienda perseguir
consistentemente sus propias ideas y proyectos.
Incluso si esto puede parecer difícil, especialmente al principio, probablemente encontrará desafíos que no
podrá resolver directamente. Ahora tienes todas las herramientas para lograr tus objetivos.
Utilice comunidades y foros en Internet e intercambie ideas con otros.5
Si te gustó este libro, estaré encantado de recibir comentarios y reseñas en las distintas tiendas online. Te
deseo mucha diversión y éxito usando Python.

5 Dos excelentes lugares para comenzar son [Link] y [Link]/r/python

164
Machine Translated by
Google
• Índice

• Índice

A
modelado basado en agentes 96
análisis 74
cualquiera
162 agregar
23
argumentos 69 afirmar 16

B
retroceso 70 caso
base 24
arranque 104 ruptura
12 fuerza
bruta 65

C coordenadas cartesianas 42
caos 46
elección
84 clase
97 colatz
29 combinaciones
64 comprensión 13
comentario 19
convergencia 49
administrador de contexto
132 continuar
13 sistema de coordenadas
56 contar
33 producto cruzado 57

D
decimal 34
estrategia de decisión 114
decorador 62
diccionario 10
discontinuidad 52

cifrado 141
enumerar 94
evaluación
77, 152
excepto 17 excepción 17

165
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

F
factorial 25
fibonacci 22

G
juego 102, 104 juego
de la vida 92
generador 26
código genético 135
cuadrícula 89

h
función hash 142
histograma 125

I
importar 17
inicio 97

integral 74
islice 38

Juan Conway 92

k
recorrido del caballero
70 kwargs 69

l
lcs 137
función logística 50
mapa logístico
51
bucle 11 lps 135

METRO

matriz 90
módulo 17
monótono 68
multiprocesamiento 84

norte

norma 57
integración numérica 74

166
Machine Translated by
Google
• Índice

O
objeto 18
optimización 103

PAG

palíndromo 135, 154


paralelización 84
pase 13
pep8 20
pi 30, 80
cerdo
104 población
96 número primo 26
proceso 85
producto 123
creación de
perfiles 41 flujo de programa 140

cola Q 84

R
aleatorio 83
paseo aleatorio 88
recursividad 22, 24,
36 límite de
recursividad 40
refactorización 21
remuestreo
128 retorno 15 números romanos 147

S
muestra 84
producto escalar 59
serie 31
simulación 103
rebanada 9

t
tiempo 37,
79 transposición
161 trigonometría 31, 88
prueba 17

EN

vector 57

167
Machine Translated by
Google
Python 3 para aplicaciones de ciencia e ingeniería

rendimiento

27 rendimiento de 153

CON

código postal 58

168
Machine Translated by Google
liblirborsos

Aprenda a usar Python de manera productiva en escenarios


de la vida real en el trabajo y en la vida cotidiana.

Python 3 para ciencia y


Aplicaciones de ingeniería
Si domina los conceptos básicos de Python y desea explorar el lenguaje con
más profundidad, este libro es para usted. Mediante ejemplos concretos
utilizados en diferentes aplicaciones, el libro ilustra muchos aspectos de la
programación (por ejemplo, algoritmos, recursividad, estructuras de datos) y Felix Bittmann es investigador
ayuda a estrategias de resolución de problemas. Incluyendo ideas y soluciones asociado en el Instituto Leibniz de
Trayectorias Educativas y
generales, se discuten los detalles de Python y cómo se pueden aplicar en la práctica. candidato a doctorado en la
Universidad de Bamberg, Alemania.
Sus intereses de investigación
Python 3 para aplicaciones de ciencia e ingeniería incluye:
incluyen la desigualdad social,
>aprendizaje práctico y orientado a objetivos
el papel de la educación en el
>técnicas básicas de Python curso de la vida, los métodos
> Python 3.6+ moderno que incluye comprensiones, decoradores y cuantitativos y la filosofía de la
generadores ciencia. Con enfoque en análisis
estadístico e investigación aplicada,
>código completo disponible en línea
Python es una herramienta integral y
> más de 40 ejercicios, soluciones documentadas online
multifuncional de su flujo de trabajo diario.
>no se requieren paquetes ni instalación adicionales, 100% Python puro

Los temas cubren:


>identificar números primos grandes y calcular Pi
> escribir y comprender funciones recursivas con memorización
> Computar en paralelo y utilizar todos los núcleos del sistema.
>procesar datos de texto y cifrar mensajes
>comprender el retroceso y la resolución de Sudokus
>análisis y simulación de juegos de azar para desarrollar estrategias ganadoras
óptimas
>manejar el código genético y generar palíndromos extremadamente largos

Elektor International Media BV


[Link]

También podría gustarte