Think Python Es
Think Python Es
Think Python es una introducción a Python para personas que nunca han programado, o para
quienes lo han intentado y se han encontrado con dificultades.
Esta edición en español traduce la tercera edición de Think Python: How to Think Like a Computer
Scientist, de Allen B. Downey, con traducción de midudev.
Puedes pedir las versiones impresa y electrónica de Think Python 3e en [Link] y Amazon.
Aquí está la página principal del libro en Green Tea Press.
Descargar el libro completo en PDF
Los notebooks
Capítulo 1: Programar como una forma de pensar
Abrir el capítulo 1 en Colab
Capítulo 2: Variables y sentencias
Abrir el capítulo 2 en Colab
Capítulo 3: Funciones
Abrir el capítulo 3 en Colab
Capítulo 4: Funciones e interfaces
Abrir el capítulo 4 en Colab
Capítulo 5: Condicionales y recursión
Abrir el capítulo 5 en Colab
Capítulo 6: Valores de retorno
Abrir el capítulo 6 en Colab
Capítulo 7: Iteración y búsqueda
Abrir el capítulo 7 en Colab
Capítulo 8: Cadenas y expresiones regulares
Abrir el capítulo 8 en Colab
Capítulo 9: Listas
Abrir el capítulo 9 en Colab
Capítulo 10: Diccionarios
Abrir el capítulo 10 en Colab
Capítulo 11: Tuplas
Abrir el capítulo 11 en Colab
Capítulo 12: Análisis y generación de texto
Abrir el capítulo 12 en Colab
Capítulo 13: Archivos y bases de datos
Abrir el capítulo 13 en Colab
Capítulo 14: Clases y funciones
Abrir el capítulo 14 en Colab
Capítulo 15: Clases y métodos
Abrir el capítulo 15 en Colab
Capítulo 16: Clases y objetos
Abrir el capítulo 16 en Colab
Capítulo 17: Herencia
Abrir el capítulo 17 en Colab
Capítulo 18: Extras de Python
Abrir el capítulo 18 en Colab
Capítulo 19: Reflexiones finales
Abrir el capítulo 19 en Colab
Primeros pasos
Para la mayoría de los lenguajes de programación, incluido Python, hay muchas herramientas que
puedes usar para escribir y ejecutar programas. Estas herramientas se llaman entornos de
desarrollo integrados (IDEs). En general, hay dos tipos de IDEs:
Algunos trabajan con archivos que contienen código, así que proporcionan herramientas para
editar y ejecutar esos archivos.
Otros trabajan principalmente con notebooks, que son documentos que contienen texto y
código.
Para principiantes, recomiendo empezar con un entorno de desarrollo de notebooks como Jupyter.
Los notebooks de este libro están disponibles en un repositorio online en
[Link]
Hay dos formas de usarlos:
Puedes descargar los notebooks y ejecutarlos en tu propio ordenador. En ese caso, tienes que
instalar Python y Jupyter, lo cual no es difícil, pero si quieres aprender Python puede ser
frustrante pasar mucho tiempo instalando software.
Una alternativa es ejecutar los notebooks en Colab, que es un entorno Jupyter que se ejecuta
en un navegador web, así que no tienes que instalar nada. Colab está operado por Google y es
gratuito.
Si estás empezando, te recomiendo encarecidamente que empieces con Colab.
Recursos para docentes
Si enseñas con este libro, aquí tienes algunos recursos que pueden resultarte útiles.
Puedes encontrar notebooks con soluciones a los ejercicios en
[Link] junto con enlaces a los recursos adicionales de
abajo.
Hay cuestionarios para cada capítulo, y un cuestionario final para todo el libro, disponibles bajo
petición.
Teaching and Learning with Jupyter es un libro online con sugerencias para usar Jupyter de
forma eficaz en el aula. Puedes leer el libro en [Link]
Una de las mejores formas de usar notebooks es programación en vivo (live coding), donde un
instructor escribe código y los estudiantes le siguen en sus propios notebooks. Para aprender
sobre programación en vivo, y obtener otros grandes consejos sobre enseñanza de
programación, recomiendo la formación para instructores ofrecida por The Carpentries, en
[Link]
Agradecimientos
Muchas gracias a Jeff Elkner, que tradujo mi libro de Java a Python, lo que puso en marcha este
proyecto y me presentó lo que ha acabado siendo mi lenguaje favorito. Gracias también a Chris
Meyers, que contribuyó con varias secciones a How to Think Like a Computer Scientist.
Gracias a la Free Software Foundation por desarrollar la GNU Free Documentation License, que
ayudó a hacer posible mi colaboración con Jeff y Chris, y gracias a Creative Commons por la
licencia que uso ahora.
Gracias a quienes desarrollan y mantienen el lenguaje Python y las librerías que usé, incluido el
módulo de Turtle graphics; las herramientas que usé para desarrollar el libro, incluidos Jupyter y
JupyterBook; y los servicios que usé, incluidos ChatGPT, Copilot, Colab y GitHub.
Gracias a los editores de Lulu que trabajaron en How to Think Like a Computer Scientist y a los
editores de O’Reilly Media que trabajaron en Think Python.
Un agradecimiento especial a los revisores técnicos de la segunda edición, Melissa Lewis y Luciano
Ramalho, y de la tercera edición, Sam Lau y Luciano Ramalho (¡otra vez!). También agradezco a
Luciano haber desarrollado el turtle graphics módulo que uso en varios capítulos, llamado
jupyturtle .
Gracias a todos los estudiantes que trabajaron con versiones anteriores de este libro y a todos los
colaboradores que enviaron correcciones y sugerencias. Más de 100 lectores atentos y reflexivos
han enviado sugerencias y correcciones durante los últimos años. Sus contribuciones, y su
entusiasmo por este proyecto, han sido de enorme ayuda.
Si tienes una sugerencia o corrección, envía un email a feedback@[Link] . Si incluyes al
menos una parte de la frase en la que aparece el error, eso me facilita la búsqueda. Los números de
página y sección también sirven, pero no son tan fáciles de manejar. ¡Gracias!
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Traducción al español por midudev (Miguel Ángel Durán).
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
1. Bienvenida
Este es el Jupyter notebook del capítulo 1 de Think Python, 3.ª edición, de Allen B. Downey.
Si no conoces los Jupyter notebooks, haz clic aquí para ver una breve introducción.
Luego, si aún no estás ejecutando este notebook en Colab, haz clic aquí para ejecutar este
notebook en Colab.
La siguiente celda descarga un archivo y ejecuta algo de código que se usa específicamente para
este libro. Todavía no tienes que entender este código, pero deberías ejecutarlo antes de hacer
cualquier otra cosa en este notebook. Recuerda que puedes ejecutar el código seleccionando la
celda y pulsando el botón de reproducción (un triángulo dentro de un círculo) o manteniendo
pulsada la tecla Shift y presionando Enter .
1. Programar como una forma de pensar
El primer objetivo de este libro es enseñarte a programar en Python. Pero aprender a programar
implica aprender una nueva forma de pensar, así que el segundo objetivo de este libro es ayudarte
a pensar como un científico de la computación. Esta forma de pensar combina algunas de las
mejores características de las matemáticas, la ingeniería y las ciencias naturales. Como los
matemáticos, los científicos de la computación usan lenguajes formales para expresar ideas –
específicamente, cómputos. Como los ingenieros, diseñan cosas, ensamblan componentes en
sistemas y evalúan las ventajas y desventajas de distintas alternativas. Como los científicos,
observan el comportamiento de sistemas complejos, formulan hipótesis y prueban predicciones.
Empezaremos con los elementos más básicos de la programación y avanzaremos poco a poco. En
este capítulo veremos cómo Python representa números, letras y palabras. Y aprenderás a realizar
operaciones aritméticas.
También empezarás a aprender el vocabulario de la programación, incluyendo términos como
operador, expresión, valor y tipo. Este vocabulario es importante – lo necesitarás para entender el
resto del libro, comunicarte con otros programadores y usar y entender asistentes virtuales.
Observa que el resultado de la división es 42.0 en lugar de 42 . Eso se debe a que hay dos tipos
de números en Python:
enteros, que representan números sin parte fraccionaria o decimal, y
números de coma flotante, que representan enteros y números con punto decimal.
Si sumas, restas o multiplicas dos enteros, el resultado es un entero. Pero si divides dos enteros, el
resultado es un número de coma flotante. Python proporciona otro operador, // , que realiza
división entera. El resultado de la división entera siempre es un entero.
84 // 2
La división entera también se llama “división de piso” porque siempre redondea hacia abajo (hacia
el “piso”).
85 // 2
Por último, el operador ** realiza exponenciación; es decir, eleva un número a una potencia:
7 ** 2
No cubriré los operadores bit a bit en este libro, pero puedes leer sobre ellos en
[Link]
1.2. Expresiones
Un conjunto de operadores y números se llama una expresión. Una expresión puede contener
cualquier cantidad de operadores y números. Por ejemplo, aquí hay una expresión que contiene
dos operadores.
6 + 6 ** 2
Observa que la exponenciación ocurre antes que la suma. Python sigue el orden de operaciones
que quizá aprendiste en clase de matemáticas: la exponenciación ocurre antes que la
multiplicación y la división, que ocurren antes que la suma y la resta.
En el siguiente ejemplo, la multiplicación ocurre antes que la suma.
12 + 5 * 6
round(42.6)
La función abs calcula el valor absoluto de un número. Para un número positivo, el valor absoluto
es el propio número.
abs(42)
Cuando usamos una función como esta, decimos que estamos llamando a la función. Una
expresión que llama a una función es una llamada a función.
Cuando llamas a una función, los paréntesis son obligatorios. Si los omites, obtienes un mensaje de
error.
NOTA: La siguiente celda usa %%expect , que es un “magic command” de Jupyter que significa que
esperamos que el código de esta celda produzca un error. Para más información sobre este tema,
consulta la introducción a Jupyter notebook.
abs 42
Puedes ignorar la primera línea de este mensaje; no contiene ninguna información que necesitemos
entender ahora mismo. La segunda línea es el código que contiene el error, con un acento
circunflejo ( ^ ) debajo para indicar dónde se descubrió el error.
La última línea indica que se trata de un error de sintaxis, lo que significa que hay algo incorrecto
en la estructura de la expresión. En este ejemplo, el problema es que una llamada a función
requiere paréntesis.
Veamos qué ocurre si omites los paréntesis y el valor.
abs
Un nombre de función por sí solo es una expresión válida que tiene un valor. Cuando se muestra, el
valor indica que abs es una función e incluye información adicional que explicaré más adelante.
1.4. Strings
Además de números, Python también puede representar secuencias de letras, que se llaman
strings porque las letras están ensartadas como cuentas en un collar. Para escribir un string,
podemos poner una secuencia de letras entre comillas rectas.
'Hello'
Las comillas dobles facilitan escribir un string que contiene un apóstrofo, que es el mismo símbolo
que una comilla recta.
"it's a small "
El operador + funciona con strings; une dos strings en un solo string, lo que se llama
concatenación
'Well, ' + "it's a small " + 'world.'
El operador * también funciona con strings; hace varias copias de un string y las concatena.
'Spam, ' * 4
Observa que len cuenta las letras entre las comillas, pero no las comillas.
Cuando crees un string, asegúrate de usar comillas rectas. La comilla invertida, también conocida
como backtick, causa un error de sintaxis.
`Hello`
Las comillas tipográficas, también conocidas como comillas curvas, tampoco son válidas.
‘Hello’
'Hello' es un string.
Una clase de valor se llama tipo. Todo valor tiene un tipo – o a veces decimos que “pertenece a” un
tipo.
Python proporciona una función llamada type que te dice el tipo de cualquier valor. El tipo de un
entero es int .
type(2)
Los tipos int , float y str se pueden usar como funciones. Por ejemplo, int puede tomar un
número de coma flotante y convertirlo en un entero (siempre redondeando hacia abajo).
int(42.9)
Ahora, aquí hay algo que puede ser confuso. ¿Qué obtienes si pones una secuencia de dígitos
entre comillas?
'126'
Este ejemplo genera un TypeError , lo que significa que los valores de la expresión, que se llaman
operandos, tienen el tipo incorrecto. El mensaje de error indica que el operador / no admite los
tipos de estos valores, que son str e int .
Si tienes un string que contiene dígitos, puedes usar int para convertirlo en un entero.
int('126') / 3
Si tienes un string que contiene dígitos y un punto decimal, puedes usar float para convertirlo en
un número de coma flotante.
float('12.6')
Cuando escribes un entero grande, puede que te sientas tentado a usar comas entre grupos de
dígitos, como en 1,000,000 . Esta es una expresión válida en Python, pero el resultado no es un
entero.
1,000,000
Python interpreta 1,000,000 como una secuencia de enteros separados por comas.
Aprenderemos más sobre este tipo de secuencia más adelante.
Puedes usar guiones bajos para que los números grandes sean más fáciles de leer.
1_000_000
1.7. Debugging
Los programadores cometen errores. Por razones caprichosas, los errores de programación se
llaman bugs y el proceso de encontrarlos se llama debugging.
Programar, y especialmente debugging, a veces provoca emociones intensas. Si estás luchando
con un bug difícil, puede que sientas enfado, tristeza o vergüenza.
Prepararte para estas reacciones puede ayudarte a manejarlas. Un enfoque es pensar en la
computadora como un empleado con ciertas fortalezas, como velocidad y precisión, y debilidades
particulares, como falta de empatía e incapacidad para captar el panorama general.
Tu trabajo es ser un buen gerente: encontrar formas de aprovechar las fortalezas y mitigar las
debilidades. Y encontrar formas de usar tus emociones para involucrarte con el problema, sin dejar
que tus reacciones interfieran con tu capacidad de trabajar eficazmente.
Aprender a debug puede ser frustrante, pero es una habilidad valiosa que resulta útil para muchas
actividades más allá de la programación. Al final de cada capítulo hay una sección, como esta, con
mis sugerencias para debugging. ¡Espero que te ayuden!
1.8. Glosario
operador aritmético: Un símbolo, como + y * , que denota una operación aritmética como suma
o multiplicación.
entero: Un tipo que representa números sin parte fraccionaria o decimal.
número de coma flotante: Un tipo que representa enteros y números con partes decimales.
división entera: Un operador, // , que divide dos números y redondea hacia abajo a un entero.
expresión: Una combinación de variables, valores y operadores.
valor: Un entero, número de coma flotante o string – u otro de los tipos de valores que veremos
más adelante.
función: Una secuencia con nombre de sentencias que realiza alguna operación útil. Las funciones
pueden tomar argumentos o no, y pueden producir un resultado o no.
llamada a función: Una expresión – o parte de una expresión – que ejecuta una función. Consiste
en el nombre de la función seguido de una lista de argumentos entre paréntesis.
error de sintaxis: Un error en un programa que hace imposible analizarlo – y por lo tanto imposible
ejecutarlo.
string: Un tipo que representa secuencias de caracteres.
concatenación: Unir dos strings extremo con extremo.
tipo: Una categoría de valores. Los tipos que hemos visto hasta ahora son enteros (tipo int ),
números de coma flotante (tipo float ) y strings (tipo str ).
operando: Uno de los valores sobre los que opera un operador.
lenguaje natural: Cualquiera de los idiomas que hablan las personas y que evolucionaron de forma
natural.
lenguaje formal: Cualquiera de los lenguajes que las personas han diseñado para propósitos
específicos, como representar ideas matemáticas o programas de computadora. Todos los
lenguajes de programación son lenguajes formales.
bug: Un error en un programa.
debugging: El proceso de encontrar y corregir errores.
1.9. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
También mencioné el orden de operaciones. Para más detalles, pregunta “¿Cuál es el orden de
las operaciones en Python?”
La función round , que usamos para redondear un número de coma flotante al entero más
cercano, puede tomar un segundo argumento. Prueba a preguntar “¿Cuáles son los
argumentos de la función round?” o “¿Cómo redondeo pi a tres decimales?”
Hay un operador aritmético más que no mencioné; prueba a preguntar “¿Qué es el operador
módulo en Python?”
La mayoría de los asistentes virtuales saben sobre Python, así que responden preguntas como
estas de forma bastante fiable. Pero recuerda que estas herramientas cometen errores. Si obtienes
código de un chatbot, ¡pruébalo!
1.9.2. Ejercicio
Quizá te preguntes qué hace round si un número termina en 0.5 . La respuesta es que a veces
redondea hacia arriba y a veces hacia abajo. Prueba estos ejemplos y mira si puedes averiguar qué
regla sigue.
round(42.5)
round(43.5)
Si tienes curiosidad, pregunta a un asistente virtual: “Si un número termina en 0.5, ¿Python
redondea hacia arriba o hacia abajo?”
1.9.3. Ejercicio
Cuando aprendes una característica nueva, deberías probarla y cometer errores a propósito. Así
aprendes los mensajes de error y, cuando los vuelvas a ver, sabrás qué significan. Es mejor
cometer errores ahora y deliberadamente que más tarde y accidentalmente.
1. Puedes usar un signo menos para crear un número negativo como -2 . ¿Qué ocurre si pones
un signo más delante de un número? ¿Y con 2++2 ?
2. ¿Qué ocurre si tienes dos valores sin operador entre ellos, como 4 2 ?
3. Si llamas a una función como round(42.5) , ¿qué ocurre si omites uno o ambos paréntesis?
1.9.4. Ejercicio
Recuerda que toda expresión tiene un valor, todo valor tiene un tipo, y podemos usar la función
type para encontrar el tipo de cualquier valor.
¿Cuál es el tipo del valor de las siguientes expresiones? Haz tu mejor conjetura para cada una y
luego usa type para averiguarlo.
765
2.718
'2 pi'
abs(-7)
abs(-7.0)
abs
int
type
1.9.5. Ejercicio
Las siguientes preguntas te dan la oportunidad de practicar escribiendo expresiones aritméticas.
1. ¿Cuántos segundos hay en 42 minutos y 42 segundos?
2. ¿Cuántas millas hay en 10 kilómetros? Pista: hay 1,61 kilómetros en una milla.
3. Si corres una carrera de 10 kilómetros en 42 minutos y 42 segundos, ¿cuál es tu ritmo medio
en segundos por milla?
4. ¿Cuál es tu ritmo medio en minutos y segundos por milla?
5. ¿Cuál es tu velocidad media en millas por hora?
Si ya sabes sobre variables, puedes usarlas para este ejercicio. Si no, puedes hacer el ejercicio sin
ellas – y luego las veremos en el siguiente capítulo.
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
2. Variables y sentencias
En el capítulo anterior usamos operadores para escribir expresiones que realizan cálculos
aritméticos.
En este capítulo aprenderás sobre variables y sentencias, la sentencia import y la función print .
Y presentaré más vocabulario que usamos para hablar de programas, incluyendo “argumento” y
“módulo”.
2.1. Variables
Una variable es un nombre que se refiere a un valor. Para crear una variable, podemos escribir una
sentencia de asignación como esta.
n = 17
Una sentencia de asignación tiene tres partes: el nombre de la variable a la izquierda, el operador
igual, = , y una expresión a la derecha. En este ejemplo, la expresión es un entero. En el siguiente
ejemplo, la expresión es un número de punto flotante.
pi = 3.141592653589793
Cuando ejecutas una sentencia de asignación, no hay salida. Python crea la variable y le da un
valor, pero la sentencia de asignación no tiene ningún efecto visible. Sin embargo, después de
crear una variable, puedes usarla como una expresión. Así que podemos mostrar el valor de
message así:
message
También puedes usar una variable como parte de una expresión con operadores aritméticos.
n + 25
2 * pi
Y puedes usar una variable cuando llamas a una función.
round(pi)
len(message)
Este tipo de figura se llama diagrama de estado porque muestra en qué estado está cada una de
las variables (piensa en ello como el estado mental de la variable). Usaremos diagramas de estado
a lo largo del libro para representar un modelo de cómo Python almacena variables y sus valores.
Si le das a una variable un nombre no válido, obtienes un error de sintaxis. El nombre million! no
es válido porque contiene puntuación.
million! = 1000000
class tampoco es válido, pero puede que no sea obvio por qué.
class = 'Self-Defence Against Fresh Fruit'
Resulta que class es una palabra clave, que es una palabra especial usada para especificar la
estructura de un programa. Las palabras clave no se pueden usar como nombres de variables.
Aquí tienes una lista completa de las palabras clave de Python:
False await else import pass
None break except in raise
True class finally is return
and continue for lambda try
as def from nonlocal while
assert del global not with
async elif if or yield
len(kwlist)
No tienes que memorizar esta lista. En la mayoría de los entornos de desarrollo, las palabras clave
se muestran en un color diferente; si intentas usar una como nombre de variable, lo sabrás.
2.4. La sentencia import
Para usar algunas características de Python, tienes que importarlas. Por ejemplo, la siguiente
sentencia importa el módulo math .
import math
Un módulo es una colección de variables y funciones. El módulo math proporciona una variable
llamada pi que contiene el valor de la constante matemática denotada . Podemos mostrar su
π
valor así.
[Link]
Para usar una variable de un módulo, tienes que usar el operador punto ( . ) entre el nombre del
módulo y el nombre de la variable.
El módulo math también contiene funciones. Por ejemplo, sqrt calcula raíces cuadradas.
[Link](25)
En este punto hemos visto dos formas de elevar un número a una potencia: podemos usar la
función [Link] o el operador de exponenciación, ** . Cualquiera de las dos está bien, pero el
operador se usa más a menudo que la función.
También hemos visto algunos tipos de sentencias. Una sentencia es una unidad de código que
tiene un efecto, pero no un valor. Por ejemplo, una sentencia de asignación crea una variable y le da
un valor, pero la sentencia en sí no tiene valor.
n = 17
De forma similar, una sentencia import tiene un efecto – importa un módulo para que podamos usar
las variables y funciones que contiene – pero no tiene efecto visible.
import math
Calcular el valor de una expresión se llama evaluación. Ejecutar una sentencia se llama ejecución.
2.7. Argumentos
Cuando llamas a una función, la expresión entre paréntesis se llama un argumento. Normalmente
explicaría por qué, pero en este caso el significado técnico de un término no tiene casi nada que
ver con el significado común de la palabra, así que ni siquiera lo intentaré.
Algunas de las funciones que hemos visto hasta ahora toman solo un argumento, como int .
int('101')
Algunas pueden tomar argumentos adicionales que son opcionales. Por ejemplo, int puede tomar
un segundo argumento que especifica la base del número.
int('101', 2)
Y si proporcionas un argumento con un tipo que la función no puede manejar, eso también es un
TypeError .
[Link]('123')
Este tipo de comprobación puede ser molesto cuando estás empezando, pero te ayuda a detectar
y corregir errores.
2.8. Comentarios
A medida que los programas se hacen más grandes y complicados, se vuelven más difíciles de leer.
Los lenguajes formales son densos, y a menudo es difícil mirar un fragmento de código y averiguar
qué hace y por qué.
Por esta razón, es buena idea añadir notas a tus programas para explicar en lenguaje natural qué
está haciendo el programa. Estas notas se llaman comentarios, y empiezan con el símbolo # .
# number of seconds in 42:42
seconds = 42 * 60 + 42
En este caso, el comentario aparece en una línea por sí solo. También puedes poner comentarios al
final de una línea:
miles = 10 / 1.61 # 10 kilometers in miles
Todo desde el # hasta el final de la línea se ignora—no tiene efecto en la ejecución del programa.
Los comentarios son más útiles cuando documentan características no obvias del código. Es
razonable suponer que el lector puede averiguar qué hace el código; es más útil explicar por qué.
Este comentario es redundante con el código y no sirve:
v = 8 # assign 8 to v
Los buenos nombres de variables pueden reducir la necesidad de comentarios, pero los nombres
largos pueden hacer que las expresiones complejas sean difíciles de leer, así que hay una
compensación.
2.9. Debugging
En un programa pueden ocurrir tres tipos de errores: errores de sintaxis, errores en tiempo de
ejecución y errores semánticos. Es útil distinguirlos para localizarlos más rápidamente.
Error de sintaxis: “Sintaxis” se refiere a la estructura de un programa y a las reglas sobre esa
estructura. Si hay un error de sintaxis en cualquier parte de tu programa, Python no ejecuta el
programa. Muestra un mensaje de error inmediatamente.
Error en tiempo de ejecución: Si no hay errores de sintaxis en tu programa, puede empezar a
ejecutarse. Pero si algo sale mal, Python muestra un mensaje de error y se detiene. Este tipo
de error se llama error en tiempo de ejecución. También se llama una excepción porque indica
que ha ocurrido algo excepcional.
Error semántico: El tercer tipo de error es “semántico”, lo que significa relacionado con el
significado. Si hay un error semántico en tu programa, se ejecuta sin generar mensajes de
error, pero no hace lo que pretendías. Identificar errores semánticos puede ser complicado
porque requiere que trabajes hacia atrás mirando la salida del programa e intentando averiguar
qué está haciendo.
Como hemos visto, un nombre de variable no válido es un error de sintaxis.
million! = 1000000
Si usas un operador con un tipo que no admite, eso es un error en tiempo de ejecución.
'126' / 3
Por último, aquí tienes un ejemplo de error semántico. Supongamos que queremos calcular la
media de 1 y 3 , pero olvidamos el orden de operaciones y escribimos esto:
1 + 3 / 2
Cuando se evalúa esta expresión, no produce un mensaje de error, así que no hay error de sintaxis
ni error en tiempo de ejecución. Pero el resultado no es la media de 1 y 3 , así que el programa no
es correcto. Este es un error semántico porque el programa se ejecuta pero no hace lo que se
pretendía.
2.10. Glosario
variable: Un nombre que se refiere a un valor.
sentencia de asignación: Una sentencia que asigna un valor a una variable.
diagrama de estado: Una representación gráfica de un conjunto de variables y los valores a los
que se refieren.
palabra clave: Una palabra especial usada para especificar la estructura de un programa.
sentencia de importación: Una sentencia que lee un archivo de módulo para que podamos usar
las variables y funciones que contiene.
módulo: Un archivo que contiene código Python, incluyendo definiciones de funciones y a veces
otras sentencias.
operador punto: El operador, . , usado para acceder a una función en otro módulo especificando
el nombre del módulo seguido de un punto y el nombre de la función.
evaluar: Realizar las operaciones de una expresión para calcular un valor.
sentencia: Una o más líneas de código que representan una orden o acción.
ejecutar: Ejecutar una sentencia y hacer lo que dice.
argumento: Un valor proporcionado a una función cuando se llama a la función.
comentario: Texto incluido en un programa que proporciona información sobre el programa pero
no tiene efecto en su ejecución.
error en tiempo de ejecución: Un error que hace que un programa muestre un mensaje de error y
termine.
excepción: Un error que se detecta mientras el programa se está ejecutando.
error semántico: Un error que hace que un programa haga algo incorrecto, pero no muestre un
mensaje de error.
2.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
2.11.2. Ejercicio
Repitiendo mi consejo del capítulo anterior: siempre que aprendas una característica nueva,
deberías cometer errores a propósito para ver qué falla.
Hemos visto que n = 17 es válido. ¿Qué pasa con 17 = n ?
¿Y con x = y = 1 ?
En algunos lenguajes, cada sentencia termina con punto y coma ( ; ). ¿Qué ocurre si pones un
punto y coma al final de una sentencia de Python?
¿Qué pasa si pones un punto al final de una sentencia?
¿Qué ocurre si escribes mal el nombre de un módulo e intentas importar maath ?
2.11.3. Ejercicio
Practica usando el intérprete de Python como calculadora:
Parte 1. El volumen de una esfera con radio es . ¿Cuál es el volumen de una esfera con
r
4 3
πr
radio 5? Empieza con una variable llamada radius y luego asigna el resultado a una variable
3
llamada volume . Muestra el resultado. Añade comentarios para indicar que radius está en
centímetros y volume en centímetros cúbicos.
Parte 2. Una regla de trigonometría dice que para cualquier valor de ,
x (cos x)
2 2
+ (sin x) = 1.
Veamos si es cierta para un valor específico de , como 42.
x
Crea una variable llamada x con este valor. Luego usa [Link] y [Link] para calcular el
seno y el coseno de , y la suma de sus cuadrados.
x
El resultado debería estar cerca de 1. Puede que no sea exactamente 1 porque la aritmética de
punto flotante no es exacta—solo es aproximadamente correcta.
Parte 3. Además de pi , la otra variable definida en el módulo math es e , que representa la base
del logaritmo natural, escrita en notación matemática como . Si no conoces este valor, pregunta a
e
Puede que notes que el último resultado es ligeramente diferente de los otros dos. Mira si puedes
averiguar cuál es correcto.
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Puedes comprar versiones impresas y ebook de Think Python 3e en [Link] y Amazon.
3. Funciones
En el capítulo anterior usamos varias funciones proporcionadas por Python, como int y float , y
algunas proporcionadas por el módulo math , como sqrt y pow . En este capítulo aprenderás a
crear tus propias funciones y a ejecutarlas. Y veremos cómo una función puede llamar a otra. Como
ejemplos, mostraremos letras de canciones de Monty Python. Estos ejemplos absurdos
demuestran una característica importante: la capacidad de escribir tus propias funciones es la base
de la programación.
Este capítulo también introduce una nueva sentencia, el bucle for , que se usa para repetir un
cálculo.
def es una palabra clave que indica que esto es una definición de función. El nombre de la función
es print_lyrics . Cualquier cosa que sea un nombre de variable válido también es un nombre de
función válido.
Los paréntesis vacíos después del nombre indican que esta función no recibe argumentos.
La primera línea de la definición de función se llama encabezado; el resto se llama cuerpo. El
encabezado tiene que terminar con dos puntos y el cuerpo tiene que estar indentado. Por
convención, la indentación es siempre de cuatro espacios. El cuerpo de esta función son dos
sentencias print ; en general, el cuerpo de una función puede contener cualquier número de
sentencias de cualquier tipo.
Definir una función crea un objeto función, que podemos mostrar así.
print_lyrics
La salida indica que print_lyrics es una función que no recibe argumentos. __main__ es el
nombre del módulo que contiene print_lyrics .
Ahora que hemos definido una función, podemos llamarla del mismo modo que llamamos a las
funciones incorporadas.
print_lyrics()
Cuando la función se ejecuta, ejecuta las sentencias del cuerpo, que muestran las dos primeras
líneas de “The Lumberjack Song”.
3.2. Parámetros
Algunas de las funciones que hemos visto requieren argumentos; por ejemplo, cuando llamas a
abs pasas un número como argumento. Algunas funciones reciben más de un argumento; por
ejemplo, [Link] recibe dos: la base y el exponente.
Aquí tienes la definición de una función que recibe un argumento.
def print_twice(string):
print(string)
print(string)
El nombre de variable entre paréntesis es un parámetro. Cuando se llama a la función, el valor del
argumento se asigna al parámetro. Por ejemplo, podemos llamar a print_twice así.
print_twice('Dennis Moore, ')
Ejecutar esta función tiene el mismo efecto que asignar el argumento al parámetro y luego ejecutar
el cuerpo de la función, así.
string = 'Dennis Moore, '
print(string)
print(string)
Podemos usar esta función para imprimir la primera línea de la canción, así.
spam = 'Spam, '
repeat(spam, 4)
Para mostrar las dos primeras líneas, podemos definir una nueva función que use repeat .
def first_two_lines():
repeat(spam, 4)
repeat(spam, 4)
Para mostrar las últimas tres líneas, podemos definir otra función, que también usa repeat .
def last_three_lines():
repeat(spam, 2)
print('(Lovely Spam, Wonderful Spam!)')
repeat(spam, 2)
last_three_lines()
Por último, podemos unirlo todo con una función que imprime la estrofa completa.
def print_verse():
first_two_lines()
last_three_lines()
print_verse()
Cuando ejecutamos print_verse , llama a first_two_lines , que llama a repeat , que llama a
print . Son muchas funciones.
Por supuesto, podríamos haber hecho lo mismo con menos funciones, pero el objetivo de este
ejemplo es mostrar cómo las funciones pueden trabajar juntas.
3.4. Repetición
Si queremos mostrar más de una estrofa, podemos usar una sentencia for . Aquí tienes un
ejemplo sencillo.
for i in range(2):
print(i)
La primera línea es un encabezado que termina con dos puntos. La segunda línea es el cuerpo, que
tiene que estar indentado.
El encabezado empieza con la palabra clave for , una nueva variable llamada i y otra palabra
clave, in . Usa la función range para crear una secuencia de dos valores, que son 0 y 1 . En
Python, cuando empezamos a contar, normalmente empezamos desde 0 .
Cuando la sentencia for se ejecuta, asigna el primer valor de range a i y luego ejecuta la
función print en el cuerpo, que muestra 0 .
Cuando llega al final del cuerpo, vuelve al encabezado, por eso esta sentencia se llama bucle. La
segunda vez que recorre el bucle, asigna el siguiente valor de range a i y lo muestra. Entonces,
como ese es el último valor de range , el bucle termina.
Así es como podemos usar un bucle for para imprimir dos estrofas de la canción.
for i in range(2):
print("Verse", i)
print_verse()
print()
Puedes poner un bucle for dentro de una función. Por ejemplo, print_n_verses recibe un
parámetro llamado n , que tiene que ser un entero, y muestra el número dado de estrofas.
def print_n_verses(n):
for i in range(n):
print_verse()
print()
En este ejemplo, no usamos i en el cuerpo del bucle, pero de todos modos tiene que haber un
nombre de variable en el encabezado.
Cuando cat_twice se ejecuta, crea una variable local llamada cat , que se destruye cuando
termina la función. Si intentamos mostrarla, obtenemos un NameError :
print(cat)
d1 = dict(line1=line1, line2=line2)
frame1 = make_frame(d1, name='__main__', dy=-0.3, loc='left')
d3 = dict(string=line1+line2)
frame3 = make_frame(d3, name='print_twice',
offsetx=0.04, offsety=-0.3, loc='left')
d4 = {"?": line1+line2}
frame4 = make_frame(d4, name='print',
offsetx=-0.22, offsety=0, loc='left')
Los marcos se organizan en un stack que indica qué función llamó a cuál, y así sucesivamente.
Leyendo desde abajo, print fue llamada por print_twice , que fue llamada por cat_twice , que
fue llamada por __main__ , que es un nombre especial para el marco superior. Cuando creas una
variable fuera de cualquier función, pertenece a __main__ .
En el marco de print , el signo de interrogación indica que no sabemos el nombre del parámetro.
Si tienes curiosidad, pregunta a un asistente virtual: «¿Cuáles son los parámetros de la función
print de Python?
3.7. Tracebacks
Cuando ocurre un error en tiempo de ejecución dentro de una función, Python muestra el nombre
de la función que se estaba ejecutando, el nombre de la función que la llamó, y así sucesivamente,
subiendo por el stack. Para ver un ejemplo, definiré una versión de print_twice que contiene un
error: intenta imprimir cat , que es una variable local de otra función.
def print_twice(string):
print(cat) # NameError
print(cat)
%xmode Verbose
cat_twice(line1, line2)
El mensaje de error incluye un traceback, que muestra la función que se estaba ejecutando
cuando ocurrió el error, la función que la llamó, y así sucesivamente. En este ejemplo, muestra que
cat_twice llamó a print_twice , y que el error ocurrió en un print_twice .
El orden de las funciones en el traceback es el mismo que el orden de los marcos en el diagrama de
pila. La función que se estaba ejecutando está abajo del todo.
3.10. Glosario
definición de función: Una sentencia que crea una función.
encabezado: La primera línea de una definición de función.
cuerpo: La secuencia de sentencias dentro de una definición de función.
objeto de función: Un valor creado por una definición de función. El nombre de la función es una
variable que se refiere a un objeto función.
parámetro: Un nombre usado dentro de una función para referirse al valor pasado como
argumento.
bucle: Una sentencia que ejecuta una o más sentencias, a menudo repetidamente.
variable local: Una variable definida dentro de una función, y a la que solo se puede acceder
dentro de la función.
diagrama de pila: Una representación gráfica de un stack de funciones, sus variables y los valores
a los que se refieren.
marco: Una caja en un diagrama de pila que representa una llamada a función. Contiene las
variables locales y los parámetros de la función.
traceback: Una lista de las funciones que se están ejecutando, impresa cuando ocurre una
excepción.
3.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
3.11.2. Ejercicio
Escribe una función llamada print_right que reciba un string llamado text como parámetro e
imprima el string con suficientes espacios iniciales para que la última letra del string esté en la
columna 40 de la pantalla.
Pista: usa la función len , el operador de concatenación de strings ( + ) y el operador de repetición
de strings ( * ).
Aquí tienes un ejemplo que muestra cómo debería funcionar.
print_right("Monty")
print_right("Python's")
print_right("Flying Circus")
3.11.3. Ejercicio
Escribe una función llamada triangle que reciba un string y un entero, y dibuje una pirámide con
la altura dada, hecha usando copias del string. Aquí tienes un ejemplo de una pirámide con 5
niveles, usando el string 'L' .
triangle('L', 5)
3.11.4. Ejercicio
Escribe una función llamada rectangle que reciba un string y dos enteros, y dibuje un rectángulo
con la anchura y altura dadas, hecho usando copias del string. Aquí tienes un ejemplo de un
rectángulo con anchura 5 y altura 4 , hecho con el string 'H' .
rectangle('H', 5, 4)
3.11.5. Ejercicio
La canción “99 Bottles of Beer” empieza con esta estrofa:
99 bottles of beer on the wall
99 bottles of beer
Take one down, pass it around
98 bottles of beer on the wall
Luego la segunda estrofa es igual, excepto que empieza con 98 botellas y termina con 97. La
canción continúa, durante muchísimo tiempo, hasta que hay 0 botellas de cerveza.
Escribe una función llamada bottle_verse que reciba un número como parámetro y muestre la
estrofa que empieza con el número dado de botellas.
Pista: considera empezar con una función que pueda imprimir la primera, segunda o última línea de
la estrofa, y luego úsala para escribir bottle_verse .
Usa esta llamada a función para mostrar la primera estrofa.
bottle_verse(99)
Si quieres imprimir la canción completa, puedes usar este bucle for , que cuenta hacia atrás
desde 99 hasta 1 . No tienes que entender completamente este ejemplo; aprenderemos más
sobre los bucles for y la función range más adelante.
for n in range(99, 0, -1):
bottle_verse(n)
print()
4. Funciones e interfaces
Este capítulo introduce un módulo llamado jupyturtle , que te permite crear dibujos sencillos
dando instrucciones a una turtle imaginaria. Usaremos este módulo para escribir funciones que
dibujan cuadrados, polígonos y círculos, y para demostrar el diseño de interfaces, que es una
forma de diseñar funciones que trabajan juntas.
Ahora podemos usar las funciones definidas en el módulo, como make_turtle y forward .
jupyturtle.make_turtle()
[Link](100)
make_turtle crea un canvas, que es un espacio en la pantalla donde podemos dibujar, y una
turtle, que se representa con un caparazón circular y una cabeza triangular. El círculo muestra la
ubicación de la turtle y el triángulo indica la dirección hacia la que mira.
forward mueve la turtle una distancia dada en la dirección hacia la que mira, dibujando un
segmento de línea por el camino. La distancia está en unidades arbitrarias: el tamaño real depende
de la pantalla de tu ordenador.
Usaremos muchas veces funciones definidas en el módulo jupyturtle , así que estaría bien no
tener que escribir el nombre del módulo cada vez. Eso es posible si importamos el módulo así.
from jupyturtle import make_turtle, forward
make_turtle()
forward(100)
jupyturtle proporciona otras dos funciones que usaremos, llamadas left y right . Las
importaremos así.
from jupyturtle import left, right
left hace que la turtle gire a la izquierda. Recibe un argumento, que es el ángulo del giro en
grados. Por ejemplo, podemos hacer un giro a la izquierda de 90 grados así.
make_turtle()
forward(50)
left(90)
forward(50)
Este programa mueve la turtle hacia el este y luego hacia el norte, dejando dos segmentos de línea
detrás. Antes de continuar, prueba si puedes modificar el programa anterior para hacer un
cuadrado.
4.2. Hacer un cuadrado
Aquí tienes una forma de hacer un cuadrado.
make_turtle()
forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
Como este programa repite el mismo par de líneas cuatro veces, podemos hacer lo mismo de
forma más concisa con un bucle for .
make_turtle()
for i in range(4):
forward(50)
left(90)
Añadir un parámetro a una función se llama generalización porque hace que la función sea más
general: con la versión anterior, el cuadrado siempre tiene el mismo tamaño; con esta versión
puede tener cualquier tamaño.
Si añadimos otro parámetro, podemos hacerla aún más general. La siguiente función dibuja
polígonos regulares con un número dado de lados.
def polygon(n, length):
angle = 360 / n
for i in range(n):
forward(length)
left(angle)
En un polígono regular con n lados, el ángulo entre lados adyacentes es de 360 / n grados.
El siguiente ejemplo dibuja un polígono de 7 lados con longitud de lado 30 .
make_turtle()
polygon(7, 30)
Cuando una función tiene más de unos pocos argumentos numéricos, es fácil olvidar qué son o en
qué orden deberían ir. Puede ser una buena idea incluir los nombres de los parámetros en la lista
de argumentos.
make_turtle()
polygon(n=7, length=30)
A veces se llaman “argumentos con nombre” porque incluyen los nombres de los parámetros. Pero
en Python se llaman más a menudo argumentos de palabra clave (no deben confundirse con las
palabras clave de Python como for y def ).
Este uso del operador de asignación, = , sirve como recordatorio de cómo funcionan los
argumentos y los parámetros: cuando llamas a una función, los argumentos se asignan a los
parámetros.
def circle(radius):
circumference = 2 * [Link] * radius
n = 30
length = circumference / n
polygon(n, length)
circle recibe el radio del círculo como parámetro. Calcula circumference , que es la
circunferencia de un círculo con el radio dado. n es el número de lados, así que circumference /
n es la longitud de cada lado.
Esta función podría tardar mucho en ejecutarse. Podemos acelerarla llamando a make_turtle con
un argumento de palabra clave llamado delay que establece el tiempo, en segundos, que la turtle
espera después de cada paso. El valor predeterminado es 0.2 segundos; si lo establecemos en
0.02 , se ejecuta unas 10 veces más rápido.
make_turtle(delay=0.02)
circle(30)
Una limitación de esta solución es que n es una constante, lo que significa que para círculos muy
grandes los lados son demasiado largos, y para círculos pequeños perdemos tiempo dibujando
lados muy cortos. Una opción es generalizar la función tomando n como parámetro. Pero
mantengámoslo sencillo por ahora.
4.5. Refactorización
Ahora escribamos una versión más general de circle , llamada arc , que recibe un segundo
parámetro, angle , y dibuja un arco de un círculo que abarca el ángulo dado. Por ejemplo, si
angle es 360 grados, dibuja un círculo completo. Si angle es 180 grados, dibuja un
semicírculo.
Para escribir circle , pudimos reutilizar polygon , porque un polígono con muchos lados es una
buena aproximación de un círculo. Pero no podemos usar polygon para escribir arc .
En su lugar, crearemos una versión más general de polygon , llamada polyline .
def polyline(n, length, angle):
for i in range(n):
forward(length)
left(angle)
polyline recibe como parámetros el número de segmentos de línea que debe dibujar, n , la
longitud de los segmentos, length , y el ángulo entre ellos, angle .
Ahora podemos reescribir polygon para que use polyline .
def polygon(n, length):
angle = 360.0 / n
polyline(n, length, angle)
arc es similar a circle , salvo que calcula arc_length , que es una fracción de la circunferencia
de un círculo.
Por último, podemos reescribir circle para que use arc .
def circle(radius):
arc(radius, 360)
Para comprobar que estas funciones funcionan como esperamos, las usaremos para dibujar algo
parecido a un caracol. Con delay=0 , la turtle va lo más rápido posible.
make_turtle(delay=0)
polygon(n=20, length=9)
arc(radius=70, angle=70)
circle(radius=10)
En este ejemplo, empezamos con código que funcionaba y lo reorganizamos con funciones
diferentes. Los cambios como este, que mejoran el código sin cambiar su comportamiento, se
llaman refactorización.
Si lo hubiéramos planeado con antelación, quizá habríamos escrito polyline primero y evitado la
refactorización, pero a menudo no sabes lo suficiente al principio de un proyecto para diseñar
todas las funciones. Una vez que empiezas a programar, entiendes mejor el problema. A veces la
refactorización es una señal de que has aprendido algo.
Observa que el valor de angle en polyline es diferente del valor de angle en arc . Los
parámetros son locales, lo que significa que puedes usar el mismo nombre de parámetro en
funciones diferentes; es una variable distinta en cada función y puede referirse a un valor diferente.
Estas dos funciones tienen la misma interfaz: reciben los mismos parámetros y hacen lo mismo,
pero tienen implementaciones diferentes.
4.8. Docstrings
Un docstring es un string al principio de una función que explica la interfaz (“doc” es la abreviatura
de “documentación”). Aquí tienes un ejemplo:
def polyline(n, length, angle):
"""Draws line segments with the given length and angle between them.
Por convención, los docstrings son strings entre comillas triples, también conocidos como strings
multilínea porque las comillas triples permiten que el string ocupe más de una línea.
Un docstring debería:
Explicar de forma concisa qué hace la función, sin entrar en los detalles de cómo funciona,
Explicar qué efecto tiene cada parámetro en el comportamiento de la función, e
Indicar qué tipo debería tener cada parámetro, si no es obvio.
Escribir este tipo de documentación es una parte importante del diseño de interfaces. Una interfaz
bien diseñada debería ser sencilla de explicar; si te cuesta explicar una de tus funciones, quizá la
interfaz podría mejorarse.
4.9. Depuración
Una interfaz es como un contrato entre una función y quien la llama. Quien llama acepta
proporcionar ciertos argumentos y la función acepta hacer cierto trabajo.
Por ejemplo, polyline requiere tres argumentos: n tiene que ser un entero; length debería ser
un número positivo; y angle tiene que ser un número, que se entiende que está en grados.
Estos requisitos se llaman precondiciones porque se supone que deben ser verdaderos antes de
que la función empiece a ejecutarse. Por el contrario, las condiciones al final de la función son
postcondiciones. Las postcondiciones incluyen el efecto previsto de la función (como dibujar
segmentos de línea) y cualquier efecto secundario (como mover la turtle o hacer otros cambios).
Las precondiciones son responsabilidad de quien llama. Si quien llama viola una precondición y la
función no funciona correctamente, el bug está en quien llama, no en la función.
Si las precondiciones se satisfacen y las postcondiciones no, el bug está en la función. Si tus
precondiciones y postcondiciones están claras, pueden ayudar con la depuración.
4.10. Glosario
diseño de interfaces: Un proceso para diseñar la interfaz de una función, que incluye los
parámetros que debería recibir.
lienzo: Una ventana usada para mostrar elementos gráficos, incluyendo líneas, círculos,
rectángulos y otras formas.
encapsulación: El proceso de transformar una secuencia de sentencias en una definición de
función.
generalización: El proceso de reemplazar algo innecesariamente específico (como un número) por
algo adecuadamente general (como una variable o un parámetro).
argumento de palabra clave: Un argumento que incluye el nombre del parámetro.
refactorización: El proceso de modificar un programa que funciona para mejorar las interfaces de
las funciones y otras cualidades del código.
plan de desarrollo: Un proceso para escribir programas.
docstring: Un string que aparece al principio de una definición de función para documentar la
interfaz de la función.
string multilínea: Un string encerrado entre comillas triples que puede ocupar más de una línea de
un programa.
precondición: Un requisito que debería satisfacer quien llama antes de que empiece una función.
postcondición: Un requisito que debería satisfacer la función antes de terminar.
4.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
Para los ejercicios siguientes, hay algunas funciones más de turtle que quizá quieras usar.
penup levanta el lápiz imaginario de la turtle para que no deje rastro cuando se mueve.
La siguiente función usa penup y pendown para mover la turtle sin dejar rastro.
from jupyturtle import penup, pendown
def jump(length):
"""Move forward length units without leaving a trail.
4.11.1. Ejercicio
Escribe una función llamada rectangle que dibuje un rectángulo con longitudes de lado dadas.
Por ejemplo, aquí tienes un rectángulo de 80 unidades de ancho y 40 unidades de alto.
Puedes usar el siguiente código para probar tu función.
make_turtle()
rectangle(80, 40)
4.11.2. Ejercicio
Escribe una función llamada rhombus que dibuje un rombo con una longitud de lado dada y un
ángulo interior dado. Por ejemplo, aquí tienes un rombo con longitud de lado 50 y un ángulo
interior de 60 grados.
Puedes usar el siguiente código para probar tu función.
make_turtle()
rhombus(50, 60)
4.11.3. Ejercicio
Ahora escribe una función más general llamada parallelogram que dibuje un cuadrilátero con
lados paralelos. Luego reescribe rectangle y rhombus para que usen parallelogram .
Puedes usar el siguiente código para probar tus funciones.
make_turtle(width=400)
jump(-120)
rectangle(80, 40)
jump(100)
rhombus(50, 60)
jump(80)
parallelogram(80, 50, 60)
4.11.4. Ejercicio
Escribe un conjunto de funciones adecuadamente general que pueda dibujar formas como esta.
Pista: escribe una función llamada triangle que dibuje un segmento triangular, y luego una
función llamada draw_pie que use triangle .
Puedes usar el siguiente código para probar tus funciones.
turtle = make_turtle(delay=0)
jump(-80)
size = 40
draw_pie(5, size)
jump(2*size)
draw_pie(6, size)
jump(2*size)
draw_pie(7, size)
4.11.5. Ejercicio
Escribe un conjunto de funciones adecuadamente general que pueda dibujar flores como esta.
Pista: usa arc para escribir una función llamada petal que dibuje un pétalo de flor.
Puedes usar el siguiente código para probar tus funciones.
Como la solución dibuja muchos segmentos de línea pequeños, tiende a ralentizarse mientras se
ejecuta. Para evitarlo, puedes añadir el argumento de palabra clave auto_render=False para
evitar dibujar después de cada paso, y luego llamar a la función render al final para mostrar el
resultado.
Mientras estés depurando, quizá quieras quitar auto_render=False .
from jupyturtle import render
turtle = make_turtle(auto_render=False)
jump(-60)
n = 7
radius = 60
angle = 60
flower(n, radius, angle)
jump(120)
n = 9
radius = 40
angle = 85
flower(n, radius, angle)
render()
def circle(radius):
circumference = 2 * [Link] * radius
n = 30
length = circumference / n
polygon(n, length)
make_turtle(delay=0)
circle(30)
Ten en cuenta que el resultado podría usar características que aún no hemos visto, y podría tener
errores. Copia el código del asistente virtual y comprueba si puedes hacerlo funcionar. Si no
obtuviste lo que querías, prueba a modificar el prompt.
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Puedes comprar versiones impresas y ebook de Think Python 3e en [Link] y Amazon.
5. Condicionales y recursión
El tema principal de este capítulo es la sentencia if , que ejecuta código distinto según el estado
del programa. Y con la sentencia if podremos explorar una de las ideas más potentes de la
informática: la recursión.
Pero empezaremos con tres características nuevas: el operador módulo, las expresiones booleanas
y los operadores lógicos.
Pero normalmente no escribimos las horas con decimales. La división entera devuelve el número
entero de horas, redondeando hacia abajo:
minutes = 105
hours = minutes // 60
hours
O podrías usar el operador módulo, % , que divide dos números y devuelve el resto.
remainder = minutes % 60
remainder
El operador módulo es más útil de lo que podría parecer. Por ejemplo, puede comprobar si un
número es divisible por otro: si x % y es cero, entonces x es divisible por y .
También puede extraer el dígito o los dígitos más a la derecha de un número. Por ejemplo, x % 10
produce el dígito más a la derecha de x (en base 10). De forma similar, x % 100 produce los dos
últimos dígitos.
x = 123
x % 10
x % 100
Por último, el operador módulo puede hacer «aritmética de reloj». Por ejemplo, si un evento
empieza a las 11 AM y dura tres horas, podemos usar el operador módulo para averiguar a qué hora
termina.
start = 11
duration = 3
end = (start + duration) % 12
end
5 == 7
Un error común es usar un solo signo igual ( = ) en lugar de un doble signo igual ( == ). Recuerda
que = asigna un valor a una variable y == compara dos valores.
x = 5
y = 7
x == y
True y False son valores especiales que pertenecen al tipo bool ; no son cadenas:
type(True)
type(False)
La siguiente expresión es True si una o ambas condiciones son verdaderas, es decir, si el número
es divisible por 2 o por 3:
x % 2 == 0 or x % 3 == 0
Por último, el operador not niega una expresión booleana, de modo que la siguiente expresión es
True si x > y es False .
not x > y
En sentido estricto, los operandos de un operador lógico deberían ser expresiones booleanas, pero
Python no es muy estricto. Cualquier número distinto de cero se interpreta como True :
42 and True
Esta flexibilidad puede ser útil, pero tiene algunas sutilezas que pueden resultar confusas. Quizá
prefieras evitarla.
5.4. Sentencias if
Para escribir programas útiles, casi siempre necesitamos la capacidad de comprobar condiciones y
cambiar el comportamiento del programa en consecuencia. Las sentencias condicionales nos
dan esta capacidad. La forma más sencilla es la sentencia if :
if x > 0:
print('x is positive')
if es una palabra clave de Python. Las sentencias if tienen la misma estructura que las
definiciones de función: una cabecera seguida de una sentencia indentada o de una secuencia de
sentencias llamada bloque.
La expresión booleana que va después de if se llama condición. Si es verdadera, se ejecutan las
sentencias del bloque indentado. Si no, no se ejecutan.
No hay límite para el número de sentencias que pueden aparecer en el bloque, pero tiene que
haber al menos una. A veces resulta útil tener un bloque que no hace nada, normalmente como
marcador de posición para código que todavía no has escrito. En ese caso, puedes usar la
sentencia pass , que no hace nada.
if x < 0:
pass # TODO: need to handle negative values!
La palabra TODO en un comentario es un recordatorio convencional de que hay algo que tienes que
hacer más adelante.
La sentencia if exterior contiene dos ramas. La primera rama contiene una sentencia sencilla. La
segunda rama contiene otra sentencia if , que tiene dos ramas propias. Esas dos ramas son
ambas sentencias sencillas, aunque también podrían haber sido sentencias condicionales.
Aunque la indentación de las sentencias hace visible la estructura, los condicionales anidados
pueden ser difíciles de leer. Te sugiero que los evites cuando puedas.
Los operadores lógicos suelen proporcionar una forma de simplificar sentencias condicionales
anidadas. Aquí tienes un ejemplo con un condicional anidado.
if 0 < x:
if x < 10:
print('x is a positive single-digit number.')
La sentencia print se ejecuta solo si superamos ambos condicionales, así que conseguimos el
mismo efecto con el operador and .
if 0 < x and x < 10:
print('x is a positive single-digit number.')
Para este tipo de condición, Python ofrece una opción más concisa:
if 0 < x < 10:
print('x is a positive single-digit number.')
5.8. Recursión
Es legal que una función se llame a sí misma. Puede que no sea obvio por qué eso es algo bueno,
pero resulta ser una de las cosas más mágicas que puede hacer un programa. Aquí tienes un
ejemplo.
def countdown(n):
if n <= 0:
print('Blastoff!')
else:
print(n)
countdown(n-1)
La ejecución de countdown empieza con n=3 , y como n es mayor que 0 , muestra 3 y luego se
llama a sí misma.…
La ejecución de countdown empieza con n=2 , y como n es mayor que 0 , muestra 2 y
luego se llama a sí misma.…
La ejecución de countdown empieza con n=1 , y como n es mayor que 0 , muestra 1
y luego se llama a sí misma.…
La ejecución de countdown empieza con n=0 , y como n no es mayor que 0 ,
muestra «Blastoff!» y retorna.
La countdown que recibió n=1 retorna.
La countdown que recibió n=2 retorna.
La countdown que recibió n=3 retorna.
Una función que se llama a sí misma es recursiva. Como otro ejemplo, podemos escribir una
función que imprime una cadena n veces.
def print_n_times(string, n):
if n > 0:
print(string)
print_n_times(string, n-1)
Para ejemplos sencillos como este, probablemente sea más fácil usar un bucle for . Pero más
adelante veremos ejemplos que son difíciles de escribir con un bucle for y fáciles de escribir con
recursión, así que conviene empezar pronto.
frames = []
for n in [3,2,1,0]:
d = dict(n=n)
frame = make_frame(d, name='countdown', dy=-0.3, loc='left')
[Link](frame)
Los cuatro marcos de countdown tienen valores distintos para el parámetro n . La parte inferior
del stack, donde n=0 , se llama caso base. No hace una llamada recursiva, así que no hay más
marcos.
from diagram import make_frame, Stack
from diagram import diagram, adjust
frames = []
for n in [2,1,0]:
d = dict(string='Hello', n=n)
frame = make_frame(d, name='print_n_times', dx=1.3, loc='left')
[Link](frame)
Cada vez que se llama a recurse , se llama a sí misma, lo que crea otro marco. En Python, hay un
límite para el número de marcos que pueden estar en el stack al mismo tiempo. Si un programa
supera el límite, provoca un error en tiempo de ejecución.
%xmode Context
recurse()
El traceback indica que había casi 3000 marcos en el stack cuando ocurrió el error.
Si te encuentras por accidente con una recursión infinita, revisa tu función para confirmar que hay
un caso base que no hace una llamada recursiva. Y si hay un caso base, comprueba si tienes
garantizado llegar a él.
Antes de obtener entrada del usuario, puede que quieras mostrar un prompt que le indique qué
debe escribir. input puede recibir un prompt como argumento:
name = input('What...is your name?\n')
name
La secuencia al final del prompt representa una nueva línea, que es un carácter especial que
provoca un salto de línea; así, la entrada del usuario aparece debajo del prompt.
Si esperas que el usuario escriba un entero, puedes usar la función int para convertir el valor de
retorno a int .
prompt = 'What...is the airspeed velocity of an unladen swallow?\n'
speed = input(prompt)
speed
int(speed)
5.12. Debugging
Cuando ocurre un error de sintaxis o en tiempo de ejecución, el mensaje de error contiene mucha
información, pero puede resultar abrumador. Las partes más útiles suelen ser:
Qué tipo de error fue, y
Dónde ocurrió.
Los errores de sintaxis suelen ser fáciles de encontrar, pero hay algunas trampas. Los errores
relacionados con espacios y tabulaciones pueden ser complicados porque son invisibles y estamos
acostumbrados a ignorarlos.
x = 5
y = 6
En este ejemplo, el problema es que la segunda línea está indentada con un espacio. Pero el
mensaje de error señala a y , lo que resulta engañoso. Los mensajes de error indican dónde se
descubrió el problema, pero el error real podría estar antes en el código.
Lo mismo ocurre con los errores en tiempo de ejecución. Por ejemplo, supón que intentas convertir
una razón a decibelios, así:
%xmode Context
import math
numerator = 9
denominator = 10
ratio = numerator // denominator
decibels = 10 * math.log10(ratio)
El mensaje de error indica la línea 5, pero no hay nada malo en esa línea. El problema está en la
línea 4, que usa división entera en lugar de división de punto flotante; como resultado, el valor de
ratio es 0 . Cuando llamamos a math.log10 , obtenemos un ValueError con el mensaje math
domain error , porque 0 no está en el «dominio» de argumentos válidos para math.log10 , ya
que el logaritmo de 0 no está definido.
En general, deberías tomarte el tiempo de leer cuidadosamente los mensajes de error, pero no
asumas que todo lo que dicen es correcto.
5.13. Glosario
recursión: El proceso de llamar a la función que se está ejecutando en ese momento.
operador módulo: Un operador, % , que funciona con enteros y devuelve el resto cuando un
número se divide entre otro.
expresión booleana: Una expresión cuyo valor es True o False .
operador relacional: Uno de los operadores que compara sus operandos: == , != , > , < , >= y
<= .
operador lógico: Uno de los operadores que combina expresiones booleanas, incluidos and , or
y not .
sentencia condicional: Una sentencia que controla el flujo de ejecución dependiendo de alguna
condición.
condición: La expresión booleana en una sentencia condicional que determina qué rama se
ejecuta.
bloque: Una o más sentencias indentadas para indicar que forman parte de otra sentencia.
rama: Una de las secuencias alternativas de sentencias en una sentencia condicional.
condicional encadenado: Una sentencia condicional con una serie de ramas alternativas.
condicional anidado: Una sentencia condicional que aparece en una de las ramas de otra
sentencia condicional.
recursiva: Una función que se llama a sí misma es recursiva.
caso base: Una rama condicional en una función recursiva que no hace una llamada recursiva.
recursión infinita: Una recursión que no tiene caso base o que nunca lo alcanza. Con el tiempo,
una recursión infinita provoca un error en tiempo de ejecución.
nueva línea: Un carácter que crea un salto de línea entre dos partes de una cadena.
5.14. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
En este capítulo, vimos dos formas de escribir una sentencia if con tres ramas: usando un
condicional encadenado o un condicional anidado. Puedes usar un asistente virtual para convertir
de una forma a la otra. Por ejemplo, pregunta a un asistente virtual: «Convierte esta sentencia en
un condicional encadenado».
x = 5
y = 7
if x == y:
print('x and y are equal')
else:
if x < y:
print('x is less than y')
else:
print('x is greater than y')
Aquí tienes un intento de función recursiva que cuenta hacia atrás de dos en dos.
def countdown_by_two(n):
if n == 0:
print('Blastoff!')
else:
print(n)
countdown_by_two(n-2)
Parece funcionar.
countdown_by_two(6)
Pero tiene un error. Pregunta a un asistente virtual qué está mal y cómo arreglarlo. Pega aquí la
solución que te proporcione y pruébala.
5.14.2. Ejercicio
El módulo time proporciona una función, también llamada time , que devuelve el número de
segundos transcurridos desde la «época Unix», que es el 1 de enero de 1970, 00:00:00 UTC
(tiempo universal coordinado).
from time import time
now = time()
now
Usa la división entera y el operador módulo para calcular el número de días transcurridos desde el 1
de enero de 1970 y la hora actual del día en horas, minutos y segundos.
Puedes leer más sobre el módulo time en [Link]
5.14.3. Ejercicio
Si te dan tres palos, puede que puedas colocarlos formando un triángulo, o puede que no. Por
ejemplo, si uno de los palos mide 12 pulgadas y los otros dos miden una pulgada, no podrás hacer
que los palos cortos se encuentren en el centro. Para tres longitudes cualesquiera, hay una prueba
para ver si es posible formar un triángulo:
Si cualquiera de las tres longitudes es mayor que la suma de las otras dos, entonces no
puedes formar un triángulo. En caso contrario, sí puedes. (Si la suma de dos longitudes es
igual a la tercera, forman lo que se llama un triángulo «degenerado».)
Escribe una función llamada is_triangle que reciba tres enteros como argumentos, y que
imprima «Yes» o «No», dependiendo de si se puede o no formar un triángulo con palos de las
longitudes dadas. Pista: usa un condicional encadenado.
Prueba tu función con los siguientes casos.
is_triangle(4, 5, 6) # should be Yes
is_triangle(6, 2, 3) # should be No
5.14.4. Ejercicio
¿Cuál es la salida del siguiente programa? Dibuja un diagrama de stack que muestre el estado del
programa cuando imprime el resultado.
def recurse(n, s):
if n == 0:
print(s)
else:
recurse(n-1, n+s)
recurse(3, 0)
5.14.5. Ejercicio
Los siguientes ejercicios usan el módulo jupyturtle , descrito en el Capítulo 4.
Lee la siguiente función y mira si puedes averiguar qué hace. Luego ejecútala y comprueba si
acertaste. Ajusta los valores de length , angle y factor , y observa qué efecto tienen en el
resultado. Si no tienes claro cómo funciona, prueba a preguntar a un asistente virtual.
from jupyturtle import forward, left, right, back
def draw(length):
angle = 50
factor = 0.6
if length > 5:
forward(length)
left(angle)
draw(factor * length)
right(2 * angle)
draw(factor * length)
left(angle)
back(length)
5.14.6. Ejercicio
Pregunta a un asistente virtual: «¿Qué es la curva de Koch?»
Para dibujar una curva de Koch con longitud x , todo lo que tienes que hacer es
1. Dibujar una curva de Koch con longitud x/3 .
2. Girar a la izquierda 60 grados.
3. Dibujar una curva de Koch con longitud x/3 .
4. Girar a la derecha 120 grados.
5. Dibujar una curva de Koch con longitud x/3 .
6. Girar a la izquierda 60 grados.
7. Dibujar una curva de Koch con longitud x/3 .
La excepción es si x es menor que 5 ; en ese caso, puedes dibujar simplemente una línea recta
con longitud x .
Escribe una función llamada koch que reciba x como argumento y dibuje una curva de Koch con
la longitud dada.
El resultado debería verse así:
make_turtle(delay=0)
koch(120)
Cuando tengas koch funcionando, puedes usar este bucle para dibujar tres curvas de Koch con
forma de copo de nieve.
make_turtle(delay=0, height=300)
for i in range(3):
koch(120)
right(120)
5.14.7. Ejercicio
Los asistentes virtuales conocen las funciones del módulo jupyturtle , pero hay muchas
versiones de estas funciones, con nombres distintos, así que un asistente virtual podría no saber
de cuál estás hablando.
Para resolver este problema, puedes proporcionar información adicional antes de hacer una
pregunta. Por ejemplo, podrías empezar un prompt con «Aquí tienes un programa que usa el
módulo jupyturtle », y luego pegar uno de los ejemplos de este capítulo. Después de eso, el
asistente virtual debería poder generar código que use este módulo.
Como ejemplo, pide a un asistente virtual un programa que dibuje un triángulo de Sierpiński. El
código que obtengas debería ser un buen punto de partida, pero quizá tengas que hacer algo de
debugging. Si el primer intento no funciona, puedes contarle al asistente virtual qué ocurrió y pedir
ayuda, o puedes depurarlo por tu cuenta.
Este es un posible aspecto del resultado, aunque la versión que obtengas podría ser diferente.
make_turtle(delay=0, height=200)
draw_sierpinski(100, 3)
6. Valores de retorno
En capítulos anteriores, hemos usado funciones integradas – como abs y round – y funciones
del módulo math – como sqrt y pow . Cuando llamas a una de estas funciones, devuelve un valor
que puedes asignar a una variable o usar como parte de una expresión.
Las funciones que hemos escrito hasta ahora son diferentes. Algunas usan la función print para
mostrar valores, y otras usan funciones de turtle para dibujar figuras. Pero no devuelven valores
que asignemos a variables o usemos en expresiones.
En este capítulo veremos cómo escribir funciones que devuelven valores.
[Link](42 / [Link])
circle_area toma radius como parámetro y calcula el área de un círculo con ese radio.
La última línea es una sentencia return que devuelve el valor de area .
Si llamamos a la función así, Jupyter muestra el valor de retorno.
circle_area(radius)
area es una variable local dentro de una función, así que no podemos acceder a ella desde fuera
de la función.
6.2. Y algunas tienen None
Si una función no tiene una sentencia return , devuelve None , que es un valor especial como
True y False . Por ejemplo, aquí está la función repeat del Capítulo 3.
Esta función usa la función print para mostrar una cadena, pero no usa una sentencia return
para devolver un valor. Si asignamos el resultado a una variable, muestra la cadena de todos
modos.
result = repeat('Finland, ', 3)
result en realidad tiene un valor, pero Jupyter no lo muestra. Sin embargo, podemos mostrarlo
así.
print(result)
Observa que podemos usar una expresión en una sentencia return , no solo una variable.
Con esta versión, podemos asignar el resultado a una variable. Cuando la función se ejecuta, no
muestra nada.
line = repeat_string('Spam, ', 4)
Una función como esta se llama función pura porque no muestra nada ni tiene ningún otro efecto
– aparte de devolver un valor.
Como otro ejemplo, aquí tienes una versión de absolute_value con una sentencia return
adicional al final.
def absolute_value_extra_return(x):
if x < 0:
return -x
else:
return x
2 2
distance = √ (x2 − x1) + (y2 − y1)
El primer paso es considerar cómo debería ser una función distance en Python – es decir, cuáles
son las entradas (parámetros) y cuál es la salida (valor de retorno).
Para esta función, las entradas son las coordenadas de los puntos. El valor de retorno es la
distancia. De inmediato puedes escribir un esquema de la función:
def distance(x1, y1, x2, y2):
return 0.0
Esta versión todavía no calcula distancias – siempre devuelve cero. Pero es una función completa
con un valor de retorno, lo que significa que puedes probarla antes de hacerla más complicada.
Para probar la nueva función, la llamaremos con argumentos de ejemplo:
distance(1, 2, 4, 6)
Elegí estos valores para que la distancia horizontal sea 3 y la distancia vertical sea 4 . Así, el
resultado es 5 , la hipotenusa de un triángulo rectángulo 3-4-5 . Al probar una función, es útil
conocer la respuesta correcta.
En este punto hemos confirmado que la función se ejecuta y devuelve un valor, y podemos
empezar a añadir código al cuerpo. Un buen siguiente paso es encontrar las diferencias x2 - x1 e
y2 - y1 . Aquí tienes una versión que almacena esos valores en variables temporales y los
muestra.
def distance(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
print('dx is', dx)
print('dy is', dy)
return 0.0
Y probarla.
distance(1, 2, 4, 6)
Esta versión de distance es una función pura. Si la llamamos así, solo se muestra el resultado.
distance(1, 2, 4, 6)
Las sentencias print que escribimos son útiles para depurar, pero una vez que la función
funciona, podemos eliminarlas. El código de ese tipo se llama andamiaje porque ayuda a construir
el programa, pero no forma parte del producto final.
Este ejemplo demuestra el desarrollo incremental. Los aspectos clave de este proceso son:
1. Empieza con un programa que funcione, haz cambios pequeños y prueba después de cada
cambio.
2. Usa variables para guardar valores intermedios, de modo que puedas mostrarlos y
comprobarlos.
3. Una vez que el programa funcione, elimina el andamiaje.
En cualquier punto, si hay un error, deberías tener una buena idea de dónde está. El desarrollo
incremental puede ahorrarte mucho tiempo de debugging.
Dentro de la función, el resultado del operador == es un booleano, así que podemos escribir la
función de forma más concisa devolviéndolo directamente.
def is_divisible(x, y):
return x % y == 0
Esta definición dice que el factorial de es , y que el factorial de cualquier otro valor, , es
0 1 n n
Ahora añadamos la primera parte de la definición – si resulta que el argumento es 0 , todo lo que
tenemos que hacer es devolver 1 :
def factorial(n):
if n == 0:
return 1
else:
return 0
Ahora completemos la segunda parte – si n no es 0 , tenemos que hacer una llamada recursiva
para encontrar el factorial de n-1 y luego multiplicar el resultado por n :
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
return n * recurse
ns = 3, 2, 1
recurses = 2, 1, 1
results = 6, 2, 1
binding1 = make_binding('n', 0)
frame = Frame([binding1], name='factorial', value=1,
shim=1.2, loc='left', dx=1.4)
[Link](frame)
Los valores de retorno se muestran pasando de vuelta hacia arriba por la pila. En cada marco, el
valor de retorno es el producto de n y recurse .
En el último marco, la variable local recurse no existe porque la rama que la crea no se ejecuta.
6.7. Salto de fe
Seguir el flujo de ejecución es una forma de leer programas, pero puede volverse abrumadora
rápidamente. Una alternativa es lo que llamo el “salto de fe”. Cuando llegas a una llamada a una
función, en lugar de seguir el flujo de ejecución, asumes que la función funciona correctamente y
devuelve el resultado adecuado.
De hecho, ya estás practicando este salto de fe cuando usas funciones integradas. Cuando llamas
a abs o [Link] , no examinas los cuerpos de esas funciones – simplemente asumes que
funcionan.
Lo mismo ocurre cuando llamas a una de tus propias funciones. Por ejemplo, antes escribimos una
función llamada is_divisible que determina si un número es divisible por otro. Una vez que nos
convencemos de que esta función es correcta, podemos usarla sin volver a mirar el cuerpo.
Lo mismo ocurre con los programas recursivos. Cuando llegas a la llamada recursiva, en lugar de
seguir el flujo de ejecución, deberías asumir que la llamada recursiva funciona y luego preguntarte:
“Suponiendo que puedo calcular el factorial de n − 1 , ¿puedo calcular el factorial de ?” La
n
Por supuesto, es un poco extraño asumir que la función funciona correctamente cuando aún no has
terminado de escribirla, ¡pero por eso se llama salto de fe!
6.8. Fibonacci
Después de factorial , el ejemplo más común de función recursiva es fibonacci , que tiene la
siguiente definición:
Math input error
Si intentas seguir aquí el flujo de ejecución, incluso para valores pequeños de , te explota la
n
cabeza. Pero según el salto de fe, si asumes que las dos llamadas recursivas funcionan
correctamente, puedes confiar en que la última sentencia return es correcta.
Como nota aparte, esta forma de calcular números de Fibonacci es muy ineficiente. En el Capítulo
10 explicaré por qué y sugeriré una forma de mejorarla.
Parece una recursión infinita. ¿Cómo puede ser? La función tiene casos base cuando n == 1 o n
== 0 . Pero si n no es un entero, podemos saltarnos el caso base y hacer recursión para siempre.
isinstance(1.5, int)
Si superamos ambas comprobaciones, sabemos que n es un entero no negativo, así que podemos
confiar en que la recursión terminará. Comprobar los parámetros de una función para asegurarse
de que tienen los tipos y valores correctos se llama validación de entrada.
6.10. Debugging
Dividir un programa grande en funciones más pequeñas crea puntos de control naturales para
depurar. Si una función no funciona, hay tres posibilidades que considerar:
Hay algo mal con los argumentos que recibe la función – es decir, se viola una precondición.
Hay algo mal con la función – es decir, se viola una postcondición.
Quien llama a la función está haciendo algo mal con el valor de retorno.
Para descartar la primera posibilidad, puedes añadir una sentencia print al principio de la función
que muestre los valores de los parámetros (y quizá sus tipos). O puedes escribir código que
compruebe explícitamente las precondiciones.
Si los parámetros parecen correctos, puedes añadir una sentencia print antes de cada sentencia
return y mostrar el valor de retorno. Si es posible, llama a la función con argumentos que faciliten
comprobar el resultado.
Si la función parece funcionar, mira la llamada a la función para asegurarte de que el valor de
retorno se está usando correctamente – ¡o de que se está usando siquiera!
Añadir sentencias print al principio y al final de una función puede ayudar a hacer más visible el
flujo de ejecución. Por ejemplo, aquí tienes una versión de factorial con sentencias print:
def factorial(n):
space = ' ' * (4 * n)
print(space, 'factorial', n)
if n == 0:
print(space, 'returning 1')
return 1
else:
recurse = factorial(n-1)
result = n * recurse
print(space, 'returning', result)
return result
space es una cadena de caracteres de espacio que controla la indentación de la salida. Este es el
resultado de factorial(3) :
factorial(3)
Si tienes dudas sobre el flujo de ejecución, este tipo de salida puede ser útil. Lleva tiempo
desarrollar andamiaje eficaz, pero un poco de andamiaje puede ahorrar mucho debugging.
6.11. Glosario
valor de retorno: El resultado de una función. Si una llamada a una función se usa como expresión,
el valor de retorno es el valor de la expresión.
función pura: Una función que no muestra nada ni tiene ningún otro efecto, aparte de devolver un
valor de retorno.
código muerto: Parte de un programa que nunca puede ejecutarse, a menudo porque aparece
después de una sentencia return .
desarrollo incremental: Un plan de desarrollo de programas pensado para evitar la depuración
añadiendo y probando solo una pequeña cantidad de código cada vez.
andamiaje: Código que se usa durante el desarrollo del programa pero que no forma parte de la
versión final.
Turing completo: Un lenguaje, o un subconjunto de un lenguaje, es Turing completo si puede
realizar cualquier cálculo que pueda describirse mediante un algoritmo.
validación de entrada: Comprobar los parámetros de una función para asegurarse de que tienen
los tipos y valores correctos
6.12. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
Pregunta a un asistente virtual qué está mal en cada una de estas funciones y comprueba si puede
detectar los errores o mejorar el estilo.
Luego pregunta: “Escribe una función que tome las coordenadas de dos puntos y calcule la
distancia entre ellos.” Comprueba si el resultado se parece a la versión de distance que
escribimos en este capítulo.
6.12.2. Ejercicio
Usa desarrollo incremental para escribir una función llamada hypot que devuelva la longitud de la
hipotenusa de un triángulo rectángulo dadas las longitudes de los otros dos catetos como
argumentos.
Nota: Hay una función en el módulo math llamada hypot que hace lo mismo, ¡pero no deberías
usarla para este ejercicio!
Aunque puedas escribir la función correctamente al primer intento, empieza con una función que
siempre devuelva 0 y practica haciendo cambios pequeños, probando a medida que avanzas.
Cuando termines, la función solo debería devolver un valor – no debería mostrar nada.
6.12.3. Ejercicio
Escribe una función booleana, is_between(x, y, z) , que devuelva True si x < y < z o si
z < y < x , y False en caso contrario.
Puedes usar estos ejemplos para probar tu función.
is_between(1, 2, 3) # should be True
6.12.4. Ejercicio
La función de Ackermann, A(m, n), se define así:
Math input error
Escribe una función llamada ackermann que evalúe la función de Ackermann. ¿Qué ocurre si
llamas a ackermann(5, 5) ?
Puedes usar estos ejemplos para probar tu función.
ackermann(3, 2) # should be 29
ackermann(3, 3) # should be 61
Para ver por qué, añade una sentencia print al principio de la función para mostrar los valores de
los parámetros, y luego ejecuta los ejemplos de nuevo.
6.12.5. Ejercicio
Un número, , es una potencia de si es divisible por y es una potencia de . Escribe una
a b b a/b b
función llamada is_power que tome los parámetros a y b , y devuelva True si a es una
potencia de b . Nota: tendrás que pensar en el caso base.
Puedes usar estos ejemplos para probar tu función.
is_power(65536, 2) # should be True
6.12.6. Ejercicio
El máximo común divisor (MCD) de y es el número más grande que divide a ambos sin dejar
a b
resto.
Una forma de encontrar el MCD de dos números se basa en la observación de que si es el resto
r
gcd(a, 0) = a .
Escribe una función llamada gcd que tome los parámetros a y b , y devuelva su máximo común
divisor.
Puedes usar estos ejemplos para probar tu función.
gcd(12, 8) # should be 4
7. Iteración y búsqueda
En 1939, Ernest Vincent Wright publicó una novela de 50.000 palabras llamada Gadsby que no
contiene la letra “e”. Como “e” es la letra más común en inglés, escribir incluso unas pocas palabras
sin usarla es difícil. Para hacernos una idea de lo difícil que es, en este capítulo calcularemos la
fracción de palabras en inglés que tienen al menos una “e”.
Para ello, usaremos sentencias for para recorrer las letras de una cadena y las palabras de un
archivo, y actualizaremos variables en un bucle para contar el número de palabras que contienen
una “e”. Usaremos el operador in para comprobar si una letra aparece en una palabra, y
aprenderás un patrón de programación llamado “búsqueda lineal”.
Como ejercicio, usarás estas herramientas para resolver un juego de palabras llamado “Spelling
Bee”.
Esta versión usa el argumento de palabra clave end para que la función print añada un espacio
después de cada número en lugar de un salto de línea.
También podemos usar un bucle for para mostrar las letras de una cadena.
for letter in 'Gadsby':
print(letter, end=' ')
has_e('Emma')
El objeto de archivo proporciona un método llamado readline , que lee caracteres del archivo
hasta llegar a un salto de línea y devuelve el resultado como una cadena:
file_object.readline()
Fíjate en que la sintaxis para llamar a readline es diferente de la de las funciones que hemos
visto hasta ahora. Eso se debe a que es un método, que es una función asociada a un objeto. En
este caso readline está asociado al objeto de archivo, así que lo llamamos usando el nombre del
objeto, el operador punto y el nombre del método.
La primera palabra de la lista es “aa”, que es un tipo de lava. La secuencia representa el carácter
de salto de línea que separa esta palabra de la siguiente.
El objeto de archivo lleva la cuenta de en qué parte del archivo está, así que si llamas a readline
de nuevo, obtienes la siguiente palabra:
line = file_object.readline()
line
Para eliminar el salto de línea del final de la palabra, podemos usar strip , que es un método
asociado con las cadenas, así que podemos llamarlo así.
word = [Link]()
word
strip elimina caracteres de espacio en blanco – incluidos espacios, tabulaciones y saltos de línea
– del principio y el final de la cadena.
También puedes usar un objeto de archivo como parte de un bucle for . Este programa lee
[Link] e imprime cada palabra, una por línea:
for line in open('[Link]'):
word = [Link]()
print(word)
Ahora que podemos leer la lista de palabras, el siguiente paso es contarlas. Para eso,
necesitaremos la capacidad de actualizar variables.
Un tipo común de asignación es una actualización, donde el nuevo valor de la variable depende
del anterior.
x = 7
x = x + 1
x
Esta sentencia significa “obtén el valor actual de x , suma uno y asigna el resultado de nuevo a x .”
Si intentas actualizar una variable que no existe, obtienes un error, porque Python evalúa la
expresión de la derecha antes de asignar un valor a la variable de la izquierda.
z = z + 1
Antes de poder actualizar una variable, tienes que inicializarla, normalmente con una asignación
sencilla:
z = 0
z = z + 1
z
Aumentar el valor de una variable se llama incremento; disminuir el valor se llama decremento.
Como estas operaciones son tan comunes, Python proporciona operadores de asignación
aumentada que actualizan una variable de forma más concisa. Por ejemplo, el operador +=
incrementa una variable en la cantidad dada.
z += 2
z
Hay operadores de asignación aumentada para los otros operadores aritméticos, incluidos -= y
*= .
7.4. Recorrer y contar
El siguiente programa cuenta el número de palabras en la lista de palabras.
total = 0
Empieza inicializando total a 0 . Cada vez que pasa por el bucle, incrementa total en 1 . Así
que cuando el bucle termina, total se refiere al número total de palabras.
total
Una variable como esta, usada para contar el número de veces que ocurre algo, se llama contador.
Podemos añadir un segundo contador al programa para llevar la cuenta del número de palabras
que contienen una “e”.
total = 0
count = 0
Como porcentaje de total , alrededor de dos tercios de las palabras usan la letra “e”.
count / total * 100
Así puedes entender por qué es difícil escribir un libro sin usar ninguna de esas palabras.
7.5. El operador in
La versión de has_e que escribimos en este capítulo es más complicada de lo necesario. Python
proporciona un operador, in , que comprueba si un carácter aparece en una cadena.
word = 'Gadsby'
'e' in word
def has_e(word):
return 'E' in word or 'e' in word
Podemos simplificar esta función todavía más usando el método lower , que convierte las letras
de una cadena a minúsculas. Aquí tienes un ejemplo.
[Link]()
lower crea una cadena nueva – no modifica la cadena existente – así que el valor de word no
cambia.
word
has_e('Emma')
7.6. Búsqueda
Basándonos en esta versión más sencilla de has_e , escribamos una función más general llamada
uses_any que recibe un segundo parámetro que es una cadena de letras. Devuelve True si la
palabra usa cualquiera de las letras y False en caso contrario.
def uses_any(word, letters):
for letter in [Link]():
if letter in [Link]():
return True
return False
uses_any convierte word y letters a minúsculas, así que funciona con cualquier combinación
de mayúsculas y minúsculas.
uses_any('Banana', 'AEIOU')
La estructura de uses_any es similar a la de has_e . Recorre las letras de word y las comprueba
una por una. Si encuentra una que aparece en letters , devuelve True inmediatamente. Si llega
hasta el final del bucle sin encontrar ninguna, devuelve False .
Este patrón se llama búsqueda lineal. En los ejercicios al final de este capítulo, escribirás más
funciones que usan este patrón.
7.7. Doctest
En el capítulo 4 usamos un docstring para documentar una función – es decir, para explicar qué
hace. También es posible usar un docstring para probar una función. Aquí tienes una versión de
uses_any con un docstring que incluye pruebas.
Cada prueba empieza con >>> , que se usa como indicador en algunos entornos de Python para
indicar dónde el usuario puede escribir código. En un doctest, al indicador le sigue una expresión,
normalmente una llamada a función. La línea siguiente indica el valor que debería tener la expresión
si la función funciona correctamente.
En el primer ejemplo, 'banana' usa 'a' , así que el resultado debería ser True . En el segundo
ejemplo, 'apple' no usa ninguna de 'xyz' , así que el resultado debería ser False .
Para ejecutar estas pruebas, tenemos que importar el módulo doctest y ejecutar una función
llamada run_docstring_examples . Para que esta función sea más fácil de usar, escribí la siguiente
función, que toma un objeto que representa una función como argumento.
from doctest import run_docstring_examples
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
Todavía no hemos aprendido sobre globals y __name__ – puedes ignorarlos. Ahora podemos
probar uses_any así.
run_doctests(uses_any)
La salida incluye el ejemplo que falló, el valor que se esperaba que devolviera la función y el valor
que produjo realmente.
Si no tienes claro por qué falló esta prueba, tendrás la oportunidad de depurarla como ejercicio.
7.8. Glosario
variable de bucle: Una variable definida en la cabecera de un bucle for .
objeto de archivo: Un objeto que representa un archivo abierto y lleva la cuenta de qué partes del
archivo se han leído o escrito.
método: Una función asociada a un objeto que se llama usando el operador punto.
actualización: Una sentencia de asignación que da un valor nuevo a una variable que ya existe, en
lugar de crear una variable nueva.
inicializar: Crear una variable nueva y darle un valor.
incremento: Aumentar el valor de una variable.
decremento: Disminuir el valor de una variable.
contador: Una variable usada para contar algo, normalmente inicializada a cero y luego
incrementada.
búsqueda lineal: Un patrón computacional que busca en una secuencia de elementos y se detiene
cuando encuentra lo que está buscando.
pasar: Si una prueba se ejecuta y el resultado es el esperado, la prueba pasa.
fallar: Si una prueba se ejecuta y el resultado no es el esperado, la prueba falla.
7.9. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
7.9.2. Ejercicio
Escribe una función llamada uses_none que reciba una palabra y una cadena de letras prohibidas,
y devuelva True si la palabra no usa ninguna de las letras prohibidas.
Aquí tienes un esquema de la función que incluye dos doctests. Completa la función para que pase
estas pruebas, y añade al menos un doctest más.
def uses_none(word, forbidden):
"""Checks whether a word avoid forbidden letters.
run_doctests(uses_none)
7.9.3. Ejercicio
Escribe una función llamada uses_only que reciba una palabra y una cadena de letras, y que
devuelva True si la palabra contiene solo letras de la cadena.
Aquí tienes un esquema de la función que incluye dos doctests. Completa la función para que pase
estas pruebas, y añade al menos un doctest más.
def uses_only(word, available):
"""Checks whether a word uses only the available letters.
run_doctests(uses_only)
7.9.4. Ejercicio
Escribe una función llamada uses_all que reciba una palabra y una cadena de letras, y que
devuelva True si la palabra contiene todas las letras de la cadena al menos una vez.
Aquí tienes un esquema de la función que incluye dos doctests. Completa la función para que pase
estas pruebas, y añade al menos un doctest más.
def uses_all(word, required):
"""Checks whether a word uses all required letters.
run_doctests(uses_all)
7.9.5. Ejercicio
The New York Times publica un juego diario llamado “Spelling Bee” que reta a los lectores a formar
tantas palabras como sea posible usando solo siete letras, donde una de las letras es obligatoria.
Las palabras deben tener al menos cuatro letras.
Por ejemplo, el día que escribí esto, las letras eran ACDLORT , con R como letra obligatoria. Así que
“color” es una palabra aceptable, pero “told” no, porque no usa R , y “rat” tampoco porque solo
tiene tres letras. Las letras se pueden repetir, así que “ratatat” es aceptable.
Escribe una función llamada check_word que compruebe si una palabra dada es aceptable. Debe
recibir como parámetros la palabra que se va a comprobar, una cadena de siete letras disponibles y
una cadena que contiene la única letra obligatoria. Puedes usar las funciones que escribiste en
ejercicios anteriores.
Aquí tienes un esquema de la función que incluye doctests. Completa la función y luego
comprueba que todas las pruebas pasan.
def check_word(word, available, required):
"""Check whether a word is acceptable.
run_doctests(check_word)
run_doctests(word_score)
Cuando todas tus funciones pasen sus pruebas, usa el siguiente bucle para buscar palabras
aceptables en la lista de palabras y sumar sus puntuaciones.
available = 'ACDLORT'
required = 'R'
total = 0
file_object = open('[Link]')
for line in file_object:
word = [Link]()
if check_word(word, available, required):
score = word_score(word, available)
total = total + score
print(word, score)
7.9.6. Ejercicio
Quizá hayas notado que las funciones que escribiste en los ejercicios anteriores tenían mucho en
común. De hecho, son tan parecidas que a menudo puedes usar una función para escribir otra.
Por ejemplo, si una palabra no usa ninguna letra de un conjunto de letras prohibidas, eso significa
que no usa ninguna. Así que podemos escribir una versión de uses_none así.
def uses_none(word, forbidden):
"""Checks whether a word avoids forbidden letters.
run_doctests(uses_none)
También hay una similitud entre uses_only y uses_all que puedes aprovechar. Si tienes una
versión funcional de uses_only , mira si puedes escribir una versión de uses_all que llame a
uses_only .
7.9.7. Ejercicio
Si te atascaste en la pregunta anterior, prueba a preguntar a un asistente virtual: “Dada una
función, uses_only , que recibe dos cadenas y comprueba que la primera usa solo las letras del
segundo, úsala para escribir uses_all , que recibe dos cadenas y comprueba si la primera usa
todas las letras del segundo, permitiendo repeticiones.”
Usa run_doctests para comprobar la respuesta.
run_doctests(uses_all)
7.9.8. Ejercicio
Ahora veamos si podemos escribir uses_all basándonos en uses_any .
Pregunta a un asistente virtual: “Dada una función, uses_any , que recibe dos cadenas y
comprueba si la primera usa cualquiera de las letras del segundo, ¿puedes usarla para escribir
uses_all , que recibe dos cadenas y comprueba si la primera usa todas las letras del segundo,
permitiendo repeticiones?”
Si dice que puede, ¡asegúrate de probar el resultado!
# Here's what I got from ChatGPT 4o December 26, 2024
# It's correct, but it makes multiple calls to uses_any
La expresión entre corchetes es un índice, llamado así porque indica qué carácter de la secuencia
seleccionar. Pero el resultado quizá no sea lo que esperas.
letter
Pero el valor del índice tiene que ser un entero – de lo contrario obtienes un TypeError .
fruit[1.5]
Como vimos en el capítulo 1, podemos usar la función integrada len para obtener la longitud de
una cadena.
n = len(fruit)
n
Para obtener la última letra de una cadena, podrías sentir la tentación de escribir esto:
fruit[n]
Pero eso causa un IndexError porque no hay ninguna letra en 'banana' con el índice 6. Como
empezamos a contar en 0 , las seis letras están numeradas de 0 a 5 . Para obtener el último
carácter, tienes que restar 1 a n :
fruit[n-1]
Pero hay una forma más sencilla. Para obtener la última letra de una cadena, puedes usar un índice
negativo, que cuenta hacia atrás desde el final.
fruit[-1]
El operador [n:m] devuelve la parte de la cadena desde el carácter n -ésimo hasta el carácter
m -ésimo, incluyendo el primero pero excluyendo el segundo. Este comportamiento es
contraintuitivo, pero puede ayudar imaginar que los índices apuntan entre los caracteres, como en
esta figura:
from diagram import make_binding, Element, Value
ax = diagram(width, height)
bbox = [Link](ax, x, y)
bboxes = [bbox]
x1 = ([Link] + [Link]) / 2
y1 = [Link] + 0.02
y2 = y1 + 0.14
handle = [Link]([x1, x1], [y1, y2], ':', lw=0.5, color='gray')
x += 0.105
Por ejemplo, el segmento [3:6] selecciona las letras ana , lo que significa que 6 es legal como
parte de un segmento, pero no como índice.
Si omites el primer índice, el segmento empieza al principio de la cadena.
fruit[:3]
Si el primer índice es mayor o igual que el segundo, el resultado es una cadena vacía,
representado por dos comillas:
fruit[3:3]
Este ejemplo concatena una nueva primera letra con un segmento de greeting . No tiene ningún
efecto sobre la cadena original.
greeting
8.4. Comparación de cadenas
Los operadores relacionales funcionan con cadenas. Para ver si dos cadenas son iguales, podemos
usar el operador == .
word = 'banana'
if word == 'banana':
print('All right, banana.')
Otras operaciones relacionales son útiles para poner palabras en orden alfabético:
def compare_word(word):
if word < 'banana':
print(word, 'comes before banana.')
elif word > 'banana':
print(word, 'comes after banana.')
else:
print('All right, banana.')
compare_word('apple')
Python no trata las letras mayúsculas y minúsculas de la misma manera que lo hacen las personas.
Todas las letras mayúsculas van antes que todas las minúsculas, así que:
compare_word('Pineapple')
Para resolver este problema, podemos convertir las cadenas a un formato estándar, como todo en
minúsculas, antes de realizar la comparación. Tenlo en cuenta si tienes que defenderte de un
hombre armado con una piña.
Este uso del operador punto especifica el nombre del método, upper , y el nombre de la cadena al
que aplicar el método, word . Los paréntesis vacíos indican que este método no toma argumentos.
Una llamada a método se llama invocación; en este caso, diríamos que estamos invocando upper
sobre word .
if not [Link]('[Link]'):
!wget [Link]
He descargado el libro en un archivo de texto plano llamado [Link] , que podemos abrir para
lectura así:
reader = open('[Link]')
Además del texto del libro, este archivo contiene una sección al principio con información sobre el
libro y una sección al final con información sobre la licencia. Antes de procesar el texto, podemos
eliminar este material extra encontrando las líneas especiales al principio y al final que empiezan
con '***' .
La siguiente función recibe una línea y comprueba si es una de las líneas especiales. Usa el método
startswith , que comprueba si una cadena empieza con una secuencia determinada de
caracteres.
def is_special_line(line):
return [Link]('*** ')
Podemos usar esta función para recorrer las líneas del archivo e imprimir solo las líneas especiales.
for line in reader:
if is_special_line(line):
print([Link]())
Ahora creemos un archivo nuevo, llamado pg345_cleaned.txt , que contenga solo el texto del
libro. Para recorrer el libro de nuevo, tenemos que abrirlo otra vez para lectura. Y, para escribir un
archivo nuevo, podemos abrirlo para escritura.
reader = open('[Link]')
writer = open('pg345_cleaned.txt', 'w')
open recibe un parámetro opcional que especifica el “modo” – en este ejemplo, 'w' indica que
estamos abriendo el archivo para escritura. Si el archivo no existe, se creará; si ya existe, el
contenido será reemplazado.
Como primer paso, recorreremos el archivo hasta encontrar la primera línea especial.
for line in reader:
if is_special_line(line):
break
La sentencia break “rompe” el bucle – es decir, hace que el bucle termine inmediatamente, antes
de llegar al final del archivo.
Cuando el bucle termina, line contiene la línea especial que hizo que la condición fuera
verdadera.
line
Como reader lleva la cuenta de en qué parte del archivo está, podemos usar un segundo bucle
para continuar donde lo dejamos.
El siguiente bucle lee el resto del archivo, una línea cada vez. Cuando encuentra la línea especial
que indica el final del texto, rompe el bucle. En caso contrario, escribe la línea en el archivo de
salida.
for line in reader:
if is_special_line(line):
break
[Link](line)
En este punto reader y writer siguen abiertos, lo que significa que podríamos seguir leyendo
líneas de reader o escribiendo líneas en writer . Para indicar que hemos terminado, podemos
cerrar ambos archivos invocando el método close .
[Link]()
[Link]()
Para comprobar si este proceso tuvo éxito, podemos leer las primeras líneas del archivo nuevo que
acabamos de crear.
for line in open('pg345_cleaned.txt'):
line = [Link]()
if len(line) > 0:
print(line)
if [Link]('Stoker'):
break
El método endswith comprueba si una cadena termina con una secuencia determinada de
caracteres.
total
Para ver si una línea contiene “Jonathan”, podemos usar el operador in , que comprueba si esta
secuencia de caracteres aparece en cualquier parte de la línea.
total = 0
for line in open('pg345_cleaned.txt'):
if 'Jonathan' in line:
total += 1
total
Hay 199 líneas que contienen el nombre, pero ese no es exactamente el número total de veces que
aparece, porque puede aparecer más de una vez en una línea. Para obtener el total, podemos usar
el método count , que devuelve el número de veces que una secuencia aparece en una cadena.
total = 0
for line in open('pg345_cleaned.txt'):
total += [Link]('Jonathan')
total
total
Si el patrón aparece en el texto, search devuelve un objeto Match que contiene los resultados de
la búsqueda. Entre otra información, tiene una variable llamada string que contiene el texto en el
que se buscó.
[Link]
También proporciona un método llamado group que devuelve la parte del texto que coincidió con
el patrón.
[Link]()
Y proporciona un método llamado span que devuelve el índice en el texto donde empieza y
termina el patrón.
[Link]()
Así que podemos comprobar si la búsqueda tuvo éxito comprobando si el resultado es None .
result == None
Juntando todo eso, aquí tienes una función que recorre las líneas del libro hasta encontrar una que
coincida con el patrón dado, y devuelve el objeto Match .
def find_first(pattern):
for line in open('pg345_cleaned.txt'):
result = [Link](pattern, line)
if result != None:
return result
Para este ejemplo, no tuvimos que usar expresiones regulares – podríamos haber hecho lo mismo
más fácilmente con el operador in . Pero las expresiones regulares pueden hacer cosas que el
operador in no puede.
Por ejemplo, si el patrón incluye el carácter de barra vertical, '|' , puede coincidir con la
secuencia de la izquierda o con la secuencia de la derecha. Supongamos que queremos encontrar
la primera mención de Mina Murray en el libro, pero no estamos seguros de si se la menciona por
su nombre o por su apellido. Podemos usar el siguiente patrón, que coincide con cualquiera de los
dos nombres.
pattern = 'Mina|Murray'
result = find_first(pattern)
[Link]
Podemos usar un patrón como este para ver cuántas veces se menciona a un personaje por
cualquiera de los dos nombres. Aquí tienes una función que recorre el libro y cuenta el número de
líneas que coinciden con el patrón dado.
def count_matches(pattern):
count = 0
for line in open('pg345_cleaned.txt'):
result = [Link](pattern, line)
if result != None:
count += 1
return count
El carácter especial '^' coincide con el principio de una cadena, así que podemos encontrar una
línea que empieza con un patrón dado.
result = find_first('^Dracula')
[Link]
Y el carácter especial '$' coincide con el final de una cadena, así que podemos encontrar una
línea que termina con un patrón dado (ignorando el salto de línea del final).
result = find_first('Harker$')
[Link]
8.9. Sustitución de cadenas
Bram Stoker nació en Irlanda, y cuando Dracula se publicó en 1897, vivía en Inglaterra. Así que
esperaríamos que usara la ortografía británica de palabras como “centre” y “colour”. Para
comprobarlo, podemos usar el siguiente patrón, que coincide con “centre” o con la ortografía
estadounidense “center”.
pattern = 'cent(er|re)'
En este patrón, los paréntesis encierran la parte del patrón a la que se aplica la barra vertical. Así
que este patrón coincide con una secuencia que empieza con 'cent' y termina con 'er' o con
're' .
result = find_first(pattern)
[Link]
Este patrón coincide con “colour” con la 'u' o con “color” sin ella.
result = find_first(pattern)
line = [Link]
line
def all_matches(pattern):
for line in open('pg345_cleaned.txt'):
result = [Link](pattern, line)
if result:
print([Link]())
# Here's the pattern I used (which uses some features we haven't seen)
names = r'(?<!\.\s)[A-Z][a-zA-Z]+'
all_matches(names)
8.10. Depuración
Cuando lees y escribes archivos, depurar puede ser complicado. Si trabajas en un Jupyter
notebook, puedes usar comandos de shell para ayudar. Por ejemplo, para mostrar las primeras
líneas de un archivo, puedes usar el comando !head , así:
!head pg345_cleaned.txt
El signo de exclamación inicial, ! , indica que esto es un comando de shell, que no forma parte de
Python. Para mostrar las últimas líneas, puedes usar !tail .
!tail pg345_cleaned.txt
Cuando trabajas con archivos grandes, depurar puede ser difícil porque puede haber demasiada
salida para revisarla a mano. Una buena estrategia de depuración es empezar con solo una parte
del archivo, hacer que el programa funcione, y luego ejecutarlo con el archivo completo.
Para crear un archivo pequeño que contenga parte de un archivo más grande, podemos usar
!head de nuevo con el operador de redirección, > , que indica que los resultados deben
escribirse en un archivo en lugar de mostrarse.
!head pg345_cleaned.txt > pg345_cleaned_10_lines.txt
Por defecto, !head lee las primeras 10 líneas, pero recibe un argumento opcional que indica el
número de líneas a leer.
!head -100 pg345_cleaned.txt > pg345_cleaned_100_lines.txt
Este comando de shell lee las primeras 100 líneas de pg345_cleaned.txt y las escribe en un
archivo llamado pg345_cleaned_100_lines.txt .
Nota: Los comandos de shell !head y !tail no están disponibles en todos los sistemas
operativos. Si no te funcionan, podemos escribir funciones similares en Python. Consulta el primer
ejercicio al final de este capítulo para ver sugerencias.
8.11. Glosario
secuencia: Una colección ordenada de valores donde cada valor se identifica mediante un índice
entero.
carácter: Un elemento de una cadena, incluidas letras, números y símbolos.
índice: Un valor entero usado para seleccionar un elemento en una secuencia, como un carácter
en una cadena. En Python los índices empiezan desde 0 .
segmento: Una parte de una cadena especificada por un rango de índices.
cadena vacía: Una cadena que no contiene caracteres y tiene longitud 0 .
objeto: Algo a lo que una variable puede referirse. Un objeto tiene un tipo y un valor.
inmutable: Si los elementos de un objeto no se pueden cambiar, el objeto es inmutable.
invocación: Una expresión – o parte de una expresión – que llama a un método.
expresión regular: Una secuencia de caracteres que define un patrón de búsqueda.
patrón: Una regla que especifica los requisitos que una cadena debe cumplir para constituir una
coincidencia.
sustitución de cadenas: Reemplazo de una cadena, o parte de una cadena, por otra cadena.
comando de shell: Una sentencia en un lenguaje de shell, que es un lenguaje usado para
interactuar con un sistema operativo.
8.12. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
download('[Link]
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
8.12.2. Ejercicio
Mira si puedes escribir una función que haga lo mismo que el comando de shell !head . Debe
recibir como argumentos el nombre de un archivo que leer, el número de líneas que leer y el
nombre del archivo donde escribir las líneas. Si el tercer parámetro es None , debe mostrar las
líneas en lugar de escribirlas en un archivo.
Considera pedir ayuda a un asistente virtual, pero si lo haces, dile que no use una sentencia with
ni una sentencia try .
Puedes usar los siguientes ejemplos para probar tu función.
head('pg345_cleaned.txt', 10)
!tail pg345_cleaned_100_lines.txt
8.12.3. Ejercicio
“Wordle” es un juego de palabras online donde el objetivo es adivinar una palabra de cinco letras
en seis intentos o menos. Cada intento tiene que ser reconocido como una palabra, sin incluir
nombres propios. Después de cada intento, obtienes información sobre cuáles de las letras que
adivinaste aparecen en la palabra objetivo, y cuáles están en la posición correcta.
Por ejemplo, supongamos que la palabra objetivo es MOWER y que intentas TRIED . Aprenderías
que E está en la palabra y en la posición correcta, R está en la palabra pero no en la posición
correcta, y T , I y D no están en la palabra.
Como ejemplo distinto, supongamos que has intentado las palabras SPADE y CLERK , y has
aprendido que E está en la palabra, pero no en ninguna de esas posiciones, y que ninguna de las
otras letras aparece en la palabra. De las palabras de la lista, ¿cuántas podrían ser la palabra
objetivo? Escribe una función llamada check_word que reciba una palabra de cinco letras y
compruebe si podría ser la palabra objetivo, dadas estas conjeturas.
Puedes usar cualquiera de las funciones del capítulo anterior, como uses_any .
def uses_any(word, letters):
for letter in [Link]():
if letter in [Link]():
return True
return False
8.12.4. Ejercicio
Continuando con el ejercicio anterior, supongamos que intentas la palabra TOTEM y aprendes que
la E todavía no está en el lugar correcto, pero la M sí. ¿Cuántas palabras quedan?
8.12.5. Ejercicio
The Count of Monte Cristo es una novela de Alexandre Dumas que se considera un clásico. Sin
embargo, en la introducción de una traducción inglesa del libro, el escritor Umberto Eco confiesa
que le pareció “una de las novelas peor escritas de todos los tiempos”.
En particular, dice que es “desvergonzada en su repetición del mismo adjetivo”, y menciona en
particular el número de veces que “sus personajes se estremecen o palidecen”.
Para ver si su objeción es válida, contemos el número de líneas que contienen la palabra pale en
cualquier forma, incluidas pale , pales , paled y paleness , así como la palabra relacionada
pallor . Usa una sola expresión regular que coincida con cualquiera de estas palabras. Como
desafío adicional, asegúrate de que no coincida con otras palabras, como impale – quizá quieras
pedir ayuda a un asistente virtual.
La siguiente celda descarga el libro de Project Gutenberg [Link]
import os
if not [Link]('[Link]'):
!wget [Link]
La siguiente celda ejecuta una función que lee el archivo de Project Gutenberg y escribe un archivo
que contiene solo el texto del libro, no la información añadida sobre el libro.
def clean_file(input_file, output_file):
reader = open(input_file)
writer = open(output_file, 'w')
[Link]()
[Link]()
clean_file('[Link]', 'pg1184_cleaned.txt')
Según este recuento, estas palabras aparecen en 223 líneas del libro, así que el señor Eco quizá
tenga razón.
Think Python: 3rd Edition
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Licencia del código: MIT License
Licencia del texto: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
9. Listas
Este capítulo presenta uno de los tipos integrados más útiles de Python: las listas. También
aprenderás más sobre objetos y lo que puede ocurrir cuando varias variables se refieren al mismo
objeto.
En los ejercicios al final del capítulo, construiremos una lista de palabras y la usaremos para buscar
palabras especiales como palíndromos y anagramas.
Los elementos de una lista no tienen que ser del mismo tipo. La siguiente lista contiene un string,
un float, un entero e incluso otra lista.
t = ['spam', 2.0, 5, [10, 20]]
empty = []
Las listas se representan con cajas con la palabra “lista” fuera y los elementos numerados de la
lista dentro.
9.2. Las listas son mutables
Para leer un elemento de una lista, podemos usar el operador de corchetes. El índice del primer
elemento es 0 .
cheeses[0]
A diferencia de los strings, las listas son mutables. Cuando el operador de corchetes aparece en el
lado izquierdo de una asignación, identifica el elemento de la lista al que se asignará el valor.
numbers[1] = 17
numbers
'Wensleydale' in cheeses
Aunque una lista puede contener otra lista, la lista anidada sigue contando como un solo elemento;
por eso, en la siguiente lista, solo hay cuatro elementos.
t = ['spam', 2.0, 5, [10, 20]]
len(t)
Como list es el nombre de una función integrada, deberías evitar usarlo como nombre de
variable.
Ningún otro operador matemático funciona con listas, pero la función integrada sum suma los
elementos.
sum(t1)
max(t2)
extend toma una lista como argumento y añade todos sus elementos:
[Link](['f', 'g'])
letters
Hay dos métodos que eliminan elementos de una lista. Si conoces el índice del elemento que
quieres, puedes usar pop .
t = ['a', 'b', 'c']
[Link](1)
Si conoces el elemento que quieres eliminar (pero no el índice), puedes usar remove :
t = ['a', 'b', 'c']
[Link]('b')
El valor de retorno de remove es None . Pero podemos confirmar que la lista se ha modificado.
t
La función list divide un string en letras individuales. Si quieres dividir un string en palabras,
puedes usar el método split :
s = 'pining for the fjords'
t = [Link]()
t
Un argumento opcional llamado delimitador especifica qué caracteres se usan como límites entre
palabras. El siguiente ejemplo usa un guion como delimitador.
s = 'ex-parrot'
t = [Link]('-')
t
Si tienes una lista de strings, puedes concatenarlos en un solo string usando join . join es un
método de string, así que tienes que invocarlo sobre el delimitador y pasar la lista como argumento.
delimiter = ' '
t = ['pining', 'for', 'the', 'fjords']
s = [Link](t)
s
En este caso el delimitador es un carácter de espacio, así que join pone un espacio entre las
palabras. Para unir strings sin espacios, puedes usar el string vacío, '' , como delimitador.
Por ejemplo, después de usar split para crear una lista de palabras, podemos usar for para
recorrerlas.
s = 'pining for the fjords'
sorted funciona con cualquier tipo de secuencia, no solo con listas. Así que podemos ordenar las
letras de un string así.
sorted('letters')
El resultado es una lista. Para convertir la lista en un string, podemos usar join .
''.join(sorted('letters'))
Con un string vacío como delimitador, los elementos de la lista se unen sin nada entre ellos.
s = 'banana'
bindings = [Binding(Value(name), Value(repr(s))) for name in 'ab']
frame1 = Frame(bindings, dy=-0.25)
En este ejemplo, Python solo creó un objeto string, y tanto a como b se refieren a él. Pero
cuando creas dos listas, obtienes dos objetos.
a = [1, 2, 3]
b = [1, 2, 3]
a is b
En este caso diríamos que las dos listas son equivalentes, porque tienen los mismos elementos,
pero no idénticos, porque no son el mismo objeto. Si dos objetos son idénticos, también son
equivalentes, pero si son equivalentes, no necesariamente son idénticos.
9.10. Aliasing
Si a se refiere a un objeto y asignas b = a , entonces ambas variables se refieren al mismo
objeto.
a = [1, 2, 3]
b = a
b is a
La asociación de una variable con un objeto se llama referencia. En este ejemplo, hay dos
referencias al mismo objeto.
Un objeto con más de una referencia tiene más de un nombre, así que decimos que el objeto está
aliased. Si el aliased objeto es mutable, los cambios hechos con un nombre afectan al otro. En este
ejemplo, si cambiamos el objeto al que se refiere b , también estamos cambiando el objeto al que
se refiere a .
b[0] = 5
a
Así que diríamos que a “ve” este cambio. Aunque este comportamiento puede ser útil, es
propenso a errores. En general, es más seguro evitar el aliasing cuando trabajas con objetos
mutables.
Para objetos inmutables como strings, el aliasing no supone tanto problema. En este ejemplo:
a = 'banana'
b = 'banana'
def pop_first(lst):
return [Link](0)
El valor de retorno es el primer elemento, que se ha eliminado de la lista, como podemos ver al
mostrar la lista modificada.
letters
En este ejemplo, el parameter lst y la variable letters son aliases del mismo objeto, así que el
diagrama de estado se ve así:
lst = make_list('abc', dy=-0.3, offsetx=0.1)
binding1 = Binding(Value('letters'), draw_value=False)
frame1 = Frame([binding1], name='__main__', loc='left')
Pasar una referencia a un objeto como argumento a una función crea una forma de aliasing. Si la
función modifica el objeto, esos cambios persisten después de que la función termine.
word_list = []
len(word_list)
Antes del bucle, word_list se inicializa con una lista vacía. Cada vez que pasa por el bucle, el
método append añade una palabra al final. Cuando termina el bucle, hay más de 113 000 palabras
en la lista.
Otra forma de hacer lo mismo es usar read para leer todo el archivo en un string.
string = open('[Link]').read()
len(string)
El resultado es un único string con más de un millón de caracteres. Podemos usar el método
split para dividirlo en una lista de palabras.
word_list = [Link]()
len(word_list)
Ahora, para comprobar si un string aparece en la lista, podemos usar el operador in . Por ejemplo,
'demotic' está en la lista.
'demotic' in word_list
9.13. Depuración
Ten en cuenta que la mayoría de métodos de lista modifican el argumento y devuelven None . Esto
es lo contrario de los métodos de string, que devuelven un nuevo string y dejan el original intacto.
Si estás acostumbrado a escribir código con strings así:
word = 'plumage!'
word = [Link]('!')
word
remove modifica la lista y devuelve None , así que la siguiente operación que realices con t
probablemente fallará.
[Link](2)
Este mensaje de error requiere algo de explicación. Un atributo de un objeto es una variable o
método asociado con él. En este caso, el valor de t es None , que es un objeto NoneType , que no
tiene un atributo llamado remove , así que el resultado es un AttributeError .
Si ves un mensaje de error como este, deberías mirar hacia atrás en el programa y ver si podrías
haber llamado incorrectamente a un método de lista.
9.14. Glosario
lista: Un objeto que contiene una secuencia de valores.
elemento: Uno de los valores de una lista u otra secuencia.
lista anidada: Una lista que es un elemento de otra lista.
delimitador: Un carácter o string usado para indicar dónde debe dividirse un string.
equivalentes: Tener el mismo valor.
idénticos: Ser el mismo objeto (lo que implica equivalencia).
referencia: La asociación entre una variable y su valor.
aliased: Si hay más de una variable que se refiere a un objeto, el objeto está aliased.
atributo: Uno de los valores con nombre asociados con un objeto.
9.15. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
9.15.2. Ejercicio
Dos palabras son anagramas si puedes reordenar las letras de una para escribir la otra. Por
ejemplo, tops es un anagrama de stop .
Una forma de comprobar si dos palabras son anagramas es ordenar las letras de ambas palabras.
Si las listas de letras ordenadas son iguales, las palabras son anagramas.
Escribe una función llamada is_anagram que tome dos strings y devuelva True si son
anagramas.
Para empezar, aquí tienes un esquema de la función con doctests.
def is_anagram(word1, word2):
"""Checks whether two words are anagrams.
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(is_anagram)
9.15.3. Ejercicio
Python proporciona una función integrada llamada reversed que toma como argumento una
secuencia de elementos, como una lista o un string, y devuelve un objeto reversed que contiene
los elementos en orden inverso.
reversed('parrot')
Si quieres los elementos invertidos en una lista, puedes usar la función list .
list(reversed('parrot'))
Así podemos escribir una función que invierte una palabra así.
def reverse_word(word):
return ''.join(reversed(word))
Un palíndromo es una palabra que se escribe igual hacia atrás y hacia adelante, como “noon” y
“rotator”. Escribe una función llamada is_palindrome que tome un string argumento y devuelva
True si es un palíndromo y False en caso contrario.
Aquí tienes un esquema de la función con doctests que puedes usar para comprobar tu función.
def is_palindrome(word):
"""Check if a word is a palindrome.
>>> is_palindrome('bob')
True
>>> is_palindrome('alice')
False
>>> is_palindrome('a')
True
>>> is_palindrome('')
True
"""
return False
run_doctests(is_palindrome)
Puedes usar el siguiente bucle para encontrar todos los palíndromos en la lista de palabras con al
menos 7 letras.
for word in word_list:
if len(word) >= 7 and is_palindrome(word):
print(word)
9.15.4. Ejercicio
Escribe una función llamada reverse_sentence que tome como argumento un string que contiene
cualquier número de palabras separadas por espacios. Debe devolver un nuevo string que contiene
las mismas palabras en orden inverso. Por ejemplo, si el argumento es “Reverse this sentence”, el
resultado debería ser “Sentence this reverse”.
Pista: Puedes usar los métodos capitalize para poner en mayúscula la primera palabra y
convertir las demás palabras a minúsculas.
Para empezar, aquí tienes un esquema de la función con doctests.
def reverse_sentence(input_string):
'''Reverse the words in a string and capitalize the first.
>>> reverse_sentence('Python')
'Python'
>>> reverse_sentence('')
''
run_doctests(reverse_sentence)
9.15.5. Ejercicio
Escribe una función llamada total_length que tome una lista de strings y devuelva la longitud
total de los strings. La longitud total de las palabras en word_list debería ser
902,728 .
Think Python: 3rd Edition
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Código license: MIT License
Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
10. Diccionarios
Este capítulo presenta un tipo integrado llamado diccionario. Es una de las mejores características
de Python, y el bloque de construcción de muchos algoritmos eficientes y elegantes.
Usaremos diccionarios para calcular el número de palabras únicas en un libro y el número de veces
que aparece cada una. Y en los ejercicios, usaremos diccionarios para resolver juegos de palabras.
Pero supongamos que queremos ir en la otra dirección, y buscar una palabra para obtener el entero
correspondiente. No podemos hacerlo con una lista, pero sí con un diccionario. Empezaremos
creando un diccionario vacío y asignándolo a numbers .
numbers = {}
numbers
Las llaves, {} , representan un diccionario vacío. Para añadir elementos al diccionario, usaremos
corchetes.
numbers['zero'] = 0
Esta asignación añade al diccionario un elemento, que representa la asociación de una clave y un
valor. En este ejemplo, la clave es el string 'zero' y el valor es el entero 0 . Si mostramos el
diccionario, vemos que contiene un elemento, que contiene una clave y un valor separados por dos
puntos, : .
numbers
Un diccionario se representa con una caja con la palabra “dict” fuera y los elementos dentro. Cada
elemento se representa con una clave y una flecha que apunta a un valor. Las comillas indican que
las claves aquí son strings, no nombres de variables.
Cada elemento consiste en una clave y un valor separados por dos puntos. Los elementos se
separan con comas y se encierran entre llaves.
Otra forma de crear un diccionario es usar la función dict . Podemos crear un diccionario vacío
así.
empty = dict()
empty
Y podemos hacer una copia de un diccionario así.
numbers_copy = dict(numbers)
numbers_copy
A menudo es útil hacer una copia antes de realizar operaciones que modifican diccionarios.
10.3. El operador in
El operador in también funciona con diccionarios; te dice si algo aparece como clave en el
diccionario.
'one' in numbers
Para ver si algo aparece como valor en un diccionario, puedes usar el método values , que
devuelve una secuencia de valores, y luego usar el operador in .
1 in [Link]()
Los elementos de un diccionario de Python se almacenan en una hash table, que es una forma de
organizar datos con una propiedad notable: el operador in tarda aproximadamente la misma
cantidad de tiempo sin importar cuántos elementos haya en el diccionario. Eso hace posible
escribir algunos algoritmos notablemente eficientes.
download('[Link]
Para demostrarlo, compararemos dos algoritmos para encontrar pares de palabras donde una es la
inversa de la otra, como stressed y desserts . Empezaremos leyendo la lista de palabras.
word_list = open('[Link]').read().split()
len(word_list)
La siguiente función recorre las palabras de la lista. Para cada una, invierte las letras y luego
comprueba si la palabra invertida está en la palabra lista.
def too_slow():
count = 0
for word in word_list:
if reverse_word(word) in word_list:
count += 1
return count
Esta función tarda más de un minuto en ejecutarse. El problema es que el operador in comprueba
las palabras de la lista una por una, empezando por el principio. Si no encuentra lo que busca, que
ocurre la mayor parte del tiempo, tiene que buscar hasta el final.
Para medir cuánto tarda una función, podemos usar %time , que es uno de los “built-in magic
commands” de Jupyter. Estos comandos no forman parte del lenguaje Python, así que puede que
no funcionen en otros entornos de desarrollo.
# %time too_slow()
Y el operador in está dentro del bucle, así que se ejecuta una vez por cada palabra. Como hay
más de 100 000 palabras en la lista, y para cada una comprobamos más de 100 000 palabras, el
número total de comparaciones es el número de palabras al cuadrado – aproximadamente –, que
es casi 13 mil millones.
len(word_list)**2
Podemos hacer que esta función sea mucho más rápida con un diccionario. El siguiente bucle crea
un diccionario que contiene las palabras como claves.
word_dict = {}
for word in word_list:
word_dict[word] = 1
Los valores en word_dict son todos 1 , pero podrían ser cualquier cosa, porque nunca los vamos
a buscar; solo usaremos este diccionario para comprobar si existe una clave.
Ahora aquí tienes una versión de la función anterior que reemplaza word_list por word_dict .
def much_faster():
count = 0
for word in word_dict:
if reverse_word(word) in word_dict:
count += 1
return count
Esta función tarda menos de una centésima de segundo, así que es unas 10 000 veces más rápida
que la versión anterior.
En general, el tiempo que se tarda en encontrar un elemento en una lista es proporcional a la
longitud de la lista. El tiempo que se tarda en encontrar una clave en un diccionario es casi
constante, independientemente del número de elementos.
%time much_faster()
Mientras recorremos las letras del string, supongamos que vemos la letra 'a' por primera vez.
Podemos añadirla al diccionario así.
counter['a'] = 1
El valor 1 indica que hemos visto la letra una vez. Más tarde, si volvemos a ver la misma letra,
podemos incrementar el contador así.
counter['a'] += 1
Ahora el valor asociado con 'a' es 2 , porque hemos visto la letra dos veces.
counter
La siguiente función usa estas características para contar el número de veces que aparece cada
letra en un string.
def value_counts(string):
counter = {}
for letter in string:
if letter not in counter:
counter[letter] = 1
else:
counter[letter] += 1
return counter
Cada vez que pasa por el bucle, si letter no está en el diccionario, creamos un nuevo elemento
con clave letter y valor 1 . Si letter ya está en el diccionario, incrementamos el valor asociado
con letter .
Aquí tienes un ejemplo.
counter = value_counts('brontosaurus')
counter
Los elementos de counter muestran que la letra 'b' aparece una vez, 'r' aparece dos veces,
y así sucesivamente.
Para imprimir las claves y los valores, podemos recorrer las claves y buscar los valores
correspondientes.
for key in counter:
value = counter[key]
print(key, value)
Pero no puedes poner una lista en un diccionario como clave. Esto es lo que ocurre si lo
intentamos.
letters = list('abcd')
d[letters] = 4
Mencioné antes que los diccionarios usan hash tables, y eso significa que las claves tienen que ser
hasheable.
Un hash es una función que toma un valor (de cualquier tipo) y devuelve un entero. Los
diccionarios usan estos enteros, llamados hash valores, para almacenar y buscar claves.
Este sistema solo funciona si una clave es inmutable, de modo que su hash valor siempre sea el
mismo. Pero si una clave es mutable, su hash valor podría cambiar, y el diccionario no funcionaría.
Por eso las claves tienen que ser hasheables, y por eso tipos mutables como las listas no lo son.
Como los diccionarios son mutables, tampoco pueden usarse como claves. Pero sí pueden usarse
como valores.
count
palindromes[:10]
long_palindromes
Recorrer una lista así, seleccionando algunos elementos y omitiendo otros, se llama filtrado.
10.8. Memos
Si ejecutaste la función fibonacci del Capítulo 6, quizá notaste que cuanto mayor es el
argumento que proporcionas, más tarda la función en ejecutarse.
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
Además, el tiempo de ejecución aumenta rápidamente. Para entender por qué, considera la
siguiente figura, que muestra el call graph de fibonacci con n=4 :
from diagram import make_binding, Frame, Arrow
dx = 0.6
dy = 0.55
bboxes = []
[Link](frames[4].draw(ax, x+6*dx, y))
bbox = [Link](bboxes)
# adjust(x, y, bbox)
Un call graph muestra un conjunto de función frames, con líneas que conectan cada frame con los
frames de las funciones que llama. En la parte superior del graph, fibonacci con n=4 llama a
fibonacci con n=3 y n=2 . A su vez, fibonacci con n=3 llama a fibonacci con n=2 y n=1 .
Y así sucesivamente.
Cuenta cuántas veces se llama a fibonacci(0) y fibonacci(1) . Esta es una solución ineficiente
al problema, y empeora a medida que el argumento crece.
Una solución es llevar un registro de los valores que ya se han calculado almacenándolos en un
diccionario. Un valor calculado previamente que se almacena para usarlo más tarde se llama
memo. Aquí tienes una versión “memoized” de fibonacci :
known = {0:0, 1:1}
def fibonacci_memo(n):
if n in known:
return known[n]
known es un diccionario que lleva el registro de los números de Fibonacci que ya conocemos.
Empieza con dos elementos: 0 mapea a 0 y 1 mapea a 1 .
Cada vez que se llama a fibonacci_memo , comprueba known . Si el resultado ya está ahí, puede
devolverlo inmediatamente. De lo contrario, tiene que calcular el nuevo valor, añadirlo al diccionario
y devolverlo.
Comparando las dos funciones, fibonacci(40) tarda unos 30 segundos en ejecutarse.
fibonacci_memo(40) tarda unos 30 microsegundos, así que es un millón de veces más rápida. En
el notebook de este capítulo, verás de dónde vienen estas mediciones.
# %time fibonacci(40)
%time fibonacci_memo(40)
10.9. Depuración
A medida que trabajas con datasets más grandes, puede volverse inmanejable depurar
imprimiendo y comprobando la salida a mano. Aquí tienes algunas sugerencias para depurar
datasets grandes:
1. Reduce el tamaño de la entrada: Si es posible, reduce el tamaño del dataset. Por ejemplo, si el
programa lee un archivo de texto, empieza solo con las primeras 10 líneas, o con el ejemplo
más pequeño que puedas encontrar. Puedes editar los archivos directamente o, mejor,
modificar el programa para que lea solo las primeras n líneas.
Si hay un error, puedes reducir n al valor más pequeño donde ocurre el error. A medida que
encuentres y corrijas errores, puedes aumentar n gradualmente.
2. Comprueba resúmenes y tipos: En lugar de imprimir y comprobar el dataset completo,
considera imprimir resúmenes de los datos; por ejemplo, el número de elementos en un
diccionario o el total de una lista de números.
Una causa común de errores en tiempo de ejecución es un valor que no tiene el tipo correcto.
Para depurar de este tipo de error, a menudo basta con imprimir el tipo de un valor.
3. Escribe autocomprobaciones: A veces puedes escribir código para comprobar errores
automáticamente. Por ejemplo, si estás calculando la media de una lista de números, podrías
comprobar que el resultado no sea mayor que el elemento más grande de la lista ni menor que
el más pequeño. Esto se llama una “sanity check” porque detecta resultados que son “insane”.
Otro tipo de comprobación compara los resultados de dos computaciones diferentes para ver
si son consistentes. Esto se llama una “consistency check”.
4. Formatea la salida: Formatear la salida de depuración puede facilitar detectar un error. Vimos
un ejemplo en Capítulo 6. Otra herramienta que puede resultarte útil es el módulo pprint ,
que proporciona una función pprint que muestra tipos integrados en un formato más legible
para humanos ( pprint significa “pretty print”).
De nuevo, el tiempo que dediques a construir código de apoyo puede reducir el tiempo que
dedicas a depurar.
10.10. Glosario
diccionario: Un objeto que contiene pares clave-valor, también llamados elementos.
elemento: En un diccionario, otro nombre para un par clave-valor.
clave: Un objeto que aparece en un diccionario como la primera parte de un par clave-valor.
valor: Un objeto que aparece en un diccionario como la segunda parte de un par clave-valor. Esto
es más específico que nuestro uso anterior de la palabra “valor”.
mapeo: Una relación en la que cada elemento de un conjunto corresponde a un elemento de otro
conjunto.
hash table: Una colección de pares clave-valor organizada para que podamos buscar una clave y
encontrar su valor de forma eficiente.
hasheable: Tipos inmutables como enteros, floats y strings son hasheable. Tipos mutables como
listas y diccionarios no lo son.
función hash: Una función que toma un objeto y calcula un entero que se usa para localizar una
clave en una hash table.
accumulator: Una variable usada en un bucle para sumar o acumular un resultado.
filtrado: Recorrer una secuencia y seleccionar u omitir elementos.
call graph: Un diagrama que muestra cada frame creado durante la ejecución de un programa, con
una flecha desde cada código que llama hacia cada callee.
memo: Un valor calculado almacenado para evitar computación futura innecesaria.
10.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
10.11.2. Ejercicio
Los diccionarios tienen un método llamado get que toma una clave y un valor por defecto. Si la
clave aparece en el diccionario, get devuelve el valor correspondiente; de lo contrario devuelve el
valor por defecto. Por ejemplo, aquí tienes un diccionario que mapea las letras de un string al
número de veces que aparecen.
counter = value_counts('brontosaurus')
Si buscamos una letra que aparece en la palabra, get devuelve el número de veces que aparece.
[Link]('b', 0)
Usa get para escribir una versión más concisa de value_counts . Deberías poder eliminar la
sentencia if .
10.11.3. Ejercicio
¿Cuál es la palabra más larga que se te ocurre donde cada letra aparece solo una vez? Veamos si
podemos encontrar una más larga que unpredictably .
Escribe una función llamada has_duplicates que tome una secuencia, como una lista o string,
como parameter y devuelva True si hay algún elemento que aparece en la secuencia más de una
vez.
Para empezar, aquí tienes un esquema de la función con doctests.
def has_duplicates(t):
"""Check whether any element in a sequence appears more than once.
>>> has_duplicates('banana')
True
>>> has_duplicates('ambidextrously')
False
>>> has_duplicates([1, 2, 2])
True
>>> has_duplicates([1, 2, 3])
False
"""
return None
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(has_duplicates)
Puedes usar este bucle para encontrar las palabras más largas sin letras repetidas.
no_repeats = []
no_repeats
10.11.4. Ejercicio
Escribe una función llamada find_repeats que tome un diccionario que mapea cada clave a un
contador, como el resultado de value_counts . Debe recorrer el diccionario y devolver una lista de
claves que tengan recuentos mayores que 1 . Puedes usar el siguiente esquema para empezar.
def find_repeats(counter):
"""Makes a list of keys with values greater than 1.
Puedes usar los siguientes ejemplos para probar tu código. Primero, haremos un diccionario que
mapea letras a recuentos.
counter1 = value_counts('banana')
counter1
Aquí tienes otro ejemplo que empieza con una lista de números. El resultado debería ser [1, 2] .
counter1 = value_counts([1, 2, 3, 2, 1])
repeats = find_repeats(counter1)
repeats
10.11.5. Ejercicio
Supongamos que ejecutas value_counts con dos palabras diferentes y guardas los resultados en
dos diccionarios.
counter1 = value_counts('brontosaurus')
counter2 = value_counts('apatosaurus')
Cada diccionario mapea un conjunto de letras al número de veces que aparecen. Escribe una
función llamada add_counters que tome dos diccionarios como estos y devuelva un nuevo
diccionario que contenga todas las letras y el número total de veces que aparecen en cualquiera de
las dos palabras.
Hay muchas formas de resolver este problema. Cuando tengas una solución que funcione,
considera pedir a un asistente virtual soluciones diferentes.
10.11.6. Ejercicio
Una palabra es “interlocking” si podemos dividirla en dos palabras tomando letras alternas. Por
ejemplo, “schooled” es una palabra interlocking porque puede dividirse en “shoe” y “cold”.
Para seleccionar letras alternas de un string, puedes usar un operador de porción con tres
componentes que indican dónde empezar, dónde detenerse y el “step size” entre las letras.
En el siguiente porción, el primer componente es 0 , así que empezamos con la primera letra. El
segundo componente es None , lo que significa que debemos llegar hasta el final del string. Y el
tercer componente es 2 , así que hay dos pasos entre las letras que seleccionamos.
word = 'schooled'
first = word[0:None:2]
first
En lugar de proporcionar None como segundo componente, podemos obtener el mismo efecto
omitiéndolo por completo. Por ejemplo, el siguiente porción selecciona letras alternas, empezando
por la segunda letra.
second = word[1::2]
second
Escribe una función llamada is_interlocking que tome una palabra como argumento y devuelva
True si puede dividirse en dos palabras interlocking.
Puedes usar el siguiente bucle para encontrar las palabras interlocking en la palabra lista.
for word in word_list:
if len(word) >= 8 and is_interlocking(word):
first = word[0::2]
second = word[1::2]
print(word, first, second)
11. Tuplas
Este capítulo presenta un tipo integrado más, la tupla, y luego muestra cómo listas, diccionarios y
tuplas trabajan juntos. También presenta la asignación de tuplas y una característica útil para
funciones con listas de argumentos de longitud variable: los operadores de empaquetado y
desempaquetado.
En los ejercicios, usaremos tuplas, junto con listas y diccionarios, para resolver más puzles de
palabras e implementar algoritmos eficientes.
Una nota: hay dos maneras de pronunciar “tupla”. Algunas personas dicen “tuh-ple”, que rima con
“supple”. Pero en el contexto de la programación, la mayoría dice “too-ple”, que rima con
“quadruple”.
Para crear una tupla con un solo elemento, tienes que incluir una coma final.
t1 = 'p',
type(t1)
Otra forma de crear una tupla es la función integrada tuple . Sin argumentos, crea una tupla vacío.
t = tuple()
t
Si el argumento es una secuencia (string, lista o tupla), el resultado es una tupla con los elementos
de la secuencia.
t = tuple('lupin')
t
Como tuple es el nombre de una función integrada, deberías evitar usarlo como nombre de
variable.
La mayoría de los operadores de lista también funcionan con tuplas. Por ejemplo, el operador de
corchetes indexa un elemento.
t[0]
La función sorted funciona con tuplas – pero el resultado es una lista, no una tupla.
sorted(t)
El resultado es un objeto reversed , que podemos convertir en una lista o en una tupla.
tuple(reversed(t))
Según los ejemplos hasta ahora, podría parecer que las tuplas son iguales que las listas.
Y las tuplas no tienen ninguno de los métodos que modifican listas, como append y remove .
[Link]('l')
Recuerda que un “atributo” es una variable o método asociado con un objeto – este mensaje de
error significa que las tuplas no tienen un método llamado remove .
Como las tuplas son inmutables, son hasheables, lo que significa que pueden usarse como claves
en un diccionario. Por ejemplo, el siguiente diccionario contiene dos tuplas como claves que se
asocian con enteros.
d = {}
d[1, 2] = 3
d[3, 4] = 7
O, si tenemos una variable que se refiere a una tupla, podemos usarla como clave.
t = (3, 4)
d[t]
Los valores se asignan a las variables de izquierda a derecha – en este ejemplo, a recibe el valor
1 y b recibe el valor 2 . Podemos mostrar los resultados así:
a, b
De forma más general, si el lado izquierdo de una asignación es una tupla, el lado derecho puede
ser cualquier tipo de secuencia – string, lista o tupla. Por ejemplo, para dividir una dirección de
email en un nombre de usuario y un dominio, podrías escribir:
email = 'monty@[Link]'
username, domain = [Link]('@')
El valor de retorno de split es una lista con dos elementos – el primer elemento se asigna a
username , el segundo a domain .
username, domain
El número de variables a la izquierda y el número de valores a la derecha tienen que ser iguales –
de lo contrario obtienes un ValueError .
a, b = 1, 2, 3
La asignación de tupla es útil si quieres intercambiar los valores de dos variables. Con asignaciones
convencionales, tienes que usar una variable temporal, así:
temp = a
a = b
b = temp
Eso funciona, pero con la asignación de tupla podemos hacer lo mismo sin una variable temporal.
a, b = b, a
Esto funciona porque todas las expresiones del lado derecho se evalúan antes de hacer cualquiera
de las asignaciones.
También podemos usar asignación de tupla en una sentencia for . Por ejemplo, para hacer un
bucle por los elementos de un diccionario, podemos usar el método items .
d = {'one': 1, 'two': 2}
Cada vez que pasa por el bucle, item se asigna a una tupla que contiene una clave y el valor
correspondiente.
Podemos escribir este bucle de forma más concisa, así:
for key, value in [Link]():
print(key, '->', value)
Cada vez que pasa por el bucle, una clave y el valor correspondiente se asignan directamente a
key y value .
Podemos usar asignación de tupla para guardar los elementos dla tupla en dos variables.
quotient, remainder = divmod(7, 3)
quotient
remainder
Aquí tienes un ejemplo de una función que devuelve una tupla.
def min_max(t):
return min(t), max(t)
max y min son funciones integradas que encuentran los elementos mayor y menor de una
secuencia. min_max calcula ambos y devuelve una tupla de dos valores.
min_max([2, 4, 1, 3])
El parámetro puede tener cualquier nombre que quieras, pero args es convencional. Podemos
llamar a la función así:
mean(1, 2, 3)
Si tienes una secuencia de valores y quieres pasarlos a una función como múltiples argumentos,
puedes usar el operador * para desempaquetar la tupla. Por ejemplo, divmod toma
exactamente dos argumentos – si pasas una tupla como parámetro, obtienes un error.
t = (7, 3)
divmod(t)
Aunque la tupla contiene dos elementos, cuenta como un único argumento. Pero si desempaquetas
la tupla, se trata como dos argumentos.
divmod(*t)
Primero usa min_max para encontrar los elementos menor y mayor. Luego convierte args en una
lista para poder usar el método remove . Finalmente desempaqueta la lista para que los elementos
se pasen a mean como argumentos separados, en lugar de como una única lista.
Aquí tienes un ejemplo que muestra el efecto.
mean(1, 2, 3, 10)
trimmed_mean(1, 2, 3, 10)
Este tipo de media “recortada” se usa en algunos deportes con puntuación subjetiva – como saltos
y gimnasia – para reducir el efecto de un juez cuya puntuación se desvía de las demás.
11.6. Zip
Las tuplas son útiles para hacer un bucle por los elementos de dos secuencias y realizar
operaciones sobre elementos correspondientes. Por ejemplo, supongamos que dos equipos juegan
una serie de siete partidos, y registramos sus puntuaciones en dos listas, una para cada equipo.
scores1 = [1, 2, 4, 5, 1, 5, 2]
scores2 = [5, 5, 2, 2, 5, 2, 3]
Veamos cuántos partidos ganó cada equipo. Usaremos zip , que es una función integrada que
toma dos o más secuencias y devuelve un zip objeto, llamado así porque empareja los elementos
de las secuencias como los dientes de una cremallera.
zip(scores1, scores2)
Podemos usar el zip objeto para hacer un bucle por los valores de las secuencias por pares.
for pair in zip(scores1, scores2):
print(pair)
Cada vez que pasa por el bucle, pair recibe una tupla de puntuaciones. Así que podemos asignar
las puntuaciones a variables y contar las victorias del primer equipo, así:
wins = 0
for team1, team2 in zip(scores1, scores2):
if team1 > team2:
wins += 1
wins
El resultado es una lista de tuplas, así que podemos obtener el resultado del último partido así:
t[-1]
Si tienes una lista de claves y una lista de valores, puedes usar zip y dict para crear un
diccionario. Por ejemplo, así podemos crear un diccionario que asocia cada letra con su posición en
el alfabeto.
letters = 'abcdefghijklmnopqrstuvwxyz'
numbers = range(len(letters))
letter_map = dict(zip(letters, numbers))
El resultado es un enumerate objeto que hace un bucle por una secuencia de pares, donde cada
par contiene un índice (empezando en 0) y un elemento de la secuencia dada.
for index, element in enumerate('abc'):
print(index, element)
Esta forma de comparar tuplas es útil para ordenar una lista de tuplas, o para encontrar el mínimo o
el máximo. Como ejemplo, encontremos la letra más común en una palabra. En el capítulo anterior,
escribimos value_counts , que toma un string y devuelve un diccionario que asocia cada letra con
el número de veces que aparece.
def value_counts(string):
counter = {}
for letter in string:
if letter not in counter:
counter[letter] = 1
else:
counter[letter] += 1
return counter
Con solo tres elementos, podemos ver fácilmente que la letra más frecuente es 'a' , que aparece
tres veces. Pero si hubiera más elementos, sería útil ordenarlos automáticamente.
Podemos obtener los elementos de counter así.
items = [Link]()
items
El resultado es un objeto dict_items que se comporta como una lista de tuplas, así que podemos
ordenarlo así.
sorted(items)
El comportamiento por defecto es usar el primer elemento de cada tupla para ordenar la lista, y
usar el segundo elemento para desempatar.
Sin embargo, para encontrar los elementos con los conteos más altos, queremos usar el segundo
elemento para ordenar la lista. Podemos hacerlo escribiendo una función que toma una tupla y
devuelve el segundo elemento.
def second_element(t):
return t[1]
Luego podemos pasar esa función a sorted como un argumento opcional llamado key , que
indica que esta función debe usarse para calcular la sort clave de cada elemento.
sorted_items = sorted(items, key=second_element)
sorted_items
La sort clave determina el orden de los elementos en la lista. La letra con el conteo más bajo
aparece primero, y la letra con el conteo más alto aparece al final. Así podemos encontrar la letra
más común.
sorted_items[-1]
Si solo queremos el máximo, no tenemos que ordenar la lista. Podemos usar max , que también
toma key como argumento opcional.
max(items, key=second_element)
Para encontrar la letra con el conteo más bajo, podríamos usar min de la misma manera.
Si invertimos este diccionario, el resultado debería ser {1: ['p', 'a', 'o', 't'], 2: ['r']} ,
lo que indica que las letras que aparecen una vez son 'p' , 'a' , 'o' y 't' , y la letra que
aparece dos veces es 'r' .
La siguiente función toma un diccionario y devuelve su inverso como un nuevo diccionario.
def invert_dict(d):
new = {}
for key, value in [Link]():
if value not in new:
new[value] = [key]
else:
new[value].append(key)
return new
La sentencia for hace un bucle por las claves y los valores de d . Si el valor todavía no está en el
nuevo diccionario, se añade y se asocia con una lista que contiene un único elemento. De lo
contrario, se añade a la lista existente.
Podemos probarlo así:
invert_dict(d)
11.9. Depuración
Listas, diccionarios y tuplas son estructuras de datos. En este capítulo estamos empezando a ver
estructuras de datos compuestas, como listas de tuplas, o diccionarios que contienen tuplas como
claves y listas como valores. Las estructuras de datos compuestas son útiles, pero son propensas
a errores cuando una estructura de datos tiene el tipo, tamaño o estructura equivocados. Por
ejemplo, si una función espera una lista de enteros y le das un entero sin más (no dentro de una
lista), probablemente no funcionará.
Para ayudar a depurar este tipo de errores, escribí un módulo llamado structshape que
proporciona una función, también llamada structshape , que toma cualquier tipo de estructura de
datos como argumento y devuelve un string que resume su estructura. Puedes descargarlo desde
[Link]
download('[Link]
Si los elementos de la lista no son del mismo tipo, structshape los agrupa por tipo.
t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9]
structshape(t3)
Si tienes problemas para seguir la pista de tus estructuras de datos, structshape puede ayudar.
11.10. Glosario
pack: Reunir múltiples argumentos en una tupla.
unpack: Tratar una tupla (u otra secuencia) como múltiples argumentos.
zip objeto: El resultado de llamar a la función integrada zip ; se puede usar para hacer un bucle
por una secuencia de tuplas.
enumerate objeto: El resultado de llamar a la función integrada enumerate ; se puede usar para
hacer un bucle por una secuencia de tuplas.
sort clave: Un valor, o una función que calcula un valor, usado para ordenar los elementos de una
colección.
estructura de datos: Una colección de valores, organizada para realizar ciertas operaciones de
manera eficiente.
11.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
11.11.2. Ejercicio
En este capítulo dije que las tuplas pueden usarse como claves en diccionarios porque son
hasheables, y son hasheables porque son inmutables. Pero eso no siempre es cierto.
Si una tupla contiene un valor mutable, como una lista o un diccionario, la tupla ya no es hasheable
porque contiene elementos que no son hasheablesss. Como ejemplo, aquí tienes una tupla que
contiene dos listas de enteros.
list0 = [1, 2, 3]
list1 = [4, 5]
t = (list0, list1)
t
Escribe una línea de código que añada el valor 6 al final de la segunda lista en t . Si muestras t ,
el resultado debería ser ([1, 2, 3], [4, 5, 6]) .
Intenta crear un diccionario que asocie t con un string, y confirma que obtienes un TypeError .
Para saber más sobre este tema, pregunta a un asistente virtual: «Are Python tuplas always
hasheable?
11.11.3. Ejercicio
En este capítulo hicimos un diccionario que asocia cada letra con su índice en el alfabeto.
letters = 'abcdefghijklmnopqrstuvwxyz'
numbers = range(len(letters))
letter_map = dict(zip(letters, numbers))
Para ir en la otra dirección, podemos usar indexación de lista. Por ejemplo, la letra en el índice 1
es 'b' .
letters[1]
Podemos usar letter_map y letters para codificar y decodificar palabras usando un cifrado
César.
Un cifrado César es una forma débil de cifrado que consiste en desplazar cada letra un número fijo
de posiciones en el alfabeto, volviendo al principio si es necesario. Por ejemplo, 'a' desplazada 2
posiciones es 'c' y 'z' desplazada 1 posición es 'a' .
Escribe una función llamada shift_word que tome como parámetros un string y un entero, y
devuelva un nuevo string que contiene las letras del string desplazadas el número de posiciones
dado.
Para probar tu función, confirma que “cheer” desplazada 7 posiciones es “jolly” y “melon”
desplazada 16 posiciones es “cubed”.
Pistas: usa el operador módulo para volver de 'z' a 'a' . Haz un bucle por las letras de la
palabra, desplaza cada una y añade el resultado a una lista de letras. Luego usa join para
concatenar las letras en un string.
Puedes usar este esquema para empezar.
def shift_word(word, n):
"""Shift the letters of `word` by `n` places.
>>> shift_word('cheer', 7)
'jolly'
>>> shift_word('melon', 16)
'cubed'
"""
return None
shift_word('cheer', 7)
shift_word('melon', 16)
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(shift_word)
11.11.4. Ejercicio
Escribe una función llamada most_frequent_letters que tome un string e imprima las letras en
orden decreciente de frecuencia.
Para obtener los elementos en orden decreciente, puedes usar reversed junto con sorted o
puedes pasar reverse=True como keyword parameter a sorted .
Puedes usar este esquema de la función para empezar.
def most_frequent_letters(string):
return None
Cuando tu función funcione, puedes usar el siguiente código para imprimir las letras más comunes
en Dracula, que podemos descargar de Project Gutenberg.
download('[Link]
string = open('[Link]').read()
most_frequent_letters(string)
Según Codes and Secret Writing de Zim, la secuencia de letras en orden decreciente de frecuencia
en inglés empieza con “ETAONRISH”. ¿Cómo se compara esta secuencia con los resultados de
Dracula?
11.11.5. Ejercicio
En un ejercicio anterior, probamos si dos strings son anagramas ordenando las letras de ambas
palabras y comprobando si las letras ordenadas son iguales. Ahora hagamos el problema un poco
más desafiante.
Escribiremos un programa que toma una lista de palabras e imprime todos los conjuntos de
palabras que son anagramas. Aquí tienes un ejemplo de cómo podría verse la salida:
['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled']
['retainers', 'ternaries']
['generating', 'greatening']
['resmelts', 'smelters', 'termless']
Pista: para cada palabra de la lista de palabras, ordena las letras y vuelve a unirlas en un string.
Crea un diccionario que asocie este string ordenado con una lista de palabras que son anagramas
de él.
Las siguientes celdas descargan [Link] y leen las palabras en una lista.
download('[Link]
word_list = open('[Link]').read().split()
Para encontrar la lista más larga de anagramas, puedes usar la siguiente función, que toma un par
clave-valor donde la clave es un string y el valor es una lista de palabras. Devuelve la longitud de la
lista.
def value_length(pair):
key, value = pair
return len(value)
Podemos usar esta función como sort clave para encontrar las listas más largas de anagramas.
anagram_items = sorted(anagram_dict.items(), key=value_length)
for key, value in anagram_items[-10:]:
print(value)
Si quieres saber cuáles son las palabras más largas que tienen anagramas, puedes usar el siguiente
bucle para imprimir algunas.
longest = 7
11.11.6. Ejercicio
Escribe una función llamada word_distance que tome dos palabras con la misma longitud y
devuelva el número de posiciones donde las dos palabras difieren.
Pista: usa zip para hacer un bucle por las letras correspondientes de las palabras.
Aquí tienes un esquema de la función con doctests que puedes usar para comprobar tu función.
def word_distance(word1, word2):
"""Computes the number of places where two word differ.
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(word_distance)
11.11.7. Ejercicio
“Metathesis” es la transposición de letras en una palabra. Dos palabras forman una “metathesis
pair” si puedes transformar una en la otra intercambiando dos letras, como converse y
conserve . Escribe un programa que encuentre todos los pares de metathesis en la lista de
palabras.
Pista: las palabras de un par de metathesis deben ser anagramas entre sí.
Crédito: este ejercicio está inspirado en un ejemplo de [Link]
11.11.8. Ejercicio
Este es un ejercicio extra que no está en el libro. Es más desafiante que los otros ejercicios de este
capítulo, así que quizá quieras pedir ayuda a un asistente virtual, o volver a él después de leer
algunos capítulos más.
Aquí tienes otro Car Talk Puzzler ([Link]
¿Cuál es la palabra inglesa más larga que sigue siendo una palabra inglesa válida mientras le
quitas letras una a una?
Ahora, las letras se pueden quitar de cualquiera de los extremos o del medio, pero no puedes
reordenar ninguna de las letras. Cada vez que quitas una letra, terminas con otra palabra
inglesa. Si haces eso, al final terminarás con una letra, y esa también será una palabra inglesa
– una que aparece en el diccionario. Quiero saber cuál es la palabra más larga y cuántas letras
tiene.
Te voy a dar un ejemplo pequeño: Sprite. ¿Ok? Empiezas con sprite, le quitas una letra, una del
interior de la palabra, quitas la r, y nos queda la palabra spite; luego quitamos la e del final, nos
queda spit; quitamos la s, nos queda pit, it e I.
Escribe un programa para encontrar todas las palabras que pueden reducirse de esta manera, y
luego encuentra la más larga.
Este ejercicio es un poco más desafiante que la mayoría, así que aquí tienes algunas sugerencias:
1. Quizá quieras escribir una función que tome una palabra y calcule una lista de todas las
palabras que pueden formarse quitando una letra. Estas son las “children” de la palabra.
2. Recursivamente, una palabra es reducible si cualquiera de sus children es reducible. Como
caso base, puedes considerar que el string vacío es reducible.
3. La lista de palabras que hemos estado usando no contiene palabras de una sola letra. Así que
quizá tengas que añadir “I” y “a”.
4. Para mejorar el rendimiento de tu programa, quizá quieras memoizar las palabras que se sabe
que son reducibles.
Think Python: 3rd Edition
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Código license: MIT License
Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
[Link]()
[Link]()
filename = 'dr_jekyll.txt'
clean_file('[Link]', filename)
Usaremos un bucle for para leer líneas del archivo y split para dividir las líneas en palabras.
Luego, para llevar la cuenta de las palabras únicas, guardaremos cada palabra como una clave en
un diccionario.
unique_words = {}
for line in open(filename):
seq = [Link]()
for word in seq:
unique_words[word] = 1
len(unique_words)
La longitud del diccionario es el número de palabras únicas – unas 6000 con esta forma de contar.
Pero si las inspeccionamos, veremos que algunas no son palabras válidas.
Por ejemplo, veamos las palabras más largas en unique_words . Podemos usar sorted para
ordenar las palabras, pasando la función len como keyword argumento para que las palabras se
ordenen por longitud.
sorted(unique_words, key=len)[-5:]
El índice de porción, [-5:] , selecciona los últimos 5 elementos de la lista ordenada, que son las
palabras más largas.
La lista incluye algunas palabras legítimamente largas, como “circumscription”, y algunas palabras
con guion, como “chocolate-coloured”. Pero algunas de las “palabras” más largas son en realidad
dos palabras separadas por una raya. Y otras palabras incluyen puntuación como puntos, signos de
exclamación y comillas.
Así que, antes de seguir, ocupémonos de las rayas y otros signos de puntuación.
12.2. Puntuación
Para identificar las palabras del texto, tenemos que resolver dos cuestiones:
Cuando aparece una raya en una línea, deberíamos reemplazarla por un espacio – así, cuando
usemos split , las palabras quedarán separadas.
Después de separar las palabras, podemos usar strip para eliminar la puntuación.
Para manejar la primera cuestión, podemos usar la siguiente función, que toma un string,
reemplaza las rayas por espacios, divide el string y devuelve la lista resultante.
def split_line(line):
return [Link]('—', ' ').split()
Observa que split_line solo reemplaza rayas, no guiones. Aquí tienes un ejemplo.
split_line('coolness—frightened')
Ahora, para eliminar la puntuación del principio y del final de cada palabra, podemos usar strip ,
pero necesitamos una lista de caracteres que se consideran puntuación.
Los caracteres en los strings de Python están en Unicode, que es un estándar internacional usado
para representar letras de casi todos los alfabetos, números, símbolos, signos de puntuación y
más. El módulo unicodedata proporciona una función category que podemos usar para saber
qué caracteres son puntuación. Dada una letra, devuelve un string con información sobre la
categoría a la que pertenece.
import unicodedata
[Link]('A')
El string de categoría de 'A' es 'Lu' – la 'L' significa que es una letra y la 'u' significa que
es mayúscula.
El string de categoría de '.' es 'Po' – la 'P' significa que es puntuación y la 'o' significa que
su subcategoría es “other”.
[Link]('.')
Podemos encontrar los signos de puntuación del libro comprobando los caracteres con categorías
que empiezan por 'P' . El siguiente bucle guarda los signos de puntuación únicos en un
diccionario.
punc_marks = {}
for line in open(filename):
for char in line:
category = [Link](char)
if [Link]('P'):
punc_marks[char] = 1
Para crear una lista de signos de puntuación, podemos unir las claves del diccionario en un string.
punctuation = ''.join(punc_marks)
print(punctuation)
Ahora que sabemos qué caracteres del libro son puntuación, podemos escribir una función que
toma una palabra, elimina la puntuación del principio y del final, y la convierte a minúsculas.
def clean_word(word):
return [Link](punctuation).lower()
Como strip elimina caracteres del principio y del final, deja intactas las palabras con guion.
clean_word('pocket-handkerchief')
Ahora aquí tienes un bucle que usa split_line y clean_word para identificar las palabras únicas
del libro.
unique_words2 = {}
for line in open(filename):
for word in split_line(line):
word = clean_word(word)
unique_words2[word] = 1
len(unique_words2)
Con esta definición más estricta de lo que es una palabra, hay unas 4000 palabras únicas. Y
podemos confirmar que la lista de palabras más largas se ha limpiado.
sorted(unique_words2, key=len)[-5:]
En la siguiente sección, encapsularemos este bucle en una función. Y la usaremos para demostrar
una nueva característica – parámetros opcionales.
Pero no son solo las funciones integradas – también podemos escribir funciones con parámetros
opcionales. Por ejemplo, la siguiente función toma dos parámetros, word_counter y num .
def print_most_common(word_counter, num=5):
items = sorted(word_counter.items(), key=second_element, reverse=True)
Si llamas a esta función con dos argumentos, el segundo argumento se asigna a num en lugar del
valor por defecto.
print_most_common(word_counter, 3)
En ese caso, diríamos que el argumento opcional override el valor por defecto.
Si una función tiene parámetros requeridos y opcionales, todos los parámetros requeridos tienen
que ir primero, seguidos por los opcionales.
def bad_function(n=5, word_counter):
return None
Como hemos hecho antes, podemos leer el contenido de [Link] y dividirlo en una lista de
strings.
word_list = open('[Link]').read().split()
Luego guardaremos las palabras como claves en un diccionario para poder usar el operador in y
comprobar rápidamente si una palabra es válida.
valid_words = {}
for word in word_list:
valid_words[word] = 1
Ahora, para identificar palabras que aparecen en el libro pero no en la lista de palabras, usaremos
subtract , que toma dos diccionarios como parámetros y devuelve un nuevo diccionario que
contiene todas las claves de uno que no están en el otro.
def subtract(d1, d2):
res = {}
for key in d1:
if key not in d2:
res[key] = d1[key]
return res
Para obtener una muestra de palabras que podrían estar mal escritas, podemos imprimir las
palabras más comunes en diff .
print_most_common(diff)
Las “palabras mal escritas” más comunes son en su mayoría nombres y algunas palabras de una
sola letra (Mr. Utterson es el amigo y abogado del Dr. Jekyll).
Si seleccionamos palabras que solo aparecen una vez, es más probable que sean errores
ortográficos reales. Podemos hacerlo haciendo un bucle por los elementos y creando una lista de
palabras con frecuencia 1 .
singletons = []
for word, freq in [Link]():
if freq == 1:
[Link](word)
La mayoría son palabras válidas que no están en la lista de palabras. Pero 'reindue' parece ser
un error ortográfico de 'reinduce' , así que al menos encontramos un error legítimo.
[Link](4)
El módulo random proporciona una función llamada choice que elige un elemento de una lista al
azar, con la misma probabilidad de elegir cada elemento.
t = [1, 2, 3]
[Link](t)
Si llamas a la función otra vez, podrías obtener el mismo elemento de nuevo, o uno diferente.
[Link](t)
A largo plazo, esperamos obtener cada elemento aproximadamente el mismo número de veces.
Si usas choice con un diccionario, obtienes un KeyError .
[Link](word_counter)
Para elegir una clave aleatoria, tienes que poner las claves en una lista y luego llamar a choice .
words = list(word_counter)
[Link](words)
El módulo random proporciona otra función llamada choices que toma weights como argumento
opcional.
[Link](words, weights=weights)
El resultado es una lista de strings que podemos unir en algo que se parece más a una oración.
' '.join(random_words)
Si eliges palabras del libro al azar, obtienes una idea del vocabulario, pero una serie de palabras
aleatorias rara vez tiene sentido porque no hay relación entre palabras sucesivas. Por ejemplo, en
una oración real esperas que un artículo como “the” vaya seguido de un adjetivo o un sustantivo, y
probablemente no de un verbo o adverbio. Así que el siguiente paso es observar estas relaciones
entre palabras.
12.7. Bigramas
En lugar de mirar una palabra cada vez, ahora miraremos secuencias de dos palabras, que se
llaman bigramas. Una secuencia de tres palabras se llama trigram, y una secuencia con un
número no especificado de palabras se llama n-gram.
Escribamos un programa que encuentre todos los bigramas del libro y el número de veces que
aparece cada uno. Para guardar los resultados, usaremos un diccionario donde
Las claves son tuplas de strings que representan bigramas, y
Los valores son enteros que representan frecuencias.
Llamémoslo bigram_counter .
bigram_counter = {}
La siguiente función toma una lista de dos strings como parámetro. Primero crea una tupla con los
dos strings, que puede usarse como clave en un diccionario. Luego añade la clave a
bigram_counter , si no existe, o incrementa la frecuencia si existe.
def count_bigram(bigram):
key = tuple(bigram)
if key not in bigram_counter:
bigram_counter[key] = 1
else:
bigram_counter[key] += 1
A medida que recorremos el libro, tenemos que llevar la cuenta de cada par de palabras
consecutivas. Así que si vemos la secuencia “man is not truly one”, añadiríamos los bigramas “man
is”, “is not”, “not truly”, y así sucesivamente.
Para llevar la cuenta de estos bigramas, usaremos una lista llamada window , porque es como una
ventana que se desliza sobre las páginas del libro, mostrando solo dos palabras a la vez.
Inicialmente, window está vacía.
window = []
Usaremos la siguiente función para procesar las palabras una por una.
def process_word(word):
[Link](word)
if len(window) == 2:
count_bigram(window)
[Link](0)
La primera vez que se llama a esta función, añade la palabra dada a window . Como solo hay una
palabra en la window, todavía no tenemos un bigrama, así que la función termina.
La segunda vez que se llama – y todas las veces después – añade una segunda palabra a window .
Como hay dos palabras en la window, llama a count_bigram para llevar la cuenta de cuántas
veces aparece cada bigrama. Luego usa pop para eliminar la primera palabra de la window.
El siguiente programa hace un bucle por las palabras del libro y las procesa una a una.
for line in open(filename):
for word in split_line(line):
word = clean_word(word)
process_word(word)
El resultado es un diccionario que asocia cada bigrama con el número de veces que aparece.
Podemos usar print_most_common para ver los bigramas más comunes.
print_most_common(bigram_counter)
Al mirar estos resultados, podemos hacernos una idea de qué pares de palabras tienen más
probabilidad de aparecer juntos. También podemos usar los resultados para generar texto
aleatorio, así.
[Link](0)
bigrams = list(bigram_counter)
weights = bigram_counter.values()
random_bigrams = [Link](bigrams, weights=weights, k=6)
bigrams es una lista de los bigramas que aparecen en los libros. weights es una lista de sus
frecuencias, así que random_bigrams es una muestra donde la probabilidad de que se seleccione
un bigrama es proporcional a su frecuencia.
Aquí están los resultados.
for pair in random_bigrams:
print(' '.join(pair), end=' ')
Esta forma de generar texto es mejor que elegir palabras aleatorias, pero todavía no tiene mucho
sentido.
Para guardar los resultados, usaremos un diccionario que asocia cada palabra con la lista de
palabras que la siguen.
successor_map = {}
Si la primera palabra no está en successor_map , tenemos que añadir un nuevo elemento que
asocie la primera palabra con una lista que contiene la segunda palabra.
successor_map[first] = [second]
successor_map
Si la primera palabra ya está en el diccionario, podemos buscarla para obtener la lista de sucesores
que hemos visto hasta ahora, y añadir el nuevo.
first = 'half'
second = 'not'
successor_map[first].append(second)
successor_map
Si el mismo bigrama aparece más de una vez, la segunda palabra se añade a la lista más de una
vez. De esta manera, successor_map lleva la cuenta de cuántas veces aparece cada sucesor.
Como hicimos en la sección anterior, usaremos una lista llamada window para guardar pares de
palabras consecutivas. Y usaremos la siguiente función para procesar las palabras una por una.
def process_word_bigram(word):
[Link](word)
if len(window) == 2:
add_bigram(window)
[Link](0)
Podemos buscar cualquier palabra y encontrar las palabras que pueden seguirla.
# I used this cell to find a predecessor with a good number of possible successors
# and at least one repeated word.
def has_duplicates(t):
return len(set(t)) < len(t)
successor_map['going']
En esta lista de sucesores, observa que la palabra 'to' aparece tres veces – los demás sucesores
aparecen solo una vez.
[Link](2)
Si la misma palabra aparece más de una vez en la lista, es más probable que sea seleccionada.
Repitiendo estos pasos, podemos usar el siguiente bucle para generar una serie más larga.
for i in range(10):
successors = successor_map[word]
word = [Link](successors)
print(word, end=' ')
El resultado suena más como una oración real, pero todavía no tiene mucho sentido.
Podemos hacerlo mejor usando más de una palabra como clave en successor_map . Por ejemplo,
podemos crear un diccionario que asocie cada bigrama – o trigram – con la lista de palabras que
vienen después. Como ejercicio, tendrás la oportunidad de implementar este análisis y ver cómo
son los resultados.
12.10. Depuración
A estas alturas estamos escribiendo programas más sustanciales, y puede que descubras que
pasas más tiempo depurando. Si estás atascado con un bug difícil, aquí tienes algunas cosas que
puedes probar:
Leer: examina tu código, léelo en voz alta para ti y comprueba que dice lo que querías decir.
Ejecutar: experimenta haciendo cambios y ejecutando versiones diferentes. A menudo, si
muestras lo correcto en el lugar correcto del programa, el problema se vuelve obvio, pero a
veces tienes que construir código de apoyo.
Rumiar: ¡tómate tiempo para pensar! ¿Qué tipo de error es: de sintaxis, de runtime, o
semántico? ¿Qué información puedes obtener de los mensajes de error, o del output del
programa? ¿Qué tipo de error podría causar el problema que estás viendo? ¿Qué cambiaste
por última vez, antes de que apareciera el problema?
Rubberducking: si explicas el problema a otra persona, a veces encuentras la respuesta antes
de terminar de hacer la pregunta. A menudo no necesitas a la otra persona; podrías hablarle a
un patito de goma. Y ese es el origen de la estrategia conocida como rubber duck
depuración. No me lo estoy inventando – ver
[Link]
Retirarse: en algún momento, lo mejor es retroceder – deshacer cambios recientes – hasta
llegar a un programa que funcione. Luego puedes empezar a reconstruir.
Descansar: si le das un respiro a tu cerebro, a veces encontrará el problema por ti.
Los programadores principiantes a veces se quedan atascados en una de estas actividades y
olvidan las demás. Cada actividad tiene su propio modo de fallo.
Por ejemplo, leer tu código funciona si el problema es un error tipográfico, pero no si el problema
es un malentendido conceptual. Si no entiendes lo que hace tu programa, puedes leerlo 100 veces
y nunca ver el error, porque el error está en tu cabeza.
Ejecutar experimentos puede funcionar, especialmente si ejecutas tests pequeños y simples. Pero
si haces experimentos sin pensar ni leer tu código, puede llevar mucho tiempo averiguar qué está
pasando.
Tienes que tomarte tiempo para pensar. Depurar es como una ciencia experimental. Deberías tener
al menos una hipótesis sobre cuál es el problema. Si hay dos o más posibilidades, intenta pensar en
un test que elimine una de ellas.
Pero incluso las mejores técnicas de depuración fallarán si hay demasiados errores, o si el código
que intentas arreglar es demasiado grande y complicado. A veces la mejor opción es retirarse,
simplificando el programa hasta volver a algo que funcione.
Los programadores principiantes a menudo se resisten a retirarse porque no soportan borrar una
línea de código (aunque esté mal). Si te hace sentir mejor, copia tu programa en otro archivo antes
de empezar a recortarlo. Luego puedes copiar las piezas de vuelta una por una.
Encontrar un bug difícil requiere leer, ejecutar, rumiar, retirarse y a veces descansar. Si te atascas
en una de estas actividades, prueba las otras.
12.11. Glosario
valor por defecto: El valor asignado a un parámetro si no se proporciona ningún argumento.
override: Reemplazar un valor por defecto por un argumento.
determinista: Un programa determinista hace lo mismo cada vez que se ejecuta, dados los
mismos inputs.
pseudorandom: Una secuencia pseudorandom de números parece aleatoria, pero es generada
por un programa determinista.
bigrama: Una secuencia de dos elementos, a menudo palabras.
trigram: Una secuencia de tres elementos.
n-gram: Una secuencia de un número no especificado de elementos.
rubber duck depuración: Una forma de depurar explicando un problema en voz alta a un objeto
inanimado.
12.12. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
12.12.1. Pregunta a un asistente virtual
En add_bigram , la sentencia if crea una nueva lista o añade un elemento a una lista existente,
dependiendo de si la clave ya está en el diccionario.
def add_bigram(bigram):
first, second = bigram
Diccionarios proporcionan un método llamado setdefault que podemos usar para hacer lo
mismo de forma más concisa. Pregunta a un asistente virtual cómo funciona, o copia add_bigram
en un asistente virtual y pregunta “Can you rewrite this using setdefault ?”
En este capítulo implementamos análisis y generación de texto con cadenas de Markov. Si tienes
curiosidad, puedes pedirle a un asistente virtual más información sobre el tema. Una de las cosas
que podrías aprender es que los asistentes virtuales usan algoritmos que son similares en muchos
aspectos – pero también diferentes en aspectos importantes. Pregunta a un VA: «What are the
differences between large language models like GPT and Markov chain text analysis?
12.12.2. Ejercicio
Escribe una función que cuente el número de veces que aparece cada trigram (secuencia de tres
palabras). Si pruebas tu función con el texto de Dr. Jekyll and Mr. Hyde, deberías encontrar que el
trigram más común es “said the lawyer”.
Pista: escribe una función llamada count_trigram que sea similar a count_bigram . Luego
escribe una función llamada process_word_trigram que sea similar a process_word_bigram .
Puedes usar el siguiente bucle para leer el libro y procesar las palabras.
trigram_counter = {}
window = []
Luego usa print_most_common para encontrar los trigrams más comunes del libro.
print_most_common(trigram_counter)
12.12.3. Ejercicio
Ahora implementemos análisis de texto con cadenas de Markov usando un mapeo de cada bigrama
a una lista de posibles sucesores.
Empezando con add_bigram , escribe una función llamada add_trigram que tome una lista de
tres palabras y añada o actualice un elemento en successor_map , usando las dos primeras
palabras como clave y la tercera palabra como posible sucesor.
Aquí tienes una versión de process_word_trigram que llama a add_trigram .
def process_word_trigram(word):
[Link](word)
if len(window) == 3:
add_trigram(window)
[Link](0)
Puedes usar el siguiente bucle para probar tu función con la letra de “Eric, the Half a Bee”.
successor_map = {}
window = []
Puedes usar el siguiente bucle para probar tu función con las palabras del libro.
successor_map = {}
window = []
En el siguiente ejercicio, usarás los resultados para generar nuevo texto aleatorio.
12.12.4. Ejercicio
Para este ejercicio, asumiremos que successor_map es un diccionario que asocia cada bigrama
con la lista de palabras que lo siguen.
# this cell initializes the random number generator so it
# starts at the same point in the sequence each time this
# notebook runs.
[Link](3)
Para generar texto aleatorio, empezaremos eligiendo una clave aleatoria de successor_map .
successors = list(successor_map)
bigram = [Link](successors)
bigram
Ahora escribe un bucle que genere 50 palabras más siguiendo estos pasos:
1. En successor_map , busca la lista de palabras que pueden seguir a bigram .
2. Elige una de ellas al azar e imprímela.
3. Para la siguiente iteración, crea un nuevo bigrama que contenga la segunda palabra de
bigram y el sucesor elegido.
Por ejemplo, si empezamos con el bigrama ('doubted', 'if') y elegimos 'from' como
sucesor, el siguiente bigrama es ('if', 'from') .
Si todo funciona, deberías encontrar que el texto generado es reconociblemente similar en estilo al
original, y algunas frases tienen sentido, pero el texto podría saltar de un tema a otro.
Como ejercicio extra, modifica tu solución a los dos últimos ejercicios para usar trigrams como
claves en successor_map , y mira qué efecto tiene en los resultados.
Think Python: 3rd Edition
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Código license: MIT License
Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
Crédito: las fotos se descargaron de Lorem Picsum, un servicio que proporciona imágenes de
marcador de posición. El nombre es una referencia a “lorem ipsum”, que es un nombre para texto
de marcador de posición.
# This cell downloads an archive file that contains the the files we'll
# use for the examples in this chapter.
download('[Link]
!unzip -o [Link]
13. Archivos y bases de datos
La mayoría de los programas que hemos visto hasta ahora son efímeros en el sentido de que se
ejecutan durante poco tiempo y producen output, pero cuando terminan, sus datos desaparecen.
Cada vez que ejecutas un programa efímero, empieza desde cero.
Otros programas son persistentes: se ejecutan durante mucho tiempo (o todo el tiempo);
mantienen al menos parte de sus datos en almacenamiento a largo plazo; y, si se cierran y se
reinician, continúan donde lo dejaron.
Una forma sencilla de que los programas mantengan sus datos es leer y escribir archivos de texto.
Una alternativa más versátil es almacenar datos en una base de datos. Las bases de datos son
archivos especializados que se pueden leer y escribir de forma más eficiente que los archivos de
texto, y proporcionan capacidades adicionales.
En este capítulo escribiremos programas que leen y escriben archivos de texto y bases de datos, y
como ejercicio escribirás un programa que busca duplicados en una colección de fotos. Pero antes
de poder trabajar con un archivo, tienes que encontrarlo, así que empezaremos con nombres de
archivo, rutas y directorios.
import os
def getcwd():
return "/home/dinsdale"
[Link] = getcwd
import os
[Link]()
El resultado en este ejemplo es el directorio personal de un usuario llamado dinsdale . Una string
como '/home/dinsdale' que identifica un archivo o directorio se llama ruta.
Un nombre de archivo sencillo como '[Link]' también se considera una ruta, pero es un ruta
relativa porque especifica un nombre de archivo relativo al directorio actual. En este ejemplo, el
directorio actual es /home/dinsdale , así que '[Link]' equivale a la ruta completa
'/home/dinsdale/[Link]' .
Un ruta que empieza con / no depende del directorio actual – se llama ruta absoluta. Para
encontrar la ruta absoluta de un archivo, puedes usar abspath .
[Link]('[Link]')
El módulo os proporciona otras funciones para trabajar con nombres de archivo y rutas. listdir
devuelve una lista con el contenido del directorio indicado, incluyendo archivos y otros directorios.
Aquí tienes un ejemplo que lista el contenido de un directorio llamado photos .
[Link]('photos')
Este directorio contiene un archivo de texto llamado [Link] y tres directorios. Los directorios
contienen archivos de imagen en formato JPEG.
[Link]('photos/jan-2023')
[Link]('photos/apr-2023')
Para comprobar si una ruta se refiere a un archivo o a un directorio, podemos usar isdir , que
devuelve True si una ruta se refiere a un directorio.
[Link]('photos')
Un desafío al trabajar con rutas es que se ven distintos en diferentes sistemas operativos. En
macOS y sistemas UNIX como Linux, los nombres de directorios y archivos en una ruta se separan
con una barra inclinada, / . Windows usa una barra invertida, \ . Así que, si ejecutas estos
ejemplos en Windows, verás barras invertidas en los rutas y tendrás que reemplazar las barras
inclinadas de los ejemplos.
O, para escribir código que funcione en ambos sistemas, puedes usar [Link] , que une
nombres de directorio y de archivo en una ruta usando una barra inclinada o invertida, según el
sistema operativo que estés usando.
[Link]('photos', 'jan-2023', '[Link]')
Más adelante en este capítulo usaremos estas funciones para buscar en un conjunto de directorios
y encontrar todos los archivos de imagen.
13.2. f-strings
Una forma en que los programas almacenan datos es escribirlos en un archivo de texto. Por
ejemplo, supón que observas camellos y quieres registrar el número de camellos que has visto
durante un periodo de observación. Y supón que en un año y medio has visto 23 camellos. Los
datos de tu cuaderno de observación de camellos podrían verse así.
num_years = 1.5
num_camels = 23
Para escribir estos datos en un archivo, puedes usar el método write , que vimos en el Capítulo 8.
El argumento de write tiene que ser una string, así que si queremos poner otros valores en un
archivo, tenemos que convertirlos a strings. La forma más sencilla de hacerlo es con la función
integrada str .
Así es como se ve:
writer = open('[Link]', 'w')
[Link](str(num_years))
[Link](str(num_camels))
[Link]()
Eso funciona, pero write no añade un espacio ni una nueva línea a menos que lo incluyas
explícitamente. Si volvemos a leer el archivo, vemos que los dos números quedan pegados.
open('[Link]').read()
Como mínimo, deberíamos añadir espacios en blanco entre los números. Y ya que estamos,
añadamos algo de texto explicativo.
Para escribir una combinación de strings y otros valores, podemos usar una f-string, que es una
string que tiene la letra f antes de la comilla de apertura y contiene una o más expresiones de
Python entre llaves. La siguiente f-string contiene una expresión, que es el nombre de una variable.
f'I have spotted {num_camels} camels'
Ambas f-strings terminan con la secuencia \n , que añade un carácter de nueva línea.
Podemos volver a leer el archivo así:
data = open('[Link]').read()
print(data)
En una f-string, una expresión entre llaves se convierte en una string, así que puedes incluir listas,
diccionarios y otros tipos.
t = [1, 2, 3]
d = {'one': 1}
f'Here is a list {t} and a dictionary {d}'
13.3. YAML
Una de las razones por las que los programas leen y escriben archivos es para almacenar datos de
configuración, que son información que especifica qué debe hacer el programa y cómo.
Por ejemplo, en un programa que busca fotos duplicadas, podríamos tener un diccionario llamado
config que contiene el nombre del directorio donde buscar, el nombre de otro directorio donde
debería almacenar los resultados y una lista de extensiones de archivo que debería usar para
identificar archivos de imagen.
Así podría verse:
config = {
'photo_dir': 'photos',
'data_dir': 'photo_info',
'extensions': ['jpg', 'jpeg'],
}
Para escribir estos datos en un archivo de texto, podríamos usar f-strings, como en la sección
anterior. Pero es más fácil usar un módulo llamado yaml que está diseñado justo para este tipo de
cosas.
El módulo yaml proporciona funciones para trabajar con archivos YAML, que son archivos de texto
con un formato pensado para que sean fáciles de leer y escribir tanto para humanos como para
programas.
Aquí tienes un ejemplo que usa la función dump para escribir el diccionario config en un archivo
YAML.
# this cell installs the pyyaml package, which provides the yaml module
try:
import yaml
except ImportError:
!pip install pyyaml
import yaml
config_filename = '[Link]'
writer = open(config_filename, 'w')
[Link](config, writer)
[Link]()
Si volvemos a leer el contenido del archivo, podemos ver cómo es el formato YAML.
readback = open(config_filename).read()
print(readback)
Ahora podemos usar safe_load para volver a leer el archivo YAML.
reader = open(config_filename)
config_readback = yaml.safe_load(reader)
config_readback
El resultado es un nuevo diccionario que contiene la misma información que el original, pero no es
el mismo diccionario.
config is config_readback
Convertir un objeto como un diccionario en una string se llama serialización. Convertir la string de
vuelta en un objeto se llama deserialización. Si serializas y luego deserializas un objeto, el
resultado debería ser equivalente al original.
13.4. Shelve
Hasta ahora hemos estado leyendo y escribiendo archivos de texto – ahora consideremos las
bases de datos. Una base de datos es un archivo organizado para almacenar datos. Algunas
bases de datos se organizan como una tabla con filas y columnas de información. Otras se
organizan como un diccionario que mapea claves a valores; a veces se llaman almacenes clave-
valor.
El módulo shelve proporciona funciones para crear y actualizar un almacén clave-valor llamado
“shelf”. Como ejemplo, crearemos un shelf para contener captions de las figuras del directorio
photos . Usaremos el diccionario config para obtener el nombre del directorio donde
deberíamos poner el shelf.
config['data_dir']
Ahora podemos usar [Link] para abrir el archivo del shelf. El argumento c indica que el
archivo debería crearse si es necesario.
import shelve
db = [Link](db_file, 'c')
db
Si obtienes un error como db type could not be determined , la causa más probable es que ya
exista un archivo con el mismo nombre, pero que no sea una base de datos shelve válida (por
ejemplo, puede estar corrupto o haber sido creado por otra cosa).
En ese caso, la solución más sencilla es eliminar el archivo existente y volver a ejecutar el código
para que [Link] pueda crear una base de datos nueva.
El valor de retorno es oficialmente un objeto DbfilenameShelf , llamado de manera más informal
un objeto shelf.
El objeto shelf se comporta de muchas maneras como un diccionario. Por ejemplo, podemos usar
el operador de corchetes para añadir un elemento, que es un mapeo de una clave a un valor.
key = 'jan-2023/[Link]'
db[key] = 'Cat nose'
En este ejemplo, la clave es la ruta a un archivo de imagen y el valor es una string que describe la
imagen.
También usamos el operador de corchetes para buscar una clave y obtener el valor
correspondiente.
value = db[key]
value
Si haces otra asignación a una clave existente, shelve reemplaza el valor anterior.
db[key] = 'Close up view of a cat nose'
db[key]
Algunos métodos de diccionario, como keys , values e items , también funcionan con objetos
shelf.
list([Link]())
list([Link]())
Y podemos usar una sentencia for para iterar sobre las claves.
for key in db:
print(key, ':', db[key])
Como con otros archivos, deberías cerrar la base de datos cuando termines.
[Link]()
!rm -f photo_info/[Link]
[Link](config['data_dir'])
[Link] contiene los datos que acabamos de almacenar. [Link] contiene
información sobre la organización de la base de datos que hace que el acceso sea más eficiente. El
sufijo dir significa “directorio”, pero no tiene nada que ver con los directorios con los que hemos
estado trabajando y que contienen archivos.
Ahora abramos un shelf llamado anagram_map . El argumento 'n' significa que siempre
deberíamos crear un shelf nuevo y vacío, aunque ya exista uno.
db = [Link]('anagram_map', 'n')
La clave es la misma que en el ejemplo anterior, así que queremos añadir una segunda palabra a la
misma lista de strings. Así es como lo haríamos si db fuera un diccionario.
db[key].append(word) # INCORRECT
Este es el problema: cuando buscamos la clave, obtenemos una lista de strings, pero si
modificamos la lista de strings, eso no afecta al shelf. Si queremos actualizar el shelf, tenemos que
leer el valor antiguo, actualizarlo y luego escribir el nuevo valor de vuelta en el shelf.
anagram_list = db[key]
anagram_list.append(word)
db[key] = anagram_list
Como ejercicio, puedes terminar este ejemplo leyendo la lista de palabras y almacenando todos los
anagramas en un shelf.
[Link]()
13.6. Comprobar archivos equivalentes
Ahora volvamos al objetivo de este capítulo: buscar archivos diferentes que contienen los mismos
datos. Una forma de comprobarlo es leer el contenido de ambos archivos y compararlo.
Si los archivos contienen imágenes, tenemos que abrirlos con el modo 'rb' , donde 'r' significa
que queremos leer el contenido y 'b' indica modo binario. En modo binario, el contenido no se
interpreta como texto – se trata como una secuencia de bytes.
Aquí tienes un ejemplo que abre y lee un archivo de imagen.
path1 = 'photos/jan-2023/[Link]'
data1 = open(path1, 'rb').read()
type(data1)
El resultado de read es un objeto bytes – como sugiere el nombre, contiene una secuencia de
bytes.
En general, el contenido de un archivo de imagen no es legible para humanos. Pero si leemos el
contenido de un segundo archivo, podemos usar el operador == para comparar.
path2 = 'photos/jan-2023/[Link]'
data2 = open(path2, 'rb').read()
data1 == data2
Si solo tenemos dos archivos, esta función es una buena opción. Pero supón que tenemos un gran
número de archivos y queremos saber si dos cualesquiera contienen los mismos datos. Sería
ineficiente comparar cada par de archivos.
Una alternativa es usar una función hash, que toma el contenido de un archivo y calcula un digest,
que normalmente es un entero grande. Si dos archivos contienen los mismos datos, tendrán el
mismo digest. Si dos archivos son diferentes, casi siempre tendrán digests diferentes.
El módulo hashlib proporciona varias hash funciones – la que usaremos se llama md5 .
Empezaremos usando hashlib.md5 para crear un objeto HASH .
import hashlib
md5_hash = hashlib.md5()
type(md5_hash)
El objeto HASH proporciona un método update que toma el contenido del archivo como
argumento.
md5_hash.update(data1)
Ahora podemos usar hexdigest para obtener el digest como una string de dígitos hexadecimales
que representan un entero en base 16.
digest = md5_hash.hexdigest()
digest
Si hacemos hash del contenido de un archivo diferente, podemos confirmar que obtenemos un
digest distinto.
filename2 = 'photos/feb-2023/[Link]'
md5_digest(filename2)
Ahora tenemos casi todo lo que necesitamos para encontrar archivos equivalentes. El último paso
es buscar en un directorio y encontrar todos los archivos de imagen.
if [Link](path):
print(path)
elif [Link](path):
walk(path)
13.8. Depuración
Cuando lees y escribes archivos, puedes encontrarte con problemas relacionados con espacios en
blanco. Estos errores pueden ser difíciles de depurar porque los caracteres de espacio en blanco
normalmente son invisibles. Por ejemplo, aquí tienes una string que contiene espacios, un tab
representado por la secuencia \t y una nueva línea representada por la secuencia \n . Cuando la
imprimimos, no vemos los caracteres de espacio en blanco.
s = '1 2\t 3\n 4'
print(s)
La función integrada repr puede ayudar. Toma cualquier objeto como argumento y devuelve una
representación en string del objeto. Para strings, representa los caracteres de espacio en blanco
con secuencias de barra invertida.
print(repr(s))
13.9. Glosario
efímero: Un programa efímero normalmente se ejecuta durante poco tiempo y, cuando termina,
sus datos se pierden.
persistente: Un programa persistente se ejecuta indefinidamente y mantiene al menos parte de
sus datos en almacenamiento permanente.
directorio: Una colección de archivos y otros directorios.
directorio de trabajo actual: El directorio por defecto usado por un programa a menos que se
especifique otro directorio.
ruta: Una string que especifica una secuencia de directorios, que a menudo conduce a un archivo.
ruta relativa: Un ruta que empieza desde el directorio de trabajo actual, o desde algún otro
directorio especificado.
ruta absoluta: Un ruta que no depende del directorio actual.
f-string: Una string que tiene la letra f antes de la comilla de apertura y contiene una o más
expresiones entre llaves.
datos de configuración: Datos, a menudo almacenados en un archivo, que especifican qué debe
hacer un programa y cómo.
serialización: Convertir un objeto en una string.
deserialización: Convertir una string en un objeto.
base de datos: Un archivo cuyo contenido está organizado para realizar ciertas operaciones de
manera eficiente.
almacenes clave-valor: Una base de datos cuyo contenido está organizado como un diccionario
con claves que corresponden a valores.
modo binario: Una forma de abrir un archivo para que el contenido se interprete como una
secuencia de bytes en lugar de como una secuencia de caracteres.
función hash: Una función que toma un objeto y calcula un entero, que a veces se llama digest.
digest: El resultado de una función hash, especialmente cuando se usa para comprobar si dos
objetos son iguales.
13.10. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
13.10.2. Ejercicio
Escribe una función llamada replace_all que tome como argumentos una string patrón, una
string de reemplazo y dos nombres de archivo. Debería leer el primer archivo y escribir el contenido
en el segundo archivo (creándolo si es necesario). Si la string patrón aparece en cualquier parte del
contenido, debería reemplazarse por la string de reemplazo.
Aquí tienes un esquema de la función para empezar.
def replace_all(old, new, source_path, dest_path):
# read the contents of the source file
reader = open(source_path)
Para probar tu función, lee el archivo photos/[Link] , reemplaza 'photos' por 'images' y
escribe el resultado en el archivo photos/new_notes.txt .
source_path = 'photos/[Link]'
open(source_path).read()
dest_path = 'photos/new_notes.txt'
old = 'photos'
new = 'images'
replace_all(old, new, source_path, dest_path)
open(dest_path).read()
13.10.3. Ejercicio
En una sección anterior, usamos el módulo shelve para crear un almacén clave-valor que mapea
una string ordenada de letras a una lista de anagramas. Para terminar el ejemplo, escribe una
función llamada add_word que tome como argumentos una string y un objeto shelf.
Debería ordenar las letras de la palabra para crear una clave, y luego comprobar si la clave ya está
en el shelf. Si no, debería crear una lista que contenga la nueva palabra y añadirla al shelf. Si sí,
debería añadir la nueva palabra al valor existente.
Puedes usar este bucle para probar tu función.
download('[Link]
word_list = open('[Link]').read().split()
db = [Link]('anagram_map', 'n')
for word in word_list:
add_word(word, db)
Si todo funciona, deberías poder buscar una clave como 'opst' y obtener una lista de palabras
que se pueden formar con esas letras.
db['opst']
13.10.4. Ejercicio
En una colección grande de archivos, puede haber más de una copia del mismo archivo,
almacenada en distintos directorios o con distintos nombres de archivo. El objetivo de este
ejercicio es buscar duplicados. Como ejemplo, trabajaremos con archivos de imagen en el
directorio photos .
Así es como funcionará:
Usaremos la función walk de section_walking_directories para buscar en este directorio
archivos que terminen con una de las extensiones en config['extensions'] .
Para cada archivo, usaremos md5_digest de section_md5_digest para calcular un digest
del contenido.
Usando un shelf, haremos un mapeo de cada digest a una lista de rutas con ese digest.
Finalmente, buscaremos en el shelf cualquier digest que mapee a múltiples archivos.
Si encontramos alguno, usaremos same_contents para confirmar que los archivos contienen
los mismos datos.
Voy a sugerir algunas funciones para escribir primero, y luego lo juntaremos todo.
1. Para identificar archivos de imagen, escribe una función llamada is_image que tome una ruta
y una lista de extensiones de archivo, y devuelva True si la ruta termina con una de las
extensiones de la lista. Pista: usa [Link] – o pide a un asistente virtual que
escriba esta función por ti.
Puedes usar doctest para probar tu función.
from doctest import run_docstring_examples
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(is_image)
2. Escribe una función llamada add_path que tome como argumentos una ruta y un shelf.
Debería usar md5_digest para calcular un digest del contenido del archivo. Luego debería
actualizar el shelf, ya sea creando un nuevo elemento que mapee el digest a una lista que
contenga la ruta, o añadiendo la ruta a la lista si ya existe.
3. Escribe una versión de walk llamada walk_images que tome un directorio y recorra los
archivos del directorio y sus subdirectorios. Para cada archivo, debería usar is_image para
comprobar si es un archivo de imagen y add_path para añadirlo al shelf.
Cuando todo funcione, puedes usar el siguiente programa para crear el shelf, buscar en el
directorio photos y añadir rutas al shelf, y luego comprobar si hay múltiples archivos con el mismo
digest.
db = [Link]('photos/digests', 'n')
walk_images('photos')
Deberías encontrar un par de archivos que tienen el mismo digest. Usa same_contents para
comprobar si contienen los mismos datos.
Think Python: 3rd Edition
Copyright 2024 Allen B. Downey
Código license: MIT License
Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Traducción al español por midudev (Miguel Ángel Durán).
Puedes pedir las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
La cabecera indica que la nueva clase se llama Time . El cuerpo es un docstring que explica para
qué sirve la clase. Definir una clase crea un objeto de clase.
El objeto de clase es como una fábrica para crear objetos. Para crear un objeto Time , llamas a
Time como si fuera una función.
lunch = Time()
El resultado es un objeto nuevo cuyo tipo es __main__.Time , donde __main__ es el nombre del
módulo donde se define Time .
type(lunch)
Cuando imprimes un objeto, Python te dice de qué tipo es y dónde está almacenado en memoria
(el prefijo 0x significa que el número siguiente está en hexadecimal).
print(lunch)
Este ejemplo crea atributos llamados hour , minute y second , que contienen las horas, minutos
y segundos de la hora 11:59:01 , que es la hora de comer por lo que a mí respecta.
El siguiente diagrama muestra el estado de lunch y sus atributos después de estas asignaciones.
from diagram import make_frame, make_binding
La variable lunch se refiere a un objeto Time , que contiene tres atributos. Cada atributo se
refiere a un entero. Un diagrama de estado como este – que muestra un objeto y sus atributos – se
llama diagrama de objeto.
Puedes leer el valor de un atributo usando el operador de punto.
[Link]
Pero observa que el ejemplo anterior no está en el formato estándar. Para arreglarlo, tenemos que
imprimir los atributos minute y second con un cero inicial. Podemos hacerlo extendiendo las
expresiones entre llaves con un especificador de formato. En el siguiente ejemplo, los
especificadores de formato indican que minute y second deberían mostrarse con al menos dos
dígitos y un cero inicial si hace falta.
f'{[Link]}:{[Link]:02d}:{[Link]:02d}'
Usaremos esta f-string para escribir una función que muestra el valor de un objeto Time . Puedes
pasar un objeto como argumento de la forma habitual. Por ejemplo, la siguiente función toma un
objeto Time como argumento.
def print_time(time):
s = f'{[Link]:02d}:{[Link]:02d}:{[Link]:02d}'
print(s)
Para encontrar la hora de fin, podemos modificar los atributos del objeto Time , añadiendo la
duración de la película.
[Link] += 1
[Link] += 32
print_time(start)
El siguiente stack diagram muestra el estado del programa justo antes de que increment_time
modifique el objeto.
from diagram import Frame, Binding, Value, Stack
Dentro de la función, time es un alias de start , así que cuando se modifica time , cambia
start .
Esta función funciona, pero después de ejecutarse nos quedamos con una variable llamada start
que se refiere a un objeto que representa la hora de fin, y ya no tenemos un objeto que represente
la hora de inicio. Sería mejor dejar start sin cambios y crear un objeto nuevo para representar la
hora de fin. Podemos hacerlo copiando start y modificando la copia.
14.5. Copiar
El módulo copy proporciona una función llamada copy que puede duplicar cualquier objeto.
Podemos importarla así.
from copy import copy
Para ver cómo funciona, empecemos con un objeto Time nuevo que representa la hora de inicio
de la película.
start = make_time(9, 20, 0)
Podrías esperar que == produzca True porque los objetos contienen los mismos datos. Pero para
clases definidas por el programador, el comportamiento por defecto del operador == es el mismo
que el del operador is – comprueba identidad, no equivalencia.
14.6. Funciones puras
Podemos usar copy para escribir funciones puras que no modifican sus parámetros. Por ejemplo,
aquí tienes una función que toma un objeto Time y una duración en horas, minutos y segundos.
Hace una copia del objeto original, usa increment_time para modificar la copia y la devuelve.
def add_time(time, hours, minutes, seconds):
total = copy(time)
increment_time(total, hours, minutes, seconds)
return total
Así la usamos.
end = add_time(start, 1, 32, 0)
print_time(end)
El valor de retorno es un objeto nuevo que representa la hora de fin de la película. Y podemos
confirmar que start no ha cambiado.
print_time(start)
add_time es una función pura porque no modifica ninguno de los objetos que se le pasan como
argumentos y su único efecto es devolver un valor.
Cualquier cosa que se pueda hacer con funciones impuras también se puede hacer con funciones
puras. De hecho, algunos lenguajes de programación solo permiten funciones puras. Los
programas que usan funciones puras pueden ser menos propensos a errores, pero las funciones
impuras a veces son convenientes y pueden ser más eficientes.
En general, te sugiero que escribas funciones puras siempre que sea razonable y recurras a
funciones impuras solo si hay una ventaja convincente. Este enfoque podría llamarse un estilo de
programación funcional.
El resultado no es una hora válida. El problema es que increment_time no trata los casos en los
que el número de segundos o minutos suma más de 60 .
Aquí tienes una versión mejorada que comprueba si second supera o iguala 60 – si es así,
incrementa minute – y luego comprueba si minute supera o iguala 60 – si es así, incrementa
hour .
Arreglar increment_time también arregla add_time , que la usa. Así que ahora el ejemplo anterior
funciona correctamente.
end = add_time(start, 1, 32, 0)
print_time(end)
Pero esta función todavía no es correcta, porque los argumentos podrían ser mayores que 60 . Por
ejemplo, supón que recibimos la duración como 92 minutos, en lugar de 1 hora y 32 minutos.
Podríamos llamar a add_time así.
end = add_time(start, 0, 92, 0)
print_time(end)
El resultado no es una hora válida. Así que probemos un enfoque distinto, usando la función
divmod . Haremos una copia de start y la modificaremos incrementando el atributo minute .
end = copy(start)
[Link] = [Link] + 92
[Link]
Ahora minute es 132 , que son 2 horas y 12 minutos. Podemos usar divmod para dividir por
60 y devolver el número de horas completas y el número de minutos sobrantes.
El resultado es una hora válida. Podemos hacer lo mismo con hour y second , y encapsular todo
el proceso en una función.
def increment_time(time, hours, minutes, seconds):
[Link] += hours
[Link] += minutes
[Link] += seconds
Esta sección demuestra un plan de desarrollo de programas que llamo prototype and patch.
Empezamos con un prototipo simple que funcionaba correctamente para el primer ejemplo. Luego
lo probamos con ejemplos más difíciles – cuando encontramos un error, modificamos el programa
para corregirlo, como poner un parche en un neumático pinchado.
Este enfoque puede ser efectivo, especialmente si todavía no tienes una comprensión profunda del
problema. Pero las correcciones incrementales pueden generar código innecesariamente
complicado – porque trata muchos casos especiales – y poco fiable – porque es difícil saber si has
encontrado todos los errores.
El resultado es el número de segundos desde el comienzo del día. Por ejemplo, 01:01:01 es 1
hora, 1 minuto y 1 segundo desde el comienzo del día, que es la suma de 3600 segundos, 60
segundos y 1 segundo.
time = make_time(1, 1, 1)
print_time(time)
time_to_int(time)
Y aquí tienes una función que va en la otra dirección – convierte un entero en un objeto Time –
usando la función divmod .
def int_to_time(seconds):
minute, second = divmod(seconds, 60)
hour, minute = divmod(minute, 60)
return make_time(hour, minute, second)
Usando estas funciones, podemos escribir una versión más concisa de add_time .
def add_time(time, hours, minutes, seconds):
duration = make_time(hours, minutes, seconds)
seconds = time_to_int(time) + time_to_int(duration)
return int_to_time(seconds)
La primera línea convierte los argumentos en un objeto Time llamado duration . La segunda línea
convierte time y duration a segundos y los suma. La tercera línea convierte la suma en un
objeto Time y lo devuelve.
Así funciona.
start = make_time(9, 40, 0)
end = add_time(start, 1, 32, 0)
print_time(end)
En cierto sentido, convertir de base 60 a base 10 y de vuelta es más difícil que simplemente tratar
con horas. La conversión de base es más abstracta; nuestra intuición para trabajar con valores de
tiempo es mejor.
Pero si tenemos la idea de tratar las horas como números en base 60 – e invertimos el esfuerzo de
escribir las funciones de conversión time_to_int e int_to_time – obtenemos un programa más
corto, más fácil de leer y depurar, y más fiable.
También es más fácil añadir características más adelante. Por ejemplo, imagina restar dos objetos
Time para encontrar la duración entre ellos. El enfoque ingenuo es implementar la resta con
préstamo. Usar las funciones de conversión es más fácil y tiene más probabilidades de ser
correcto.
Irónicamente, a veces hacer un problema más difícil – o más general – lo hace más fácil, porque
hay menos casos especiales y menos oportunidades de error.
14.9. Depuración
Python proporciona varias funciones integradas que son útiles para probar y depurar programas
que trabajan con objetos. Por ejemplo, si no estás seguro de qué tipo es un objeto, puedes
preguntarlo.
type(start)
También puedes usar isinstance para comprobar si un objeto es una instancia de una clase
concreta.
isinstance(end, Time)
Si no estás seguro de si un objeto tiene un atributo concreto, puedes usar la función integrada
hasattr .
hasattr(start, 'hour')
Para obtener todos los atributos, y sus valores, en un diccionario, puedes usar vars .
vars(start)
El módulo structshape , que vimos en el Capítulo 11, también funciona con tipos definidos por el
programador.
download('[Link]
from structshape import structshape
t = start, end
structshape(t)
14.10. Glosario
programación orientada a objetos: Un estilo de programación que usa objetos para organizar
código y datos.
clase: Un tipo definido por el programador. Una definición de clase crea un nuevo objeto de clase.
objeto de clase: Un objeto que representa una clase – es el resultado de una definición de clase.
instanciación: El proceso de crear un objeto que pertenece a una clase.
instancia: Un objeto que pertenece a una clase.
atributo: Una variable asociada con un objeto, también llamada variable de instancia.
diagrama de objeto: Una representación gráfica de un objeto, sus atributos y sus valores.
especificador de formato: En una f-string, un especificador de formato determina cómo se
convierte un valor en una string.
función pura: Una función que no modifica sus parámetros ni tiene ningún efecto aparte de
devolver un valor.
estilo de programación funcional: Una forma de programar que usa funciones puras siempre que
sea posible.
prototype and patch: Una forma de desarrollar programas empezando con un borrador
aproximado y añadiendo características y corrigiendo bugs gradualmente.
design-first development: Una forma de desarrollar programas con una planificación más
cuidadosa que prototype and patch.
14.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
14.11.2. Ejercicio
Escribe una función llamada subtract_time que tome dos objetos Time y devuelva el intervalo
entre ellos en segundos – asumiendo que son dos horas del mismo día.
Aquí tienes un esquema de la función para empezar.
def subtract_time(t1, t2):
"""Compute the difference between two times in seconds.
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(subtract_time)
14.11.3. Ejercicio
Escribe una función llamada is_after que tome dos objetos Time y devuelva True si la primera
hora es más tarde en el día que la segunda, y False en caso contrario.
Aquí tienes un esquema de la función para empezar.
def is_after(t1, t2):
"""Checks whether `t1` is after `t2`.
14.11.4. Ejercicio
Aquí tienes una definición de una clase Date que representa una fecha – es decir, un año, mes y
día del mes.
class Date:
"""Represents a year, month, and day"""
1. Escribe una función llamada make_date que tome year , month y day como parámetros,
cree un objeto Date , asigne los parámetros a atributos y devuelva el objeto nuevo resultante.
Crea un objeto que represente el 22 de junio de 1933.
2. Escribe una función llamada print_date que tome un objeto Date , use una f-string para
formatear los atributos e imprima el resultado. Si la pruebas con la Date que creaste, el
resultado debería ser 1933-06-22 .
3. Escribe una función llamada is_after que tome dos objetos Date como parámetros y
devuelva True si la primera viene después de la segunda. Crea un segundo objeto que
represente el 17 de septiembre de 1933 y comprueba si viene después del primer objeto.
Pista: puede resultarte útil escribir una función llamada date_to_tuple que tome un objeto Date
y devuelva una tupla que contenga sus atributos en orden año, mes, día.
Puedes usar este esquema de función para empezar.
def make_date(year, month, day):
return None
class Time:
"""Represents the time of day."""
def print_time(time):
s = f'{[Link]:02d}:{[Link]:02d}:{[Link]:02d}'
print(s)
Para convertir print_time en un método, todo lo que tenemos que hacer es mover la definición
de la función dentro de la definición de la clase. Fíjate en el cambio de indentación.
Al mismo tiempo, cambiaremos el nombre del parámetro de time a self . Este cambio no es
necesario, pero es convencional que el primer parámetro de un método se llame self .
class Time:
"""Represents the time of day."""
def print_time(self):
s = f'{[Link]:02d}:{[Link]:02d}:{[Link]:02d}'
print(s)
Para llamar a este método, tienes que pasar un objeto Time como argumento. Aquí está la función
que usaremos para crear un objeto Time .
def make_time(hour, minute, second):
time = Time()
[Link] = hour
[Link] = minute
[Link] = second
return time
Ahora hay dos formas de llamar a print_time . La primera (y menos común) es usar sintaxis de
función.
Time.print_time(start)
En esta versión, Time es el nombre de la clase, print_time es el nombre del método, y start
se pasa como parámetro. La segunda forma (y más idiomática) es usar sintaxis de método:
start.print_time()
En esta versión, start es el objeto sobre el que se invoca el método, que se llama receptor,
basándonos en la analogía de que invocar un método es como enviar un mensaje a un objeto.
Independientemente de la sintaxis, el comportamiento del método es el mismo. El receptor se
asigna al primer parámetro, así que dentro del método, self se refiere al mismo objeto que
start .
def time_to_int(self):
minutes = [Link] * 60 + [Link]
seconds = minutes * 60 + [Link]
return seconds
La primera línea usa el comando especial add_method_to , que añade un método a una clase
definida previamente. Este comando funciona en un Jupyter notebook, pero no forma parte de
Python, así que no funcionará en otros entornos. Normalmente, todos los métodos de una clase
están dentro de la definición de la clase, así que se definen al mismo tiempo que la clase. Pero para
este libro, es útil definir un método cada vez.
Como en el ejemplo anterior, la definición del método está indentada y el nombre del parámetro es
self . Aparte de eso, el método es idéntico a la función. Así es como lo invocamos.
start.time_to_int()
Es común decir que “llamamos” a una función e “invocamos” un método, pero significan lo mismo.
Esta función toma seconds como parámetro y devuelve un nuevo objeto Time . Si la
transformamos en un método de la clase Time , tenemos que invocarla sobre un objeto Time .
Pero si estamos intentando crear un nuevo objeto Time , ¿sobre qué se supone que debemos
invocarla?
Podemos resolver este problema del huevo y la gallina usando un método estático, que es un
método que no requiere una instancia de la clase para invocarse. Así es como reescribimos esta
función como un método estático.
%%add_method_to Time
def int_to_time(seconds):
minute, second = divmod(seconds, 60)
hour, minute = divmod(minute, 60)
return make_time(hour, minute, second)
Como es un método estático, no tiene self como parámetro. Para invocarlo, usamos Time , que
es el objeto de clase.
start = Time.int_to_time(34800)
Ahora que tenemos Time.int_to_time , podemos usarlo para escribir add_time como un
método. Aquí está la función del capítulo anterior.
def add_time(time, hours, minutes, seconds):
duration = make_time(hours, minutes, seconds)
seconds = time_to_int(time) + time_to_int(duration)
return int_to_time(seconds)
add_time tiene self como parámetro porque no es un método estático. Es un método ordinario
– también llamado método de instancia. Para invocarlo, necesitamos una instancia de Time .
end = start.add_time(1, 32, 0)
end.print_time()
Como estamos comparando dos objetos, y el primer parámetro es self , llamaremos al segundo
parámetro other . Para usar este método, tenemos que invocarlo sobre un objeto y pasar el otro
como argumento.
end.is_after(start)
Una cosa buena de esta sintaxis es que casi se lee como una pregunta: “¿ end va después de
start ?”
def __str__(self):
s = f'{[Link]:02d}:{[Link]:02d}:{[Link]:02d}'
return s
Este método es similar a print_time , del capítulo anterior, salvo que devuelve el string en lugar
de imprimirlo.
Puedes invocar este método de la forma habitual.
end.__str__()
Pero Python también puede invocarlo por ti. Si usas la función integrada str para convertir un
objeto Time a string, Python usa el método __str__ de la clase Time .
str(end)
Los métodos como __str__ se llaman métodos especiales. Puedes identificarlos porque sus
nombres empiezan y terminan con dos guiones bajos.
En este ejemplo, los parámetros son opcionales, así que si llamas a Time sin argumentos, obtienes
los valores por defecto.
time = Time()
print(time)
Están pasando muchas cosas cuando ejecutamos estas tres líneas de código:
Cuando instanciamos un objeto Time , se invoca el método __init__ .
Cuando usamos el operador + con un objeto Time , se invoca su método __add__ .
Y cuando imprimimos un objeto Time , se invoca su método __str__ .
Cambiar el comportamiento de un operador para que funcione con tipos definidos por el
programador se llama sobrecarga de operadores. Para cada operador, como + , hay un método
especial correspondiente, como __add__ .
15.8. Depuración
Un objeto Time es válido si los valores de minute y second están entre 0 y 60 – incluyendo 0
pero no 60 – y si hour es positivo. Además, hour y minute deberían ser valores enteros, pero
podríamos permitir que second tenga una parte fraccionaria. Requisitos como estos se llaman
invariantes porque siempre deberían ser verdaderos. Dicho de otra forma, si no son verdaderos,
algo ha ido mal.
Escribir código para comprobar invariantes puede ayudar a detectar errores y encontrar sus
causas. Por ejemplo, podrías tener un método como is_valid que toma un objeto Time y
devuelve False si viola una invariante.
%%add_method_to Time
def is_valid(self):
if [Link] < 0 or [Link] < 0 or [Link] < 0:
return False
if [Link] >= 60 or [Link] >= 60:
return False
if not isinstance([Link], int):
return False
if not isinstance([Link], int):
return False
return True
Entonces, al principio de cada método puedes comprobar los argumentos para asegurarte de que
son válidos.
%%add_method_to Time
La sentencia assert evalúa la expresión que le sigue. Si el resultado es True , no hace nada; si el
resultado es False , causa un AssertionError . Aquí hay un ejemplo.
duration = Time(minute=132)
print(duration)
start.is_after(duration)
Las sentencias assert son útiles porque distinguen el código que trata con condiciones normales
del código que comprueba errores.
15.9. Glosario
lenguaje orientado a objetos: Un lenguaje que proporciona funcionalidades para dar soporte a la
programación orientada a objetos, especialmente tipos definidos por el usuario.
método: Una función que se define dentro de una definición de clase y se invoca sobre instancias
de esa clase.
receptor (receiver): El objeto sobre el que se invoca un método.
método estático: Un método que puede invocarse sin un objeto como receptor.
método de instancia: Un método que debe invocarse con un objeto como receptor.
método especial: Un método que cambia la forma en que los operadores y algunas funciones
funcionan con un objeto.
sobrecarga de operadores: El proceso de usar métodos especiales para cambiar la forma en que
los operadores funcionan con tipos definidos por el usuario.
invariante: Una condición que siempre debería ser verdadera durante la ejecución de un
programa.
15.10. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
15.10.2. Ejercicio
En el capítulo anterior, una serie de ejercicios te pidió escribir una clase Date y varias funciones
que trabajan con objetos Date . Ahora practiquemos reescribir esas funciones como métodos.
1. Escribe una definición para una clase Date que represente una fecha – es decir, un año, un
mes y un día del mes.
2. Escribe un método __init__ que tome year , month y day como parámetros y asigne los
parámetros a atributos. Crea un objeto que represente el 22 de junio de 1933.
3. Escribe un método __str__ que use un f-string para formatear los atributos y devolver el
resultado. Si lo pruebas con el Date que creaste, el resultado debería ser 1933-06-22 .
4. Escribe un método llamado is_after que tome dos objetos Date y devuelva True si el
primero viene después del segundo. Crea un segundo objeto que represente el 17 de
septiembre de 1933 y comprueba si viene después del primer objeto.
Pista: Podría resultarte útil escribir un método llamado to_tuple que devuelva una tupla que
contiene los atributos de un objeto Date en orden año-mes-día.
Puedes usar estos ejemplos para probar tu solución.
birthday1 = Date(1933, 6, 22)
print(birthday1)
Usaré estas clases para demostrar temas de OOP, incluida la identidad y equivalencia de objetos,
las copias superficiales y profundas, y el polimorfismo.
def __str__(self):
return f'Point({self.x}, {self.y})'
El método __init__ toma las coordenadas como parámetros y las asigna a los atributos x e y .
El método __str__ devuelve una representación en string del Point .
Ahora podemos instanciar y mostrar un objeto Point así.
start = Point(0, 0)
print(start)
d1 = vars(start)
frame = make_frame(d1, name='Point', dy=-0.25, offsetx=0.18)
binding = make_binding('start', frame)
Esta función traslada el Point de una ubicación del plano a otra. Si no queremos modificar un
Point existente, podemos usar copy para copiar el objeto original y luego modificar la copia.
end1 = copy(start)
[Link](300, 0)
print(end1)
De la misma manera que el método sort modifica una lista y la función sorted crea una lista
nueva, ahora tenemos un método translate que modifica un Point y un método translated
que crea uno nuevo.
Aquí hay un ejemplo:
end2 = [Link](0, 150)
print(end2)
En la siguiente sección, usaremos estos puntos para definir y dibujar una línea.
16.2. Crear una Line
Ahora definamos una clase que representa el segmento de línea entre dos puntos. Como de
costumbre, empezaremos con un método __init__ y un método __str__ .
class Line:
def __init__(self, p1, p2):
self.p1 = p1
self.p2 = p2
def __str__(self):
return f'Line({self.p1}, {self.p2})'
Con esos dos métodos, podemos instanciar y mostrar un objeto Line que usaremos para
representar el eje x .
line1 = Line(start, end1)
print(line1)
Cuando llamamos a print y pasamos line como parámetro, print invoca __str__ sobre
line . El método __str__ usa un f-string para crear una representación en string de la line .
El f-string contiene dos expresiones entre llaves, self.p1 y self.p2 . Cuando se evalúan esas
expresiones, los resultados son objetos Point . Luego, cuando se convierten a strings, se invoca el
método __str__ de la clase Point .
Por eso, cuando mostramos una Line , el resultado contiene las representaciones en string de los
objetos Point .
El siguiente diagrama de objeto muestra el estado de este objeto Line .
from diagram import Binding, Value, Frame
d1 = vars(line1.p1)
frame1 = make_frame(d1, name='Point', dy=-0.25, offsetx=0.17)
d2 = vars(line1.p2)
frame2 = make_frame(d2, name='Point', dy=-0.25, offsetx=0.17)
Las representaciones en string y los diagramas de objeto son útiles para depurar, ¡pero el objetivo
de este ejemplo es generar gráficos, no texto! Así que usaremos el module jupyturtle para
dibujar líneas en la pantalla.
Como hicimos en el Capítulo 4, usaremos make_turtle para crear un objeto Turtle y un canvas
pequeño donde pueda dibujar. Para dibujar líneas, usaremos dos funciones nuevas del module
jupyturtle :
jumpto , que toma dos coordenadas y mueve la Turtle a la ubicación dada sin dibujar una
línea, y
moveto , que mueve la Turtle desde su ubicación actual a la ubicación dada, y dibuja un
segmento de línea entre ellas.
Así es como las importamos.
from jupyturtle import make_turtle, jumpto, moveto
def draw(self):
jumpto(self.p1.x, self.p1.y)
moveto(self.p2.x, self.p2.y)
Para mostrar cómo se usa, crearé una segunda línea que representa el eje y .
line2 = Line(start, end2)
print(line2)
A medida que definamos y dibujemos más objetos, usaremos estas líneas otra vez. Pero primero
hablemos de equivalencia e identidad de objetos.
Si usamos el operador == para compararlos, obtenemos el comportamiento por defecto para tipos
definidos por el programador – el resultado es True solo si son el mismo objeto, y no lo son.
p1 == p2
Esta definición considera que dos Points son iguales si sus atributos son iguales. Ahora, cuando
usamos el operador == , invoca el método __eq__ , que indica que p1 y p2 se consideran
iguales.
p1 == p2
No es posible sobrescribir el operador is – siempre comprueba si los objetos son idénticos. Pero
para tipos definidos por el programador, puedes sobrescribir el operador == para que compruebe
si los objetos son equivalentes. Y puedes definir qué significa equivalente.
def __str__(self):
return f'Rectangle({[Link]}, {[Link]}, {[Link]})'
Como de costumbre, el método __init__ asigna los parámetros a atributos y __str__ devuelve
una representación en string del objeto. Ahora podemos instanciar un objeto Rectangle , usando
un Point como ubicación de la esquina superior izquierda.
corner = Point(30, 20)
box1 = Rectangle(100, 50, corner)
print(box1)
d2 = dict(width=[Link], height=[Link])
frame = make_frame(d2, name='Rectangle', dy=-0.25, offsetx=0.45)
binding = Binding(Value('corner'), frame1, dx=0.92, draw_value=False, **options
[Link](binding)
Para dibujar un rectángulo, usaremos el siguiente método para crear cuatro objetos Point que
representen las esquinas.
%%add_method_to Rectangle
def make_points(self):
p1 = [Link]
p2 = [Link]([Link], 0)
p3 = [Link](0, [Link])
p4 = [Link](-[Link], 0)
return p1, p2, p3, p4
def make_lines(self):
p1, p2, p3, p4 = self.make_points()
return Line(p1, p2), Line(p2, p3), Line(p3, p4), Line(p4, p1)
def draw(self):
lines = self.make_lines()
for line in lines:
[Link]()
Aquí hay un ejemplo que demuestra el efecto creando una copia de box1 e invocando grow sobre
la copia.
box2 = copy(box1)
[Link](60, 40)
print(box2)
Si dibujamos box1 y box2 , podemos confirmar que grow funciona como se esperaba.
make_turtle()
[Link]()
[Link]()
[Link]()
[Link]()
Ahora veamos qué pasa con translate . Toma dos números, dx y dy , y mueve el rectángulo las
distancias dadas en las direcciones x e y .
%%add_method_to Rectangle
Parece que ambos rectángulos se movieron, ¡que no era lo que pretendíamos! La siguiente sección
explica qué salió mal.
Lo que hace copy se llama copia superficial porque copia el objeto pero no los objetos que
contiene. Como resultado, cambiar el width o height de un Rectangle no afecta al otro, ¡pero
cambiar los atributos del Point compartido afecta a ambos! Este comportamiento es confuso y
propenso a errores.
Afortunadamente, el module copy proporciona otra función, llamada deepcopy , que copia no solo
el objeto sino también los objetos a los que se refiere, y los objetos a los que ellos se refieren, y así
sucesivamente. Esta operación se llama copia profunda.
Para demostrarlo, empecemos con un nuevo Rectangle que contiene un nuevo Point .
corner = Point(20, 20)
box3 = Rectangle(100, 50, corner)
print(box3)
box4 = deepcopy(box3)
Podemos confirmar que los dos objetos Rectangle se refieren a objetos Point diferentes.
[Link] is [Link]
Como box3 y box4 son objetos completamente separados, podemos modificar uno sin afectar al
otro. Para demostrarlo, moveremos box3 y haremos crecer box4 .
[Link](50, 30)
[Link](100, 60)
16.7. Polimorfismo
En el ejemplo anterior, invocamos el método draw sobre dos objetos Line y dos objetos
Rectangle . Podemos hacer lo mismo de forma más concisa creando una lista de objetos.
Los elementos de esta lista son de tipos diferentes, pero todos proporcionan un método draw , así
que podemos recorrer la lista e invocar draw sobre cada uno.
make_turtle()
La primera y segunda vez a través del bucle, shape se refiere a un objeto Line , así que cuando
se invoca draw , el método que se ejecuta es el definido en la clase Line .
La tercera y cuarta vez a través del bucle, shape se refiere a un objeto Rectangle , así que
cuando se invoca draw , el método que se ejecuta es el definido en la clase Rectangle .
En cierto sentido, cada objeto sabe cómo dibujarse a sí mismo. Esta característica se llama
polimorfismo. La palabra viene de raíces griegas que significan “muchas formas”. En
programación orientada a objetos, el polimorfismo es la capacidad de diferentes tipos para
proporcionar los mismos métodos, lo que hace posible realizar muchos cálculos – como dibujar
formas – invocando el mismo método sobre objetos de tipos diferentes.
Como ejercicio al final de este capítulo, definirás una nueva clase que representa un círculo y
proporciona un método draw . Luego puedes usar polimorfismo para dibujar líneas, rectángulos y
círculos.
16.8. Depuración
En este capítulo, nos encontramos con un error sutil que ocurrió porque creamos un Point que
era compartido por dos objetos Rectangle , y luego modificamos el Point . En general, hay dos
formas de evitar problemas como este: puedes evitar compartir objetos o puedes evitar
modificarlos.
Para evitar compartir objetos, puedes usar copia profunda, como hicimos en este capítulo.
Para evitar modificar objetos, considera reemplazar funciones impuras como translate por
funciones puras como translated . Por ejemplo, aquí hay una versión de translated que crea
un nuevo Point y nunca modifica sus atributos.
def translated(self, dx=0, dy=0):
x = self.x + dx
y = self.y + dy
return Point(x, y)
Python proporciona funcionalidades que hacen más fácil evitar modificar objetos. Están fuera del
alcance de este libro, pero si tienes curiosidad, pregunta a un asistente virtual: “¿Cómo hago que
un objeto de Python sea inmutable?”
Crear un objeto nuevo toma más tiempo que modificar uno existente, pero la diferencia rara vez
importa en la práctica. Los programas que evitan objetos compartidos y funciones impuras suelen
ser más fáciles de desarrollar, probar y depurar – y la mejor depuración es el que no tienes que
hacer.
16.9. Glosario
copia superficial: Una operación de copia que no copia objetos anidados.
copia profunda: Una operación de copia que también copia objetos anidados.
polimorfismo: La capacidad de un método u operador para funcionar con múltiples tipos de
objetos.
16.10. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
16.10.2. Ejercicio
Escribe un método __eq__ para la clase Line que devuelva True si los objetos Line se refieren
a objetos Point que son equivalentes, en cualquier orden.
Puedes usar el siguiente esquema para empezar.
%%add_method_to Line
Este ejemplo debería ser True porque los objetos Line se refieren a objetos Point que son
equivalentes, en el mismo orden.
line_a = Line(start1, end)
line_b = Line(start2, end)
line_a == line_b # should be True
Este ejemplo debería ser True porque los objetos Line se refieren a objetos Point que son
equivalentes, en orden inverso.
line_c = Line(end, start1)
line_a == line_c # should be True
La equivalencia siempre debería ser transitiva – es decir, si line_a y line_b son equivalentes, y
line_a y line_c son equivalentes, entonces line_b y line_c también deberían ser
equivalentes.
line_b == line_c # should be True
Este ejemplo debería ser False porque los objetos Line se refieren a objetos Point que no son
equivalentes.
line_d = Line(start1, start2)
line_a == line_d # should be False
16.10.3. Ejercicio
Escribe un método de Line llamado midpoint que calcule el punto medio de un segmento de
línea y devuelva el resultado como un objeto Point .
Puedes usar el siguiente esquema para empezar.
%%add_method_to Line
def midpoint(self):
return Point(0, 0)
Puedes usar los siguientes ejemplos para probar tu código y dibujar el resultado.
start = Point(0, 0)
end1 = Point(300, 0)
end2 = Point(0, 150)
line1 = Line(start, end1)
line2 = Line(start, end2)
mid1 = [Link]()
print(mid1)
mid2 = [Link]()
print(mid2)
make_turtle()
16.10.4. Ejercicio
Escribe un método de Rectangle llamado midpoint que encuentre el punto en el centro de un
rectángulo y devuelva el resultado como un objeto Point .
Puedes usar el siguiente esquema para empezar.
%%add_method_to Rectangle
def midpoint(self):
return Point(0, 0)
Puedes usar el siguiente ejemplo para probar tu código.
corner = Point(30, 20)
rectangle = Rectangle(100, 80, corner)
mid = [Link]()
print(mid)
make_turtle()
16.10.5. Ejercicio
Escribe un método de Rectangle llamado make_cross que:
1. Use make_lines para obtener una lista de objetos Line que representan los cuatro lados del
rectángulo.
2. Calcule los puntos medios de las cuatro líneas.
3. Cree y devuelva una lista de dos objetos Line que representan líneas que conectan puntos
medios opuestos, formando una cruz por el centro del rectángulo.
Puedes usar este esquema para empezar.
%%add_method_to Rectangle
def make_diagonals(self):
return []
make_turtle()
[Link]()
for line in lines:
[Link]()
16.10.6. Ejercicio
Escribe una definición para una clase llamada Circle con atributos center y radius , donde
center es un objeto Point y radius es un número. Incluye los métodos especiales __init__ y
__str__ , y un método llamado draw que use funciones de jupyturtle para dibujar el círculo.
Puedes usar la siguiente función, que es una versión de la función circle que escribimos en el
Capítulo 4.
from jupyturtle import make_turtle, forward, left, right
import math
def draw_circle(radius):
circumference = 2 * [Link] * radius
n = 30
length = circumference / n
angle = 360 / n
left(angle / 2)
for i in range(n):
forward(length)
left(angle)
Puedes usar el siguiente ejemplo para probar tu código. Empezaremos con un Rectangle
cuadrado con width y height 100 .
corner = Point(20, 20)
rectangle = Rectangle(100, 100, corner)
El siguiente código debería crear un Circle que encaje dentro del cuadrado.
center = [Link]()
radius = [Link] / 2
Si todo funcionó correctamente, el siguiente código debería dibujar el círculo dentro del cuadrado
(tocando los cuatro lados).
make_turtle(delay=0.01)
[Link]()
[Link]()
17. Herencia
La característica del lenguaje que más a menudo se asocia con la programación orientada a
objetos es la herencia. La herencia es la capacidad de definir una nueva clase que es una versión
modificada de una clase existente. En este capítulo demuestro la herencia usando clases que
representan cartas de juego, barajas de cartas y manos de póker. Si no juegas al póker, no te
preocupes: te diré lo que necesitas saber.
Rango Código
Jack 11
Queen 12
King 13
Y podemos usar 1 o 14 para representar un Ace, dependiendo de si queremos que se considere
más bajo o más alto que los demás rangos.
Para representar estas codificaciones, usaremos dos listas de strings, una con los nombres de los
palos y otra con los nombres de los rangos.
Aquí tienes una definición de una clase que representa una carta, con estas listas de strings como
variables de clase, que son variables definidas dentro de una definición de clase, pero no dentro
de un método.
class Card:
"""Represents a standard playing card."""
El primer elemento de rank_names es None porque no hay ninguna carta con rango cero. Al
incluir None como marcador de posición, obtenemos una lista con la práctica propiedad de que el
índice 2 se corresponde con el string '2' , y así sucesivamente.
Las variables de clase están asociadas con la clase, en lugar de con una instancia de la clase, así
que podemos acceder a ellas de esta forma.
Card.suit_names
Pero si usas la clase, queda más claro que son variables de clase, no atributos.
def __str__(self):
rank_name = Card.rank_names[[Link]]
suit_name = Card.suit_names[[Link]]
return f'{rank_name} of {suit_name}'
Cuando imprimimos una Card , Python llama al método __str__ para obtener una representación
de la carta legible para humanos.
print(queen)
El siguiente es un diagrama del objeto de clase Card y de la instancia de Carta. Card es un objeto
de clase, así que su tipo es type . queen es una instancia de Card , así que su tipo es Card . Para
ahorrar espacio, no dibujé el contenido de suit_names y rank_names .
from diagram import Binding, Value, Frame, Stack
value = make_list([])
bbox2 = [Link](ax, x+1.66, y)
value = make_list([])
bbox3 = [Link](ax, x+1.66, y-0.5)
Cada instancia de Card tiene sus propios atributos suit y rank , pero solo hay un objeto de
clase Card y una sola copia de las variables de clase suit_names y rank_names .
No lo hacen, así que devuelve False . Podemos cambiar este comportamiento definiendo el
special método __eq__ .
%%add_method_to Card
__eq__ toma dos objetos Card como parámetros y devuelve True si tienen el mismo palo y
rango, incluso si no son el mismo objeto. En otras palabras, comprueba si son equivalentes, aunque
no sean idénticos.
Cuando usamos el operador == con objetos Card , Python llama al método __eq__ .
queen == queen2
Como segunda prueba, vamos a crear una carta con el mismo palo y un rango diferente.
six = Card(1, 6)
print(six)
queen != six
Ahora supongamos que queremos comparar dos cartas para ver cuál es mayor. Si usamos uno de
los operadores relacionales, obtenemos un TypeError .
queen < queen2
Para cambiar el comportamiento del operador < , podemos definir un special método llamado
__lt__ , abreviatura de “less than”. Para este ejemplo, supongamos que el palo es más importante
que el rango: por eso todos los Spades superan a todos los Hearts, que superan a todos los
Diamonds, y así sucesivamente. Si dos cartas tienen el mismo palo, gana la que tiene el rango más
alto.
Para implementar esta lógica, usaremos el siguiente método, que devuelve una tupla que contiene
el palo y el rango de una carta, en ese orden.
%%add_method_to Card
def to_tuple(self):
return ([Link], [Link])
La comparación de tuplas compara los primeros elementos de cada tupla, que representan los
palos. Si son iguales, compara los segundos elementos, que representan los rangos.
Ahora, si usamos el operador < , invoca el método __lt__ .
six < queen
Si usamos el operador > , invoca un special método llamado __gt__ , si existe. De lo contrario,
invoca __lt__ con los argumentos en el orden opuesto.
queen < queen2
Por último, si usamos el operador <= , invoca un special método llamado __le__ .
%%add_method_to Card
Así que podemos comprobar si una carta es menor o igual que otra.
queen <= queen2
Si usamos el operador >= , usa __ge__ si existe. De lo contrario, invoca __le__ con los
argumentos en el orden opuesto.
queen >= six
Tal como los hemos definido, estos métodos son completos en el sentido de que podemos
comparar dos objetos Card cualesquiera, y consistentes en el sentido de que los resultados de
distintos operadores no se contradicen entre sí. Con estas dos propiedades, podemos decir que
los objetos Card están totalmente ordenados. Y eso significa, como veremos pronto, que se
pueden ordenar.
17.5. Barajas
Ahora que tenemos objetos que representan cartas, definamos objetos que representen barajas. La
siguiente es una definición de clase para Deck con un método __init__ que toma una lista de
objetos Card como parámetro y la asigna a un atributo llamado cards .
class Deck:
Para crear una lista que contiene las 52 cartas de una baraja estándar, usaremos el siguiente
método estático.
%%add_method_to Deck
def make_cards():
cards = []
for suit in range(4):
for rank in range(2, 15):
card = Card(suit, rank)
[Link](card)
return cards
En make_cards , el bucle externo enumera los palos de 0 a 3 . El bucle interno enumera los
rangos de 2 a 14 , donde 14 representa un Ace que supera a un King. Cada iteración crea una
nueva Card con el palo y el rango actuales, y la añade a cards .
Así es como creamos una lista de cartas y un objeto Deck que la contiene.
cards = Deck.make_cards()
deck = Deck(cards)
len([Link])
def __str__(self):
res = []
for card in [Link]:
[Link](str(card))
return '\n'.join(res)
Este método demuestra una forma eficiente de acumular un string grande: construir una lista de
strings y luego usar el método de string join .
Probaremos este método con una baraja que solo contiene dos cartas.
small_deck = Deck([queen, six])
Cuando Jupyter muestra un string, enseña la forma “representacional” del string, que representa
un salto de línea con la secuencia \n .
Sin embargo, si imprimimos el resultado, Jupyter muestra la forma “imprimible” del string, que
imprime el salto de línea como espacio en blanco.
print(small_deck)
def take_card(self):
return [Link]()
Para barajar la baraja, podemos usar la función shuffle del module random :
import random
[Link](3)
%%add_method_to Deck
def shuffle(self):
[Link]([Link])
Si barajamos la baraja e imprimimos las primeras cartas, podemos ver que no están en ningún
orden aparente.
[Link]()
for card in [Link][:4]:
print(card)
Para ordenar las cartas, podemos usar el método de lista sort , que ordena los elementos “in
place”; es decir, modifica la lista en lugar de crear una lista nueva.
%%add_method_to Deck
def sort(self):
[Link]()
Si imprimimos las primeras cartas, podemos confirmar que están en orden creciente.
for card in [Link][:4]:
print(card)
En este ejemplo, [Link] no hace nada más que invocar [Link] . Pasar la responsabilidad
de esta forma se llama delegación.
Esta definición indica que Hand hereda de Deck , lo que significa que los objetos Hand pueden
acceder a métodos definidos en Deck , como take_card y put_card .
Hand también hereda __init__ de Deck , pero si definimos __init__ en la clase Hand ,
sobrescribe el de la clase Deck .
%%add_method_to Hand
Esta versión de __init__ toma un string opcional como parámetro y siempre empieza con una
lista vacía de cartas. Cuando creamos una Hand , Python invoca este método, no el de Deck , lo
que podemos confirmar comprobando que el resultado tiene un atributo label .
hand = Hand('player 1')
[Link]
Para repartir una carta, podemos usar take_card para quitar una carta de un Deck , y put_card
para añadir la carta a una Hand .
deck = Deck(cards)
card = deck.take_card()
hand.put_card(card)
print(hand)
Este método es polimórfico; es decir, funciona con más de un tipo: self y other pueden ser
tanto una Hand como un Deck . Así que podemos usar este método para repartir una carta de
Deck a Hand , de una Hand a otra, o de una Hand de vuelta a un Deck .
Cuando una nueva clase hereda de una existente, la existente se llama padre y la nueva clase se
llama hija. En general:
Las instancias de la clase hija deberían tener todos los atributos de la clase padre, pero
pueden tener atributos adicionales.
La clase hija debería tener todos los métodos de la clase padre, pero puede tener métodos
adicionales.
Si una clase hija sobrescribe un método de la clase padre, el nuevo método debería tomar los
mismos parámetros y devolver un resultado compatible.
Este conjunto de reglas se llama “principio de sustitución de Liskov”, en honor a la científica de la
computación Barbara Liskov.
Si sigues estas reglas, cualquier función o método diseñado para trabajar con una instancia de una
clase padre, como un Deck , también funcionará con instancias de una clase hija, como Hand . Si
rompes estas reglas, tu código se derrumbará como un castillo de naipes (lo siento).
17.9. Especialización
Hagamos una clase llamada BridgeHand que representa una mano en bridge, un juego de cartas
muy popular. Heredaremos de Hand y añadiremos un nuevo método llamado
high_card_point_count que evalúa una mano usando un método de “high carta point”, que suma
puntos por las cartas altas de la mano.
Aquí tienes una definición de clase que contiene, como variable de clase, un diccionario que asigna
nombres de cartas a sus valores de puntos.
class BridgeHand(Hand):
"""Represents a bridge hand."""
hcp_dict = {
'Ace': 4,
'King': 3,
'Queen': 2,
'Jack': 1,
}
Dado el rango de una carta, como 12 , podemos usar Card.rank_names para obtener la
representación en string del rango, y luego usar hcp_dict para obtener su puntuación.
rank = 12
rank_name = Card.rank_names[rank]
score = BridgeHand.hcp_dict.get(rank_name, 0)
rank_name, score
El siguiente método recorre las cartas de una BridgeHand y suma sus puntuaciones.
%%add_method_to BridgeHand
def high_card_point_count(self):
count = 0
for card in [Link]:
rank_name = Card.rank_names[[Link]]
count += BridgeHand.hcp_dict.get(rank_name, 0)
return count
# This cell makes a fresh Deck and
# initializes the random number generator
cards = Deck.make_cards()
deck = Deck(cards)
[Link](3)
Para probarlo, repartiremos una mano con cinco cartas; una mano de bridge normalmente tiene
trece, pero es más fácil probar código con ejemplos pequeños.
hand = BridgeHand('player 2')
[Link]()
deck.move_cards(hand, 5)
print(hand)
BridgeHand hereda las variables y métodos de Hand y añade una variable de clase y un método
que son específicos de bridge. Esta forma de usar la herencia se llama especialización porque
define una nueva clase especializada para un uso particular, como jugar al bridge.
17.10. Depuración
La herencia es una característica útil. Algunos programas que serían repetitivos sin herencia se
pueden escribir de forma más concisa con ella. Además, la herencia puede facilitar la reutilización
de código, ya que puedes personalizar el comportamiento de una clase padre sin tener que
modificarla. En algunos casos, la estructura de herencia refleja la estructura natural del problema,
lo que hace que el diseño sea más fácil de entender.
Por otro lado, la herencia puede hacer que los programas sean difíciles de leer. Cuando se invoca
un método, a veces no está claro dónde encontrar su definición: el código relevante puede estar
repartido entre varios modules.
Siempre que tengas dudas sobre el flujo de ejecución de tu programa, la solución más sencilla es
añadir sentencias print al principio de los métodos relevantes. Si [Link] imprime un
mensaje que dice algo como Running [Link] , entonces, a medida que el programa se
ejecuta, traza el flujo de ejecución.
Como alternativa, podrías usar la siguiente función, que toma un objeto y el nombre de un método
(como string) y devuelve la clase que proporciona la definición del método.
def find_defining_class(obj, method_name):
"""Find the class where the given method is defined."""
for typ in type(obj).mro():
if method_name in vars(typ):
return typ
return f'Method {method_name} not found.'
find_defining_class usa el método mro para obtener la lista de objetos clase (tipos) que se
buscarán para encontrar métodos. “MRO” significa “método resolution order”, que es la secuencia
de clases que Python busca para “resolver” el nombre de un método; es decir, para encontrar el
objeto función al que se refiere el nombre.
Como ejemplo, vamos a instanciar una BridgeHand y luego encontrar la clase que define
shuffle .
17.11. Glosario
herencia (herencia): La capacidad de definir una nueva clase que es una versión modificada de
una clase definida previamente.
codificar (encode): Representar un conjunto de valores usando otro conjunto de valores
construyendo una correspondencia entre ellos.
variable de clase: Una variable definida dentro de una definición de clase, pero no dentro de
ningún método.
totalmente ordenado: Un conjunto de objetos está totalmente ordenado si podemos comparar
dos elementos cualesquiera y los resultados son consistentes.
delegación: Cuando un método pasa la responsabilidad a otro método para que haga la mayor
parte o todo el trabajo.
clase padre: Una clase de la que se hereda.
clase hija: Una clase que hereda de otra clase.
especialización: Una forma de usar la herencia para crear una nueva clase que es una versión
especializada de una clase existente.
17.12. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
Como ejemplo, considera esta trick, donde el primer jugador empieza con el 3 of Diamonds, lo que
significa que Diamonds es el “led suit”. El segundo y el tercer jugador “follow suit”, lo que significa
que juegan una carta del palo que se ha iniciado. El cuarto jugador juega una carta de un palo
diferente, lo que significa que no puede ganar la trick. Así que el ganador de esta trick es el tercer
jugador, porque jugó la carta más alta del led suit.
cards = [Card(1, 3),
Card(1, 10),
Card(1, 12),
Card(2, 13)]
trick = Trick(cards)
print(trick)
Escribe un método de Trick llamado find_winner que recorra las cartas de la Trick y
devuelva el índice de la carta ganadora. En el ejemplo anterior, el índice de la carta ganadora es 2 .
Puedes usar el siguiente esquema para empezar.
%%add_method_to Trick
def find_winner(self):
return 0
Si pruebas tu método con el ejemplo anterior, el índice de la carta ganadora debería ser 2 .
trick.find_winner()
17.12.3. Ejercicio
Los siguientes ejercicios te piden escribir funciones que clasifiquen manos de póker. Si no estás
familiarizado con el póker, explicaré lo que necesitas saber. Usaremos la siguiente clase para
representar manos de póker.
class PokerHand(Hand):
"""Represents a poker hand."""
def get_suit_counts(self):
counter = {}
for card in [Link]:
key = [Link]
counter[key] = [Link](key, 0) + 1
return counter
def get_rank_counts(self):
counter = {}
for card in [Link]:
key = [Link]
counter[key] = [Link](key, 0) + 1
return counter
def has_flush(self):
"""Checks whether this hand has a flush."""
return False
Para probar este método, construiremos una mano con cinco cartas que son todas Clubs, así que
contiene un flush.
good_hand = PokerHand('good_hand')
suit = 0
for rank in range(10, 15):
card = Card(suit, rank)
good_hand.put_card(card)
print(good_hand)
Como segunda prueba, construiremos una mano con tres Clubs y otros dos palos.
cards = [Card(0, 2),
Card(0, 3),
Card(2, 4),
Card(3, 5),
Card(0, 7),
]
print(bad_hand)
17.12.4. Ejercicio
Escribe un método llamado has_straight que compruebe si una mano contiene una straight, que
es un conjunto de cinco cartas con rangos consecutivos. Por ejemplo, si una mano contiene los
rangos 5 , 6 , 7 , 8 y 9 , contiene una straight.
Un Ace puede ir antes de un dos o después de un King, así que Ace , 2 , 3 , 4 , 5 es una straight,
y también lo es 10 , Jack , Queen , King , Ace . Pero una straight no puede “dar la vuelta”, así que
King , Ace , 2 , 3 , 4 no es una straight.
Puedes usar el siguiente esquema para empezar. Incluye unas pocas líneas de código que cuentan
el número de Aces, representados con el código 1 o 14 , y almacenan el total en ambas
ubicaciones del contador.
%%add_method_to PokerHand
return False
good_hand , que creamos para el ejercicio anterior, contiene una straight. Si usamos
get_rank_counts , podemos confirmar que tiene al menos una carta de cada uno de cinco rangos
consecutivos.
good_hand.get_rank_counts()
bad_hand no contiene una straight, así que has_straight debería devolver False .
bad_hand.has_straight()
17.12.5. Ejercicio
Una mano tiene una straight flush si contiene un conjunto de cinco cartas que son a la vez una
straight y un flush; es decir, cinco cartas del mismo palo con rangos consecutivos. Escribe un
método de PokerHand que compruebe si una mano tiene una straight flush.
Puedes usar el siguiente esquema para empezar.
%%add_method_to PokerHand
def has_straightflush(self):
"""Check whether this hand has a straight flush."""
return False
straight_and_flush = deepcopy(bad_hand)
straight_and_flush.put_card(Card(0, 6))
straight_and_flush.put_card(Card(0, 9))
print(straight_and_flush)
Esta mano contiene una straight y un flush, pero no son las mismas cinco cartas.
straight_and_flush.has_straight(), straight_and_flush.has_flush()
17.12.6. Ejercicio
Una mano de póker tiene una pareja si contiene dos o más cartas con el mismo rango. Escribe un
método de PokerHand que compruebe si una mano contiene una pareja.
Puedes usar el siguiente esquema para empezar.
%%add_method_to PokerHand
Para probar tu método, aquí tienes una mano que tiene una pareja.
pair = deepcopy(bad_hand)
pair.put_card(Card(1, 2))
print(pair)
17.12.7. Ejercicio
Una mano tiene un full house si contiene tres cartas de un rango y dos cartas de otro rango.
Escribe un método de PokerHand que compruebe si una mano tiene un full house.
Puedes usar el siguiente esquema para empezar.
%%add_method_to PokerHand
def has_full_house(self):
return False
17.12.8. Ejercicio
Este ejercicio es una advertencia sobre un error común que puede ser difícil de depurar. Considera
la siguiente definición de clase.
class Kangaroo:
"""A Kangaroo is a marsupial."""
name: string
contents: initial pouch contents.
"""
[Link] = name
[Link] = contents
def __str__(self):
"""Return a string representaion of this Kangaroo.
"""
t = [ [Link] + ' has pouch contents:' ]
for obj in [Link]:
s = ' ' + object.__str__(obj)
[Link](s)
return '\n'.join(t)
Ahora veamos cómo funciona esta clase. Crearemos dos objetos Kangaroo con los nombres
'Kanga' y 'Roo' .
kanga = Kangaroo('Kanga')
roo = Kangaroo('Roo')
¡La bolsa de Roo contiene el mismo contenido que la de Kanga, incluida una referencia a roo !
A ver si puedes averiguar qué salió mal. Luego pregunta a un asistente virtual: “¿Qué está mal en el
siguiente programa?” y pega la definición de Kangaroo .
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Traducción al español por midudev (Miguel Ángel Durán).
Puedes comprar las versiones impresa y ebook de Think Python 3e en [Link] y Amazon.
Aquí tienes versiones de las clases Card , Deck y Hand del Capítulo 17, que usaremos en algunos
ejemplos de este capítulo.
class Card:
suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
'8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']
def __str__(self):
rank_name = Card.rank_names[[Link]]
suit_name = Card.suit_names[[Link]]
return f'{rank_name} of {suit_name}'
import random
class Deck:
def __init__(self, cards):
[Link] = cards
def __str__(self):
res = []
for card in [Link]:
[Link](str(card))
return '\n'.join(res)
def make_cards():
cards = []
for suit in range(4):
for rank in range(2, 15):
card = Card(suit, rank)
[Link](card)
return cards
def shuffle(self):
[Link]([Link])
def pop_card(self):
return [Link]()
class Hand(Deck):
def __init__(self, label=''):
[Link] = label
[Link] = []
18. Extras de Python
Uno de mis objetivos con este libro ha sido enseñarte la menor cantidad posible de Python. Cuando
había dos maneras de hacer algo, elegí una y evité mencionar la otra. O a veces puse la segunda en
un ejercicio.
Ahora quiero volver a algunas de las partes buenas que quedaron fuera. Python ofrece varias
características que no son realmente necesarias – puedes escribir buen código sin ellas – pero con
ellas puedes escribir código más conciso, legible o eficiente, y a veces las tres cosas.
18.1. Conjuntos
Python proporciona una clase llamada set que representa una colección de elementos únicos.
Para crear un conjunto vacío, podemos usar el objeto de clase como una función.
s1 = set()
s1
Un elemento solo puede aparecer una vez en un set . Si añades un elemento que ya está, no tiene
ningún efecto.
[Link]('a')
s1
O, si creas un conjunto con una secuencia que contiene duplicados, el resultado contiene solo
elementos únicos.
set('banana')
Algunos de los ejercicios de este libro se pueden hacer de forma concisa y eficiente con conjuntos.
Por ejemplo, aquí tienes una solución a un ejercicio del Capítulo 11 que usa un diccionario para
comprobar si hay elementos duplicados en una secuencia.
def has_duplicates(t):
d = {}
for x in t:
d[x] = True
return len(d) < len(t)
Esta versión añade los elementos de t como claves en un diccionario, y luego comprueba si hay
menos claves que elementos. Usando conjuntos, podemos escribir la misma función así.
def has_duplicates(t):
s = set(t)
return len(s) < len(t)
has_duplicates('abba')
Un elemento solo puede aparecer en un conjunto una vez, así que si un elemento de t aparece
más de una vez, el conjunto será más pequeño que t . Si no hay duplicados, el conjunto tendrá el
mismo tamaño que t .
Los objeto set proporcionan métodos que realizan operaciones de conjuntos. Por ejemplo,
union calcula la unión de dos conjuntos, que es un nuevo conjunto que contiene todos los
elementos que aparecen en cualquiera de los dos conjuntos.
[Link](s2)
Algunos operadores aritméticos funcionan con conjuntos. Por ejemplo, el operador - realiza la
resta de conjuntos – el resultado es un nuevo conjunto que contiene todos los elementos del
primer conjunto que no están en el segundo conjunto.
s1 - s2
En el Capítulo 12 usamos diccionarios para encontrar las palabras que aparecen en un documento
pero no en una lista de palabras. Usamos la siguiente función, que recibe dos diccionarios y
devuelve un nuevo diccionario que contiene solo las claves del primero que no aparecen en el
segundo.
def subtract(d1, d2):
res = {}
for key in d1:
if key not in d2:
res[key] = d1[key]
return res
Con conjuntos, no tenemos que escribir esta función nosotros mismos. Si word_counter es un
diccionario que contiene las palabras únicas del documento y word_list es una lista de palabras
válidas, podemos calcular la diferencia de conjuntos así.
# this cell creates a small example so we can run the following
# cell without loading the actual data
word_counter = {'word': 1}
word_list = ['word']
set(word_counter) - set(word_list)
El resultado es un conjunto que contiene las palabras del documento que no aparecen en la lista de
palabras.
Los operadores relacionales funcionan con conjuntos. Por ejemplo, <= comprueba si un conjunto
es un subconjunto de otro, incluyendo la posibilidad de que sean iguales.
set('ab') <= set('abc')
Con estos operadores, podemos usar conjuntos para hacer algunos de los ejercicios del Capítulo 7.
Por ejemplo, aquí tienes una versión de uses_only que usa un bucle.
def uses_only(word, available):
for letter in word:
if letter not in available:
return False
return True
uses_only comprueba si todas las letras de word están en available . Con conjuntos, podemos
reescribirla así.
def uses_only(word, available):
return set(word) <= set(available)
Si las letras de word son un subconjunto de las letras de available , eso significa que word usa
solo letras de available .
18.2. Counters
Un Counter es como un conjunto, excepto que si un elemento aparece más de una vez, el
Counter lleva la cuenta de cuántas veces aparece. Si conoces la idea matemática de un “multiset”,
un Counter es una forma natural de representar un multiset.
La clase Counter está definida en un module llamado collections , así que tienes que
importarla. Después puedes usar el objeto de clase como una función y pasar como argumento un
string, una lista o cualquier otro tipo de secuencia.
from collections import Counter
counter = Counter('banana')
counter
t = (1, 1, 1, 2, 2, 3)
counter = Counter(t)
counter
Un objeto Counter es como un diccionario que asocia cada clave con el número de veces que
aparece. Como en los diccionarios, las claves tienen que ser hashable.
A diferencia de los diccionarios, los objeto Counter no lanzan una excepción si accedes a un
elemento que no aparece. En su lugar, devuelven 0 .
counter['d']
Podemos usar objeto Counter para resolver uno de los ejercicios del Capítulo 10, que pide una
función que reciba dos palabras y compruebe si son anagramas – es decir, si las letras de una se
pueden reordenar para formar la otra.
Aquí tienes una solución usando objeto Counter .
def is_anagram(word1, word2):
return Counter(word1) == Counter(word2)
Si dos palabras son anagramas, contienen las mismas letras con los mismos recuentos, así que sus
objeto Counter son equivalentes.
Counter proporciona un método llamado most_common que devuelve una lista de pares valor-
frecuencia, ordenados de más común a menos común.
counter.most_common()
counter2 = Counter('bans')
counter + counter2
Tendrás la oportunidad de explorar otras operaciones de Counter en los ejercicios al final de este
capítulo.
18.3. defaultdict
El module collections también proporciona defaultdict , que es como un diccionario excepto
que, si accedes a una clave que no existe, genera automáticamente un nuevo valor.
Cuando creas un defaultdict , proporcionas una función que se usa para crear nuevos valores.
Una función que crea objetos a veces se llama fábrica. Las funciones integradas que crean listas,
conjuntos y otros tipos se pueden usar como fábricas.
Por ejemplo, aquí tienes un defaultdict que crea una nueva list cuando hace falta.
from collections import defaultdict
d = defaultdict(list)
d
Fíjate en que el argumento es list , que es un objeto de clase, no list() , que es una llamada a
función que crea una nueva lista. La fábrica función no se llama a menos que accedamos a una
clave que no existe.
t = d['new key']
t
La nueva lista, que llamamos t , también se añade al diccionario. Así que si modificamos t , el
cambio aparece en d :
[Link]('new value')
d['new key']
Si estás creando un diccionario de listas, a menudo puedes escribir código más simple usando
defaultdict .
En uno de los ejercicios del Capítulo 11, hice un diccionario que asocia un string de letras
ordenadas con la lista de palabras que se pueden formar con esas letras. Por ejemplo, el string
'opst' se asocia con la lista ['opts', 'post', 'pots', 'spot', 'stop', 'tops'] . Aquí está
el código original.
def all_anagrams(filename):
d = {}
for line in open(filename):
word = [Link]().lower()
t = signature(word)
if t not in d:
d[t] = [word]
else:
d[t].append(word)
return d
En los ejercicios al final del capítulo, tendrás la oportunidad de practicar usando objeto
defaultdict .
d = defaultdict(list)
key = ('into', 'the')
d[key].append('woods')
d[key]
Esta sentencia comprueba si x es positivo. Si lo es, calcula su logaritmo. Si no, [Link] lanzaría
un ValueError. Para evitar detener el programa, generamos un NaN , que es un valor especial de
punto flotante que representa “Not a Number”.
Podemos escribir esta sentencia de forma más concisa usando una conditional expression.
y = [Link](x) if x > 0 else float('nan')
Casi puedes leer esta línea como si fuera inglés: “ y recibe log- x si x es mayor que 0; de lo
contrario recibe NaN ”.
Las funciones recursivas a veces se pueden escribir de forma concisa usando expresiones
condicionales. Por ejemplo, aquí tienes una versión de factorial con una sentencia condicional.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
Otro uso de las expresiones condicionales es manejar argumentos opcionales. Por ejemplo, aquí
tienes una definición de clase con un método __init__ que usa una sentencia condicional para
comprobar un parámetro con un valor por defecto.
class Kangaroo:
def __init__(self, name, contents=None):
[Link] = name
if contents is None:
contents = []
[Link] = contents
En general, puedes reemplazar una sentencia condicional por una expresión condicional si ambas
ramas contienen una sola expresión y ninguna sentencia.
Podemos dividirlo en una lista de strings, recorrer los strings, capitalizarlos y añadirlos a una lista.
t = []
for word in [Link]():
[Link]([Link]())
' '.join(t)
Podemos hacer lo mismo de forma más concisa usando una comprensión de listas:
t = [[Link]() for word in [Link]()]
' '.join(t)
Los operadores de corchetes indican que estamos construyendo una nueva lista. La expresión
dentro de los corchetes especifica los elementos de la lista, y la cláusula for indica qué secuencia
estamos recorriendo.
La sintaxis de una comprensión de listas puede parecer extraña, porque la variable del bucle –
word en este ejemplo – aparece en la expresión antes de llegar a su definición. Pero te
acostumbras.
Como otro ejemplo, en el Capítulo 9 usamos este bucle para leer palabras de un archivo y añadirlas
a una lista.
download('[Link]
word_list = []
len(word_list)
len(word_list)
Una comprensión de listas también puede tener una cláusula if que determina qué elementos se
incluyen en la lista. Por ejemplo, aquí tienes un bucle for que usamos en el Capítulo 10 para crear
una lista solo con las palabras de word_list que son palíndromos.
def is_palindrome(word):
return list(reversed(word)) == list(word)
palindromes = []
palindromes[:10]
palindromes[:10]
Cuando una comprensión de listas se usa como argumento de una función, a menudo podemos
omitir los corchetes. Por ejemplo, supón que queremos sumar para valores de de 0 a 9.
1/2
n
n
Ese ejemplo no es muy útil porque hace lo mismo que el operador in . Pero podríamos usar any
para escribir soluciones concisas a algunos de los ejercicios del Capítulo 7. Por ejemplo, podemos
escribir uses_none así.
def uses_none(word, forbidden):
"""Checks whether a word avoids forbidden letters."""
return not any(letter in forbidden for letter in word)
uses_none('banana', 'xyz')
uses_none('apple', 'efg')
Esta función recorre las letras de word y comprueba si alguna de ellas está en forbidden . Usar
any con una expresión generadora es eficiente porque se detiene inmediatamente si encuentra un
valor True , así que no tiene que recorrer toda la secuencia.
Python proporciona otra función integrada, all , que devuelve True si todos los elementos de la
secuencia son True . Podemos usarla para escribir una versión concisa de uses_all .
def uses_all(word, required):
"""Check whether a word uses all required letters."""
return all(letter in word for letter in required)
uses_all('banana', 'ban')
uses_all('apple', 'api')
Las expresiones que usan any y all pueden ser concisas, eficientes y fáciles de leer.
class Point:
"""Represents a point in 2-D space."""
def __str__(self):
return f'({self.x}, {self.y})'
Eso es mucho código para transmitir una pequeña cantidad de información. namedtuple
proporciona una forma más concisa de definir clases como esta.
from collections import namedtuple
El primer argumento es el nombre de la clase que quieres crear. El segundo es una lista de los
atributos que deben tener los objeto Point . El resultado es un objeto de clase, por eso se asigna a
un nombre de variable con mayúscula inicial.
Una clase creada con namedtuple proporciona un método __init__ que asigna valores a los
atributos y un __str__ que muestra el objeto de forma legible. Así que podemos crear y mostrar
un objeto Point así.
p = Point(1, 2)
p
Point también proporciona un método __eq__ que comprueba si dos objeto Point son
equivalentes – es decir, si sus atributos son iguales.
p == Point(1, 2)
Puedes acceder a los elementos de una named tupla por nombre o por índice.
p.x, p.y
p[0], p[1]
También puedes tratar una named tupla como una tupla, como en esta asignación.
x, y = p
x, y
Pero los objeto namedtuple son inmutables. Después de inicializar los atributos, no se pueden
cambiar.
p[0] = 3
p.x = 3
namedtuple proporciona una forma rápida de definir clases simples. La desventaja es que las
clases simples no siempre siguen siendo simples. Puede que más adelante decidas que quieres
añadir métodos a una named tupla. En ese caso, puedes definir una nueva clase que herede de la
named tupla.
class Pointier(Point):
"""This class inherits from Point"""
Pero el operador * no empaqueta argumentos de palabra clave. Así que llamar a esta función con
un argumento de palabra clave causa un error.
mean(1, 2, start=3)
El parámetro que empaqueta keywords puede tener cualquier nombre, pero kwargs es una
elección común. El resultado es un diccionario que asocia keywords con valores.
mean(1, 2, start=3)
En este ejemplo, se imprime el valor de kwargs , pero por lo demás no tiene ningún efecto.
Pero el operador ** también se puede usar en una lista de argumentos para desempaquetar un
diccionario. Por ejemplo, aquí tienes una versión de mean que empaqueta cualquier argumento de
palabra clave que recibe y luego los desempaqueta como argumentos de palabra clave para sum .
def mean(*args, **kwargs):
return sum(args, **kwargs) / len(args)
Ahora, si llamamos a mean con start como argumento de palabra clave, se pasa a sum, que lo
usa como punto de partida de la suma. En el siguiente ejemplo start=3 añade 3 a la suma antes
de calcular la media, así que la suma es 6 y el resultado es 3 .
mean(1, 2, start=3)
Como otro ejemplo, si tenemos un diccionario con las claves x e y , podemos usarlo con el
operador de desempaquetado para crear un objeto Point .
d = dict(x=1, y=2)
Point(**d)
Sin el operador de desempaquetado, d se trata como un único argumento posicional, así que se
asigna a x , y obtenemos un TypeError porque no hay un segundo argumento que asignar a y .
d = dict(x=1, y=2)
Point(d)
Cuando trabajas con funciones que tienen un gran número de argumentos de palabra clave, a
menudo es útil crear y pasar diccionarios que especifican opciones usadas con frecuencia.
def pack_and_print(**kwargs):
print(kwargs)
pack_and_print(a=1, b=2)
18.9. Depuración
En capítulos anteriores, usamos doctest para probar funciones. Por ejemplo, aquí tienes una
función llamada add que recibe dos números y devuelve su suma. Incluye un doctest que
comprueba si 2 + 2 es 4 .
def add(a, b):
'''Add two numbers.
>>> add(2, 2)
4
'''
return a + b
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
class TestExample(TestCase):
def test_add(self):
result = add(2, 2)
[Link](result, 4)
Primero importamos TestCase , que es una clase del module unittest . Para usarla, tenemos que
definir una nueva clase que herede de TestCase y proporcione al menos un método de test. El
nombre del método de test debe empezar con test y debería indicar qué función prueba.
En este ejemplo, test_add prueba la función add llamándola, guardando el resultado e invocando
assertEqual , que se hereda de TestCase . assertEqual recibe dos argumentos y comprueba si
son iguales.
Para ejecutar este método de test, tenemos que ejecutar una función de unittest llamada main
y proporcionar varios argumentos de palabra clave. La siguiente función muestra los detalles – si
tienes curiosidad, puedes pedirle a un asistente virtual que explique cómo funciona.
import unittest
def run_unittest():
[Link](argv=[''], verbosity=0, exit=False)
run_unittest no recibe TestExample como argumento – en su lugar, busca clases que heredan
de TestCase . Luego busca métodos que empiezan con test y los ejecuta. Este proceso se llama
test discovery.
Esto es lo que pasa cuando llamamos a run_unittest .
run_unittest()
[Link] informa del número de pruebas que ejecutó y de los resultados. En este caso OK
indica que las pruebas pasaron.
Para ver qué pasa cuando un test falla, añadiremos un método de test incorrecto a TestExample .
%%add_method_to TestExample
def test_add_broken(self):
result = add(2, 2)
[Link](result, 100)
18.10. Glosario
fábrica: Una función usada para crear objetos, que a menudo se pasa como parámetro a una
función.
conditional expression: Una expresión que usa un condicional para seleccionar uno de dos
valores.
comprensión de listas: Una forma concisa de recorrer una secuencia y crear una lista.
expresión generadora: Similar a una comprensión de listas, excepto que no crea una lista.
test discovery: Un proceso usado para encontrar y ejecutar pruebas.
18.11. Ejercicios
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
18.11.2. Ejercicio
Uno de los ejercicios del Capítulo 7 pide una función llamada uses_none que recibe una palabra y
un string de letras prohibidas, y devuelve True si la palabra no usa ninguna de esas letras. Aquí
tienes una solución.
def uses_none(word, forbidden):
for letter in [Link]():
if letter in [Link]():
return False
return True
Escribe una versión de esta función que use operaciones de set en lugar de un bucle for . Pista:
pregunta a un asistente virtual: “¿Cómo calculo la intersección de conjuntos de Python?”
Puedes usar este esquema para empezar.
def uses_none(word, forbidden):
"""Checks whether a word avoid forbidden letters.
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(uses_none)
18.11.3. Ejercicio
Scrabble es un juego de mesa donde el objetivo es usar fichas de letras para formar palabras. Por
ejemplo, si tenemos fichas con las letras T , A , B , L , E , podemos formar BELT y LATE usando
un subconjunto de las fichas – pero no podemos formar BEET porque no tenemos dos E .
Escribe una función que reciba un string de letras y una palabra, y compruebe si las letras pueden
formar la palabra, teniendo en cuenta cuántas veces aparece cada letra.
Puedes usar el siguiente esquema para empezar.
def can_spell(letters, word):
"""Check whether the letters can spell the word.
run_doctests(can_spell)
18.11.4. Ejercicio
En uno de los ejercicios del Capítulo 17, mi solución para has_straightflush usa el siguiente
método, que particiona una PokerHand en una lista de cuatro manos, donde cada mano contiene
cartas del mismo palo.
def partition(self):
"""Make a list of four hands, each containing only one suit."""
hands = []
for i in range(4):
[Link](PokerHand())
return hands
def partition(self):
return {}
for i in range(7):
card = deck.pop_card()
random_hand.add_card(card)
print(random_hand)
Si invocas partition e imprimes los resultados, cada mano debería contener solo cartas de un
palo.
hand_dict = random_hand.partition()
18.11.5. Ejercicio
Aquí tienes la función del Capítulo 11 que calcula números de Fibonacci.
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
Escribe una versión de esta función con una sola sentencia retorno que use dos expresiones
condicionales, una anidada dentro de la otra.
fibonacci(10) # should be 55
18.11.6. Ejercicio
La siguiente es una función que calcula el coeficiente binomial de forma recursiva.
def binomial_coeff(n, k):
"""Compute the binomial coefficient "n choose k".
n: number of trials
k: number of successes
returns: int
"""
if k == 0:
return 1
if n == 0:
return 0
18.11.7. Ejercicio
Aquí tienes el método __str__ de la clase Deck en el Capítulo 17.
%%add_method_to Deck
def __str__(self):
res = []
for card in [Link]:
[Link](str(card))
return '\n'.join(res)
Escribe una versión más concisa de este método con una comprensión de listas o una expresión
generadora.
Puedes usar este ejemplo para probar tu solución.
cards = Deck.make_cards()
deck = Deck(cards)
print(deck)
También sugiere herramientas para probar programas, incluidas la sentencia assert , el módulo
doctest y el módulo unittest . Incluir pruebas en tus programas es una de las mejores formas
de prevenir y detectar errores, y de ahorrar tiempo en depuración.
Pero la mejor depuración es el que no tienes que hacer. Si usas un proceso de desarrollo
incremental como se describe en el Capítulo 6 – y pruebas a medida que avanzas – cometerás
menos errores y los encontrarás más rápido cuando aparezcan. Además, recuerda la
encapsulación y la generalización del Capítulo 4, que son especialmente útiles cuando desarrollas
código en Jupyter notebooks.
A lo largo de este libro, he sugerido formas de usar asistentes virtuales para ayudarte a aprender,
programar y depurar. Espero que estas herramientas te estén resultando útiles.
Además de asistentes virtuales como ChatGPT, quizá también quieras usar una herramienta como
Copilot, que autocompleta código mientras escribes. Al principio no recomendé usar estas
herramientas porque pueden resultar abrumadoras para principiantes. Pero quizá ahora quieras
explorarlas.
Usar herramientas de AI de forma efectiva requiere algo de experimentación y reflexión para
encontrar un flujo que funcione para ti. Si te resulta molesto copiar código de ChatGPT a Jupyter,
quizá prefieras algo como Copilot. Pero el trabajo cognitivo que haces para componer un prompt e
interpretar la respuesta puede ser tan valioso como el código que genera la herramienta, en la
misma línea que la depuración con patito de goma.
A medida que ganes experiencia programando, quizá quieras explorar otros entornos de desarrollo.
Creo que Jupyter notebooks es un buen lugar para empezar, pero son relativamente nuevos y no se
usan tanto como los entornos de desarrollo integrados (IDE) convencionales. Para Python, los IDE
más populares incluyen PyCharm y Spyder – y Thonny, que suele recomendarse para principiantes.
Otros IDE, como Visual Studio Código y Eclipse, también funcionan con otros lenguajes de
programación. O, como alternativa más sencilla, puedes escribir programas en Python usando
cualquier editor de texto que te guste.
Mientras continúas tu viaje en la programación, no tienes que hacerlo en soledad. Si vives en una
ciudad o cerca de una, es muy probable que haya un grupo de usuarios de Python al que puedas
unirte. Estos grupos suelen ser amables con principiantes, así que no tengas miedo. Si no hay
ningún grupo cerca de ti, quizá puedas unirte a eventos de forma remota. Además, mantente
atento a conferencias regionales de Python.
Una de las mejores formas de mejorar tus habilidades de programación es aprender otro lenguaje.
Si te interesan la estadística y ciencia de datos, quizá quieras aprender R. Pero recomiendo
especialmente aprender un lenguaje funcional como Racket o Elixir. La programación funcional
requiere una forma distinta de pensar, lo que cambia la manera en que piensas sobre los
programas.
¡Buena suerte!
Think Python: 3.ª edición
Copyright 2024 Allen B. Downey
Traducción al español por midudev (Miguel Ángel Durán).
Licencia del código: MIT License
Licencia del texto: Creative Commons Atribución-NoComercial-CompartirIgual 4.0 Internacional
Notebooks en blanco
Para cada capítulo hay un notebook en blanco con el texto del libro y la mayor parte del código
eliminado. Estos notebooks son útiles para ejercicios guiados donde los estudiantes completan las
celdas.
Capítulo 1: Programar como una forma de pensar
Abrir el capítulo 1 en Colab
Capítulo 2: Variables y sentencias
Abrir el capítulo 2 en Colab
Capítulo 3: Funciones
Abrir el capítulo 3 en Colab
Capítulo 4: Funciones e interfaces
Abrir el capítulo 4 en Colab
Capítulo 5: Condicionales y recursión
Abrir el capítulo 5 en Colab
Capítulo 6: Valores de retorno
Abrir el capítulo 6 en Colab
Capítulo 7: Iteración y búsqueda
Abrir el capítulo 7 en Colab
Capítulo 8: Cadenas y expresiones regulares
Abrir el capítulo 8 en Colab
Capítulo 9: Listas
Abrir el capítulo 9 en Colab
Capítulo 10: Diccionarios
Abrir el capítulo 10 en Colab
Capítulo 11: Tuplas
Abrir el capítulo 11 en Colab
Capítulo 12: Análisis y generación de texto
Abrir el capítulo 12 en Colab
Capítulo 13: Archivos y bases de datos
Abrir el capítulo 13 en Colab
Capítulo 14: Clases y funciones
Abrir el capítulo 14 en Colab
Capítulo 15: Clases y métodos
Abrir el capítulo 15 en Colab
Capítulo 16: Clases y objetos
Abrir el capítulo 16 en Colab
Capítulo 17: Herencia
Abrir el capítulo 17 en Colab
Capítulo 18: Extras de Python
Abrir el capítulo 18 en Colab
Capítulo 19: Reflexiones finales
Abrir el capítulo 19 en Colab