Think Python 2 - Spanish
Think Python 2 - Spanish
Allen Downey
La forma original de este libro está en código fuente de LATEX. La compilación de esta fuente de
LATEX tiene el efecto de generar una representación de un libro de texto que es independiente del
dispositivo, la cual se puede convertir a otros formatos e imprimir.
Que sea corto: es mejor para los estudiantes leer 10 páginas que no leer 50 páginas.
Tener cuidado con el vocabulario: intenté minimizar la jerga y definir cada término
en su primer uso.
Construir de manera gradual: para evitar trampillas, tomé los temas más difíciles y
los dividí en series de pasos pequeños.
Concentrarse en la programación, no en el lenguaje de programación: incluí el míni-
mo subconjunto útil de Java y excluí el resto.
Necesitaba un título, así que por capricho escogí Aprende a pensar como un informático (How
to Think Like a Computer Scientist).
Mi primera versión fue áspera, pero funcionó. Los estudiantes hicieron la lectura y enten-
dieron lo suficiente como para que yo pudiera ocupar el tiempo de la clase en los temas
difíciles, los temas interesantes y (más importante) dejar a los estudiantes practicar.
Publiqué el libro bajo la Licencia de documentación libre de GNU, permitiendo a los usua-
rios copiar, modificar y distribuir el libro.
Lo que ocurrió después es la parte genial. Jeff Elkner, un profesor de escuela secundaria
en Virginia, adoptó mi libro y lo tradujo a Python. Me envió una copia de su traducción y
tuve la extraña experiencia de aprender Python leyendo mi propio libro. Como Green Tea
Press, publiqué la primera versión en Python en 2001.
VI Capítulo 0. Prefacio
En 2003 comencé a enseñar en el Olin College y tuve que enseñar Python por primera vez.
El contraste con Java fue notable. Los estudiantes se esforzaban menos, aprendían más,
trabajaban en más proyectos interesantes y generalmente se divertían mucho.
El resultado es este libro, ahora con el título menos grandioso Pensar en Python (Think Pyh-
ton). Algunos de los cambios son:
Agregué una sección sobre depuración al final de cada capítulo. Estas secciones pre-
sentan técnicas generales para encontrar y evitar errores de programación y adver-
tencias sobre trampas de Python.
Agregué más ejercicios, que van desde pruebas cortas de comprensión hasta algunos
proyectos sustanciales. La mayoria de los ejercicios incluyen un enlace a mi solución.
Agregué una serie de estudios de caso: ejemplos más largos con ejercicios, soluciones
y discusión.
Agregué unas pocas secciones, y más detalles en la web, para ayudar a los princi-
piantes a empezar a ejecutar Python en un navegador, así que no tienes que lidiar
con la instalación de Python hasta que quieras hacerlo.
Para el Capítulo 4.1 cambié mi propio paquete de gráfica tortuga, llamado Swampy,
por un módulo de Python más estándar, turtle, que es más fácil de instalar y más
poderoso.
Agregué un nuevo capítulo llamado “Trucos extra”, que introduce algunas caracte-
rísticas adicionales de Python que no son estrictamente necesarias, pero a veces son
prácticas.
Espero que disfrutes trabajando con este libro y que te ayude a aprender a programar y a
pensar como un informático, al menos un poco.
Allen B. Downey
Olin College
Agradecimientos
Muchas gracias a Jeff Elkner, quien tradujo mi libro de Java a Python, lo cual comenzó este
proyecto y me presentó lo que ha resultado ser mi lenguaje favorito.
VII
Gracias también a Chris Meyers, quien contribuyó a varias secciones de How to Think Like
a Computer Scientist.
Gracias a los editores de Lulu que trabajaron en How to Think Like a Computer Scientist.
Gracias a todos los estudiantes que trabajaron con las primeras versiones de este libro y a
todos los colaboradores (nombrados a continuación) que enviaron correcciones y sugeren-
cias.
Lista de colaboradores
Más de 100 lectores perspicaces y atentos han enviado sugerencias y correcciones en los
últimos años. Sus contribuciones, y su entusiasmo por este proyecto, han sido una ayuda
enorme.
Si incluyes al menos una parte de la oración en donde aparece el error, eso me facilita la
búsqueda. Los números de página y de sección también me ayudan, pero no es tan fácil
trabajar con estos. ¡Gracias!
Jonah Cohen escribió los scripts de Perl que convierten la fuente de LaTeX de este libro en un
hermoso HTML.
Michael Conlon envió una corrección gramatical en el Capítulo 2 y una mejora de estilo en el
Capítulo 1, e inició la discusión sobre los aspectos técnicos de los intérpretes.
Courtney Gleason y Katherine Smith escribieron horsebet.py, que fue usado como un estudio
de caso en una versión anterior del libro. Su programa puede encontrarse ahora en el sitio web.
Lee Harr envió más correcciones de las que cabrían acá en una lista y, de hecho, debería apa-
recer como uno de los principales editores del texto.
David Mayo advirtió que la palabra “inconsciente” en el Capítulo 1 necesitaba ser cambiada a
“subconsciente”.
Matthew J. Moelter ha sido un colaborador por mucho tiempo que envió numerosas correccio-
nes y sugerencias al libro.
Simon Dicon Montford informó sobre una definición de función faltante y muchos errores
tipográficos en el Capítulo 3. Además, encontró errores en la función increment en el Capítulo
13.
Kevin Parks envió valiosos comentarios y sugerencias en cuanto a cómo mejorar la distribución
del libro.
David Pool envió un error tipográfico en el glosario del Capítulo 1, así como amables palabras
de aliento.
Robin Shaw señaló un error en la Sección 13.1, donde la función printTime se usó en un ejemplo
sin estar definida.
Paul Sleigh encontró un error en el Capítulo 7 y un error en el script de Perl de Jonah Cohen
que genera HTML a partir de LaTeX.
Craig T. Snydal está probando el texto en un curso en la Drew University. Ha aportado muchas
sugerencias y correcciones valiosas.
Ian Thomas y sus alumnos están usando el texto en un curso de programación. Ellos son los
primeros en probar los capítulos de la segunda mitad del libro, y han hecho numerosas correc-
ciones y sugerencicas.
Peter Winstanley nos hizo saber sobre un error en nuestro Latin que estuvo por mucho tiempo
en el Capítulo 3.
Moshe Zadka ha hecho contribuciones invaluables a este proyecto. Además de escribir el pri-
mer borrador del capítulo de Diccionarios, proporcionó constante orientación en las primeras
etapas del libro.
James Mayer nos envió una gran cantidad de errores ortográficos y tipográficos, incluyendo
dos en la lista de colaboradores.
Hayden McAfee encontró una inconsistencia potencialmente confusa entre dos ejemplos.
Tauhidul Hoque y Lex Berezhny crearon las ilustraciones en el Capítulo 1 y mejoraron muchas
de las otras ilustraciones.
Dr. Michele Alzetta encontró un error en el Capítulo 8 y envió algunos comentarios pedagógi-
cos interesantes y sugerencias sobre Fibonacci y Old Maid.
Kalin Harvey sugirió una aclaración en el Capítulo 7 y captó algunos errores tipográficos.
Christopher P. Smith encontró muchos errores tipográficos y nos ayudó a actualizar el libro a
Python 2.2.
Gregor Lingl está enseñando Python en una escuela secundaria en Vienna, Austria. Está traba-
jando en una traducción del libro al alemán y encontró un par de errores malos en el Capítulo
5.
Florin Oprina envió una mejora a makeTime, una corrección a printTime y un buen error tipo-
gráfico.
Ivo Wever encontró un error tipográfico en el Capítulo 5 y sugirió una aclaración en el Capítulo
3.
Ben Logan envió una serie de errores tipográficos y problemas con la traducción del libro a
HTML.
Louis Cordier notó un lugar en el Capítulo 16 donde el código no coincidía con el texto.
Rob Black envió un montón de correcciones, incluyendo algunos cambios para Python 2.2.
Jean-Philippe Rey de la École Centrale Paris envió una serie de parches, incluyendo algunas
actualizaciones para Python 2.2 y otras mejoras para pensar.
Jason Mader en la George Washington University hizo una serie de sugerencias y correcciones
útiles.
Abel David y Alexis Dinno nos recordaron que el plural de “matrix” es “matrices”, no “ma-
trixes”. Este error estuvo en el libro por años, pero dos lectores con las mismas iniciales lo
informaron en el mismo día. Extraño.
Charles Thayer nos animó a deshacernos de los punto y coma que habíamos puesto al final de
algunas sentencias y a limpiar nuestro uso de “argumento” y “parámetro”.
C. Corey Capel vio la palabra que faltaba en el Tercer Teorema de la Depuración y un error
tipográfico en el Capítulo 4.
Daryl Hammond y Sarah Zimmerman advirtieron que mostré a math.pi demasiado pronto. Y
Zim vio un error tipográfico.
Leah Engelbert-Fenton advirtió que usé tuple como un nombre de variable, contrario a mi
propio consejo. Y luego encontró un montón de errores tipográficos y un “use before def”.
Max Hailperin ha enviado una serie de correcciones y sugerencias. Max es uno de los autores
del extraordinario Concrete Abstractions, que tal vez quieras leer cuando termines con este libro.
Eric Pashman envió una serie de correcciones para los Capítulos 4–11.
XI
Ratnakar Tiwari sugirió una nota al pie explicando los triángulos degenerados.
Anurag Goel sugirió otra solución para es_abecedario y envió algunas correcciones adiciona-
les. Y sabe cómo deletrear Jane Austen.
Nam Nguyen encontró un error tipográfico y advirtió que usé el patrón Decorator pero sin
mencionar el nombre.
Eric Bronner advirtió sobre una confusión en la discusión del orden de operaciones.
Will McGinnis advirtió que polilinea fue definida de manera diferente en dos lugares.
Frank Hecker advirtió sobre un ejercicio que estaba subespecificado y algunos enlaces rotos.
Sven Hoexter advirtió que una variable con nombre input le hace sombra a una función incor-
porada.
XII Capítulo 0. Prefacio
Andrea Zanella tradujo el libro al italiano y envió una serie de correcciones en el camino.
Muchas, muchas gracias a Melissa Lewis y Luciano Ramalho por los excelentes comentarios y
sugerencias sobre la segunda edición.
Gracias a Harry Percival de PythonAnywhere por su ayuda al hacer que la gente comience
ejecutando Python en un navegador.
Laurent Rosenfeld y Mihaela Rotaru tradujeron este libro al francés. En el camino, me enviaron
muchas correcciones y sugerencias.
Adicionalmente, las personas que vieron errores tipográficos o hicieron correcciones incluyen
a Czeslaw Czapla, Dale Wilson, Francesco Carlo Cimini, Richard Fursa, Brian McGhie, Lo-
kesh Kumar Makani, Matthew Shultz, Viet Le, Victor Simeone, Lars O.D. Christensen, Swarup
Sahoo, Alix Etienne, Kuang He, Wei Huang, Karen Barber y Eric Ransom.
Índice general
Prefacio V
1.7. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.8. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.9. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.7. Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.8. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.9. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.10. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
XIV Índice general
3. Funciones 17
3.3. Composición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.12. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.13. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.14. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.3. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.4. Encapsulamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.5. Generalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4.7. Refactorización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
4.9. docstring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.10. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.11. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.12. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Índice general XV
5. Condicionales y recursividad 39
5.8. Recursividad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.12. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
5.13. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.14. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6. Funciones productivas 51
6.3. Composición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.6. Salto de fe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.9. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.10. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.11. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
XVI Índice general
7. Iteración 63
7.1. Reasignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.2. Actualizar variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
7.3. La sentencia while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
7.4. break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7.5. Raíces cuadradas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7.6. Algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
7.7. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
7.8. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.9. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8. Cadenas 71
8.1. Una cadena es una secuencia . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.2. len . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.3. Recorrido con un bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.4. Trozos de cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.5. Las cadenas son inmutables . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8.6. Buscar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8.7. Bucles y conteo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8.8. Métodos de cadena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8.9. El operador in . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
8.10. Comparación de cadenas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.11. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.12. Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
8.13. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
10. Listas 91
10.11. Alias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
A. Depuración 197
El objetivo de este libro es enseñarte a pensar como un informático. Esta forma de pensar
combina algunas de las mejores características de las matemáticas, la ingeniería y las cien-
cias naturales. Al igual que los matemáticos, los informáticos utilizan lenguajes formales
para denotar ideas (específicamente, computaciones). Al igual que los ingenieros, diseñan
cosas, ensamblando componentes en sistemas y evaluando compensasiones entre alterna-
tivas. Al igual que los científicos, observan el comportamiento de sistemas complejos, a
partir de hipótesis, y prueban predicciones.
En un nivel, aprenderás a programar, una habilidad útil por sí misma. En otro nivel, usarás
la programación como un medio para un fin. Mientras avancemos, ese fin se volverá más
claro.
Los detalles se ven diferentes en lenguajes diferentes, pero unas pocas instrucciones básicas
aparecen en casi todos los lenguajes:
entrada (input): Obtener datos desde el teclado, un archivo, la red u otro dispositivo.
Los paréntesis indican que print es una función. Llegaremos a las funciones en el Capítu-
lo 3.
En Python 2, la sentencia print es un poco diferente; no es una función, así que no utiliza
paréntesis.
>>> print 'Hola, mundo'
Esta distinción pronto tendrá más sentido, pero eso es suficiente para comenzar.
>>> 6 ^ 2
4
No cubriré operadores bit a bit en este libro, pero puedes leer sobre estos en http://wiki.
python.org/moin/BitwiseOperators.
Los lenguajes formales son lenguajes que están diseñados por personas para aplicaciones
específicas. Por ejemplo, la notación que utilizan los matemáticos es un lenguaje formal que
es particularmente bueno al denotar relaciones entre números y símbolos. Los químicos
utilizan un lenguaje formal para representar la estructura química de las moléculas. Y más
importante:
Los lenguajes de programación son lenguajes formales que han sido diseña-
dos para expresar computaciones.
Los lenguajes formales tienden a tener reglas de sintaxis que gobiernan la estructura de las
sentencias. Por ejemplo, en matemáticas la sentencia 3 + 3 = 6 tiene sintaxis correcta, pero
3+ = 3$6 no la tiene. En química, H2 O es una fórmula sintácticamente correcta, pero 2 Zz
no.
Hay dos tipos de reglas de sintaxis, relacionadas con los tokens y la estructura. Los tokens
son los elementos básicos del lenguaje, tales como palabras, números y elementos quími-
cos. Uno de los problemas con 3+ = 3$6 es que $ no es un token legal en matemáticas (al
menos por lo que sé). Del mismo modo, 2 Zz no es legal porque no hay ningún elemento
con la abreviatura Zz.
El segundo tipo de regla de sintaxis tiene relación con la manera en que los tokens están
combinados. La ecuación 3 + /3 es ilegal porque aunque + y / son tokens legales, no
puedes tener uno justo después del otro. Del mismo modo, en una fórmula química el
subíndice viene después del nombre del elemento, no antes.
Esta es un@ oración en e$paño/ bien estructurada con t*kens inválidos. Esta oración todos
los tokens válidos tiene, pero estructura inválida presenta.
Cuando lees una oración en español o una sentencia en un lenguaje formal, tienes que
descifrar la estructura (aunque en un lenguaje natural lo haces de manera subconsciente).
Este proceso se llama análisis sintáctico (en inglés, parsing).
Aunque los lenguajes formales y los lenguajes naturales tienen muchas características en
común —tokens, estructura y sintaxis— existen algunas diferencias:
ambigüedad: Los lenguajes naturales están llenos de ambigüedad, con la cual las perso-
nas lidian mediante el uso de pistas contextuales y otra información. Los lenguajes
formales están diseñados para ser casi o completamente inequívocos, lo cual signi-
fica que cualquier sentencia tiene exactamente un solo significado, sin importar el
contexto.
literalidad: Los lenguajes naturales están llenos de modismo y metáfora. Si yo digo “Cayó
en la cuenta”, probablemente no hay ninguna cuenta ni nadie cayendo (este modis-
mo significa que alguien entendió algo después de un periodo de confusión). Los
lenguajes formales expresan exactamente lo que dicen.
Debido a que todos crecemos hablando lenguajes naturales, a veces es difícil adaptarse a
los lenguajes formales. La diferencia entre lenguaje formal y lenguaje natural es como la
diferencia entre poesía y prosa, pero más aún:
6 Capítulo 1. El camino del programa
Poesía: Las palabras se utilizan por su sonido tanto como por su significado y todo el
poema junto crea un efecto o respuesta emocional. La ambigüedad no solo es común
sino a menudo deliberada.
Prosa: El significado literal de las palabras es más importante y la estructura aporta más
significado. La prosa es más susceptible de análisis que la poesía pero todavía a me-
nudo ambigua.
Programas: El significado de un programa de computador es inequívoco y literal, y puede
entenderse enteramente analizando los tokens y su estructura.
Los lenguajes formales son más densos que los lenguajes naturales, así que leerlos requiere
más tiempo. Además, la estructura es importante, por lo que no siempre es mejor leer de
arriba a abajo y de izquierda a derecha. En su lugar, hay que aprender a analizar sintácti-
camente el programa en tu cabeza, identificar los tokens e interpretar la estructura. Final-
mente, los detalles importan. Pequeños errores en la ortografía y puntuación, que puedes
cometer con los lenguajes naturales, pueden hacer una gran diferencia en un lenguaje for-
mal.
1.7. Depuración
Los programadores cometen errores. Por razones caprichosas, los errores de programación
se llaman bugs y el proceso de localizarlos se llama depuración (en inglés, debugging).
La programación, y especialmente la depuración, a veces provoca emociones fuertes. Si
estás luchando con un error de programación difícil, podrías sentir ira, desánimo o ver-
güenza.
Hay evidencia de que las personas responden naturalmente a los computadores como si
estos fueran personas. Cuando funcionan bien, pensamos en ellos como compañeros de
equipo, y cuando son obstinados o rudos, respondemos a ellos de la misma manera que
respondemos a las personas rudas y obstinadas (Reeves and Nass, The Media Equation: How
People Treat Computers, Television, and New Media Like Real People and Places).
Prepararse para estas reacciones podría ayudarte a lidiar con ellas. Un enfoque es pensar en
el computador como un empleado con ciertas fortalezas, como la velocidad y la precisión,
y debilidades particulares, como la falta de empatía y la incapacidad para comprender el
panorama general.
Tu trabajo es ser un buen jefe: encontrar maneras de aprovechar las fortalezas y mitigar las
debilidades. Y encontrar maneras de utilizar tus emociones para abordar el problema, sin
dejar que tus reacciones interfieran en tu capacidad de trabajar eficazmente.
Aprender a depurar puede ser frustrante, pero es una habilidad valiosa que es ú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 la depuración. ¡Espero que ayuden!
1.8. Glosario
resolución de problemas: El proceso de formular un problema, encontrar una solución y
expresarla.
1.8. Glosario 7
lenguaje de alto nivel: Un lenguaje de programación como Python que está diseñado para
que los humanos puedan leer y escribir fácilmente.
lenguaje de bajo nivel: Un lenguaje de programación que está diseñado para que sea fácil
de ejecutar por un computador; también llamado “lenguaje de máquina” o “lenguaje
ensamblador”.
prompt: Caracteres mostrados por el intérprete que indican que está listo para recibir la
entrada del usuario.
sentencia print: Una instrucción que hace que el intérprete de Python muestre un valor en
la pantalla.
operador: Un símbolo especial que representa una computación simple como suma, mul-
tiplicación o concatenación de cadenas.
valor: Una de las unidades básicas de datos, como un número o una cadena, que manipula
un programa.
tipo: Una categoría de valores. Los tipos que hemos visto hasta ahora son los enteros (tipo
int), los números de coma flotante (tipo float) y cadenas (tipo str).
lenguaje natural: Cualquiera de los lenguajes que hablan las personas y que evoluciona-
ron de manera natural.
lenguaje formal: Cualquiera de los lenguajes que las personas han diseñado para propó-
sitos específicos, tales como representar ideas matemáticas o programas de compu-
tador; todos los lenguajes de programación son lenguajes formales.
1.9. Ejercicios
Ejercicio 1.1. Es una buena idea leer este libro en frente de un computador para que puedas probar
los ejemplos mientras avanzas.
Cada vez que experimentes con una nueva característica, deberías intentar cometer errores. Por
ejemplo, en el programa “Hola, mundo”, ¿qué ocurre si omites una de las comillas? ¿Y si omites
ambas? ¿Qué ocurre si escribes print de manera incorrecta?
Este tipo de experimento te ayuda a recordar lo que leíste; también te ayuda cuando estás progra-
mando porque logras saber lo que significan los mensajes de error. Es mejor cometer errores ahora y
a propósito que después y de manera accidental.
1. En una sentencia print, ¿qué ocurre si omites uno de los paréntesis, o ambos?
2. Si estás intentando imprimir una cadena con print, ¿qué ocurre si omites una de las comillas,
o ambas?
3. Puedes utilizar un signo menos para hacer un número negativo como -2. ¿Qué ocurre si
pones un signo más antes de un número? ¿Qué pasa con 2++2?
4. En notación matemática, los ceros a la izquierda están bien, como en 09. ¿Qué ocurre si
intentas esto en Python? ¿Qué pasa con 011?
2. ¿Cuántas millas hay en 10 kilómetros? Pista: hay 1.61 kilómetros en una milla.
Una forma común de representar en papel las variables es escribir el nombre con una flecha
apuntando a su valor. Este tipo de figura se llama diagrama de estado porque muestra en
qué estado está cada una de las variables (piénsalo como el estado mental de la variable).
La Figura 2.1 muestra el resultado del ejemplo anterior.
Los nombres de variable pueden ser tan largos como quieras. Pueden contener tanto letras
como números, pero no pueden comenzar con un número. Es legal utilizar letras mayús-
culas, pero es convencional utilizar solo minúsculas para los nombres de variables.
El guión bajo, _, puede aparecer en un nombre. A menudo se utiliza en nombres con varias
palabras, tales como tu_nombre o velocidad_de_golondrina_sin_carga.
Si le das un nombre ilegal a una variable, obtendrás un error de sintaxis:
>>> 76trombones = 'gran desfile'
SyntaxError: invalid syntax
>>> mas@ = 1000000
SyntaxError: invalid syntax
>>> class = 'Cimología teórica avanzada'
SyntaxError: invalid syntax
76trombones es ilegal porque comienza con un número. mas@ es ilegal porque contiene un
carácter ilegal, @. Sin embargo, ¿qué tiene de malo class?
Resulta que class es una de las palabras clave de Python. El intérprete utiliza las palabras
clave para reconocer la estructura del programa y no se pueden utilizar como nombres de
variable.
Python 3 tiene estas palabras clave:
False class finally is return
None continue for lambda try
True def from nonlocal while
and del global not with
as elif if or yield
assert else import pass
break except in raise
No tienes que memorizar esta lista. En la mayoría de los entornos de desarrollo, las pa-
labras clave se muestran con un color diferente; si intentas utilizar una como nombre de
variable, lo sabrás.
>>> n = 17
>>> print(n)
La primera línea es una sentencia de asignación que le da un valor a n. La segunda línea es
una sentencia print que muestra el valor de n.
Cuando escribes una sentencia, el intérprete la ejecuta, lo cual significa que hace lo que
dice la sentencia. En general, las sentencias no tienen valores.
Si sabes cómo crear y ejecutar un script en tu computador, estás listo para seguir. De lo
contrario, recomiendo de nuevo utilizar PythonAnywhere. He publicado instrucciones pa-
ra utilizarlo en modo script en http://tinyurl.com/thinkpython2e.
Debido a que Python proporciona ambos modos, puedes probar pedazos de código en
modo interactivo antes de ponerlos en un script. Sin embargo, hay diferencias entre el
modo interactivo y el modo script que pueden confundir.
Los Paréntesis tienen la mayor prioridad y se pueden utilizar para forzar una expre-
sión a evaluar en el orden que tú quieras. Dado que las expresiones en paréntesis se
evalúan primero, 2 * (3-1) es 4, y (1+1)**(5-2) es 8. También puedes utilizar pa-
réntesis para hacer una expresión más fácil de leer, como en (minuto * 100) / 60,
incluso si no cambia el resultado.
El operador + realiza una concatenación, lo cual significa que une las cadenas enlazándolas
de extremo a extremo. Por ejemplo:
>>> primero = 'curruca'
>>> segundo = 'garganta'
>>> primero + segundo
currucagarganta
El operador * también funciona en cadenas: hace repetición. Por ejemplo, 'Spam'*3 es
'SpamSpamSpam'. Si uno de los valores es una cadena, el otro tiene que ser un entero.
Este uso de + y * tiene sentido por analogía con la suma y la multiplicación. Tal como 4*3 es
equivalente a 4+4+4, esperamos que 'Spam'*3 sea lo mismo que 'Spam'+'Spam'+'Spam', y
lo es. Por otro lado, hay una manera significativa en la que la concatenación y la repetición
son diferentes de la suma y multiplicación de enteros. ¿Puedes pensar en una propiedad
que tiene la suma que la concatenación no tiene?
2.7. Comentarios 13
2.7. 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 pedazo de código
y descifrar lo que hace, o por qué lo hace.
Por esta razón, es una buena idea añadir notas a tus programas para explicar en lenguaje
natural lo que hace el programa. Estas notas se llaman comentarios, y comienzan con el
símbolo #:
# calcular el porcentaje de la hora que ha transcurrido
porcentaje = (minuto * 100) / 60
En este caso, el comentario aparece en una línea por sí sola. Puedes también poner comen-
tarios al final de una línea:
porcentaje = (minuto * 100) / 60 # porcentaje de una hora
Todo desde el # hasta el final de la línea es ignorado: no tiene efecto en la ejecución del
programa.
Los comentarios son más útiles cuando documentan características del código que no son
obvias. Es razonable suponer que el lector puede descifrar qué hace el código; es más útil
explicar por qué.
Este comentario es redundante con el código e inútil:
v = 5 # asigna 5 a v
Este comentario contiene información útil que no está en el código:
v = 5 # velocidad en metros/segundos.
Los buenos nombres de variable 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.8. Depuración
En un programa pueden ocurrir tres tipos de errores: errores de sintaxis, errores de tiempo
de ejecución y errores semánticos. Es útil distinguir entre ellos para rastrearlos de manera
más rápida.
Los errores de tiempo de ejecución son poco comunes en programas simples que ve-
rás en los primeros capítulos, así que puede pasar un tiempo antes de que encuentres
uno.
Error semántico: El tercer tipo de error es “semántico”, lo cual significa que se relaciona
con el significado. Si hay un error semántico en tu programa, se ejecutará sin generar
mensajes de error, pero no hará lo correcto. Hará otra cosa. Específicamente, hará lo
que le dijiste que hiciera.
Identificar errores semánticos puede ser complicado porque requiere que trabajes ha-
cia atrás mirando la salida del programa e intentando averiguar lo que hace.
2.9. Glosario
variable: Un nombre que se refiere a un valor.
palabra clave: Una palabra reservada que se utiliza como parte de la sintaxis de un pro-
grama; no puedes utilizar palabras claves tales como if, def y while como nombres
de variables.
evaluar: Simplificar una expresión realizando las operaciones para obtener un valor único.
sentencia: Una sección de código que representa un comando o acción. Hasta aquí, las
sentencias que hemos visto son asignaciones y sentencias print.
modo script: Una manera de utilizar el intérprete de Python para leer código de un script
y ejecutarlo.
orden de operaciones: Reglas que gobiernan el orden en el cual se evalúan las expresiones
que involucran múltiples operadores y operandos.
error semántico: Un error en un programa que supone hacer algo distinto a lo que el pro-
gramador pretendía.
2.10. Ejercicios
Ejercicio 2.1. Repitiendo mi consejo del capítulo anterior, cuando aprendas una nueva caracterís-
tica, deberías intentar probarla en modo interactivo y cometer errores a propósito para ver qué sale
mal.
En algunos lenguajes cada sentencia termina con un punto y coma, ;. ¿Qué ocurre si pones
un punto y coma al final de una sentencia de Python?
En notación matemática puedes multiplicar x e y así: xy. ¿Qué ocurre si intentas eso en
Python?
Ejercicio 2.2. Practica utilizando el intérprete de Python como una calculadora:
1. El volumen de una esfera con radio r es 34 πr3 . ¿Cuál es el volumen de una esfera con radio 5?
2. Supongamos que el precio original de un libro es $24.95, pero las librerías obtienen un 40 %
de descuento. El envío cuesta $3 para la primera copia y 75 centavos por cada copia adicional.
¿Cuál es el costo al por mayor para 60 copias?
3. Si dejo mi casa a las 6:52 a.m. y corro 1 milla a un ritmo fácil (8 minutos y 15 segundos por
milla), luego 3 millas a tempo (7 minutos y 12 segundos por milla) y 1 milla a ritmo fácil de
nuevo, ¿a qué hora llego a casa para el desayuno?
16 Capítulo 2. Variables, expresiones y sentencias
Capítulo 3
Funciones
Antes de que podamos utilizar las funciones de un módulo, tenemos que importarlo con
una sentencia import:
>>> import math
Esta sentencia crea un objeto de módulo llamado math. Si muestras el objeto de módulo
en pantalla, obtienes información sobre este:
>>> math
<module 'math' (built-in)>
El objeto de módulo contiene las funciones y variables definidas en el módulo. Para tener
acceso a una de las funciones, tienes que especificar el nombre del módulo y el nombre de
la función, separados por un punto. Este formato se llama notación de punto.
>>> relacion = potencia_senal / potencia_ruido
>>> decibeles = 10 * math.log10(relacion)
Si sabes trigonometría, puedes verificar los resultados anteriores comparándolos con la raíz
cuadrada de dos, dividida por dos:
>>> math.sqrt(2) / 2.0
0.707106781187
3.3. Composición 19
3.3. Composición
Hasta aquí, hemos visto los elementos de un programa —variables, expresiones y
sentencias— de forma aislada, sin hablar sobre cómo combinarlos.
Los paréntesis vacíos después del nombre indican que esta función no toma ningún argu-
mento.
Las cadenas en las sentencias print están encerradas en comillas dobles. Las comillas sim-
ples y las comillas dobles hacen lo mismo; la mayoría de la gente utiliza comillas simples
excepto en casos como este donde una comilla simple (que también es un apóstrofe) apa-
rece en la cadena.
20 Capítulo 3. Funciones
Todas las comillas (simples y dobles) deben ser “comillas rectas”, usualmente ubicadas
cerca de Enter en el teclado. Las “comillas tipográficas”, como las de esta oración, no son
legales en Python.
Si escribes una definición de función en modo interactivo, el intérprete imprime puntos
(...) que te hacen saber que la definición no está completa:
>>> def imprimir_letra():
... print("I'm a lumberjack, and I'm okay.")
... print("I sleep all night and I work all day.")
...
Para terminar una función, tienes que insertar una línea vacía.
Al definir una función, se crea un objeto de función que tiene tipo function:
>>> print(imprimir_letra)
<function imprimir_letra at 0xb7e99e9c>
>>> type(imprimir_letra)
<class 'function'>
La sintaxis para llamar a la nueva función es la misma que para las funciones incorporadas:
>>> imprimir_letra()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
Una vez que hayas definido una función, puedes utilizarla dentro de otra función.
Por ejemplo, para repetir el estribillo anterior, podríamos escribir una función llamada
repetir_letra:
def repetir_letra():
imprimir_letra()
imprimir_letra()
Y luego llamar a repetir_letra:
>>> repetir_letra()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
Pero así no es como sigue la canción realmente.
def repetir_letra():
imprimir_letra()
imprimir_letra()
repetir_letra()
3.6. Flujo de ejecución 21
Como podrías esperar, tienes que crear la función antes de que puedas ejecutarla. En otras
palabras, la definición de función tiene que efectuarse antes de que la función sea llamada.
Como ejercicio, mueve la última línea de este programa hasta el principio, así la llamada a
la función aparece antes que las definiciones. Ejecuta el programa y mira qué mensaje de
error obtienes.
La ejecución siempre comienza con la primera sentencia del programa. Las sentencias se
ejecutan una a la vez, en orden desde arriba hacia abajo.
Las definiciones de función no alteran el flujo de ejecución del programa, pero recuerda
que las sentencias dentro de la función no se ejecutan hasta que la función es llamada.
Eso suena bastante simple, hasta que recuerdas que una función puede llamar a otra. Mien-
tras está en el medio de una función, el programa quizás tenga que ejecutar las sentencias
en otra función. Luego, mientras se ejecuta esa nueva función, ¡el programa quizás tenga
que ejecutar otra función más!
Afortunadamente, Python es bueno haciendo seguimiento de dónde está, así que cada vez
que se completa una función, el programa retoma donde lo había dejado en la función que
la llamó. Cuando llega al final del programa, termina.
En resumen, cuando lees un programa, no siempre quieres leer desde arriba hacia abajo. A
veces tiene más sentido si sigues el flujo de ejecución.
Dentro de la función, los argumentos son asignados a variables llamadas parámetros. Aquí
hay una definición para una función que toma un argumento:
22 Capítulo 3. Funciones
def impr_2veces(bruce):
print(bruce)
print(bruce)
Esta función asigna el argumento a un parámetro con nombre bruce. Cuando la función es
llamada, esta imprime el valor del parámetro (sea lo que sea) dos veces.
Esta función puede utilizarse con cualquier valor que se pueda imprimir.
>>> impr_2veces('Spam')
Spam
Spam
>>> impr_2veces(42)
42
42
>>> impr_2veces(math.pi)
3.14159265359
3.14159265359
Las mismas reglas de composición que se aplican a las funciones incorporadas también se
aplican a las funciones definidas por el programador, así que podemos utilizar cualquier
tipo de expresión como un argumento para impr_2veces:
>>> impr_2veces('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> impr_2veces(math.cos(math.pi))
-1.0
-1.0
El argumento es evaluado antes de que se llame a la función, por lo que en los ejemplos las
expresiones 'Spam '*4 y math.cos(math.pi) son evaluadas una sola vez.
Las funciones pueden hacer que un programa sea más pequeño al eliminar código
repetitivo. Después, si quieres hacer un cambio, solo tienes que hacerlo en un lugar.
Dividir un programa largo en funciones te permite depurar las partes una a la vez y
luego reunirlas en un todo funcional.
Las funciones bien diseñadas son a menudo útiles para muchos programas. Una vez
que escribes y depuras una, la puedes reutilizar.
3.12. Depuración
Una de las habilidades más importantes que adquirirás es la depuración. Aunque puede
ser frustrante, la depuración es una de las partes más intelectualmente ricas, desafiantes e
interesantes de la programación.
En algunas formas la depuración es como un trabajo de detective. Te enfrentas a pistas y
tienes que inferir los procesos y eventos que te guían a los resultados que ves.
La depuración es también como una ciencia experimental. Una vez que tienes una idea
sobre qué va mal, modificas tu programa e intentas de nuevo. Si tu hipótesis era correcta,
puedes predecir el resultado de la modificación y das un paso más cerca hacia un programa
que funcione. Si tu hipótesis era incorrecta, tienes que inventar una nueva. Como señaló
Sherlock Holmes, “Una vez descartado lo imposible, lo que queda, por improbable que
parezca, debe ser la verdad.” (A. Conan Doyle, El signo de los cuatro)
Para algunas personas, programar y depurar son lo mismo. Es decir, programar es el pro-
ceso de depurar gradualmente un programa hasta que haga lo que tú quieres. La idea es
que deberías comenzar con un programa que funcione y hacer pequeñas modificaciones,
depurándolas a medida que avanzas.
Por ejemplo, Linux es un sistema operativo que contiene millones de líneas de código, pero
comenzó como un programa simple que Linus Torvalds utilizaba para explorar el chip Intel
80386. Según Larry Greenfield, “Uno de los proyectos anteriores de Linus era un programa
que cambiaría entre imprimir AAAA y BBBB. Esto evolucionó más tarde a Linux.” (The
Linux Users’ Guide Beta Version 1).
3.13. Glosario
función: Una secuencia de sentencias que tiene nombre y realiza alguna operación útil.
Las funciones pueden o no tomar argumentos y pueden o no producir un resultado.
definición de función: Una sentencia que crea una nueva función, especificando su nom-
bre, parámetros y las sentencias que contiene.
26 Capítulo 3. Funciones
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 de función.
parámetro: Un nombre utilizado dentro de una función para referirse al valor pasado co-
mo argumento.
llamada a función: Una sentencia que ejecuta una función. Consiste en el nombre de la
función seguido de una lista de argumentos en paréntesis.
variable local: Una variable definida dentro de una función. Una variable local solo puede
utilizarse dentro de su función.
valor de retorno: El resultado de una función. Si una llamada a función se utiliza como
expresión, el valor de retorno es el valor de la expresión.
módulo: Un archivo que contiene una colección de funciones relacionadas entre sí y otras
definiciones.
sentencia import: Una sentencia que lee un archivo de módulo y crea un objeto de módu-
lo.
objeto de módulo: Un valor creado por una sentencia import que proporciona acceso a
los valores definidos en el módulo.
notación de punto: La sintaxis para llamar a una función de otro módulo especificando el
nombre del módulo, seguido de un punto y el nombre de la función.
composición: Usar una expresión como parte de una expresión más grande, o una senten-
cia como parte de una sentencia más grande.
diagrama de pila: Una representación de una pila de funciones, sus variables y los valores
a los cuales se refieren.
marco: Un recuadro en un diagrama de pila que representa una llamada a función. Con-
tiene las variables locales y los parámetros de la función.
rastreo: Una lista de las funciones que se están ejecutando, impresas cuando ocurre una
excepción.
3.14. Ejercicios 27
3.14. Ejercicios
Ejercicio 3.1. Escribe una función con nombre justificar_derecha que tome una cadena con
nombre s como parámetro e imprima la cadena con suficientes espacios al inicio de tal manera que
la última letra de la cadena esté en la columna 70 de la pantalla.
>>> justificar_derecha('monty')
monty
Pista: utiliza la repetición de cadenas y la concatenación. Además, Python proporciona una fun-
ción incorporada llamada len que devuelve la longitud de una cadena, por lo que el valor de
len('monty') es 5.
Ejercicio 3.2. Un objeto de función es un valor que puedes asignar a una variable o pasarlo co-
mo argumento. Por ejemplo, hacer_2veces es una función que toma un objeto de función como
argumento y lo llama dos veces:
def hacer_2veces(f):
f()
f()
Aquí hay un ejemplo que utiliza hacer_2veces para llamar a una función con nombre
imprimir_spam dos veces.
def imprimir_spam():
print('spam')
hacer_2veces(imprimir_spam)
2. Modifica hacer_2veces para que tome dos argumentos, un objeto de función y un valor, y
llame a la función dos veces, pasando al valor como argumento.
4. Usa la versión modificada de hacer_2veces para llamar a impr_2veces dos veces, pasando
a 'spam' como argumento.
5. Define una nueva función llamada hacer_4veces que tome un objeto de función y un valor
y llame a la función cuatro veces, pasando al valor como argumento. Debería haber solo dos
sentencias en el cuerpo de esta función, no cuatro.
+ - - - - + - - - - +
| | |
| | |
| | |
| | |
+ - - - - + - - - - +
| | |
| | |
| | |
| | |
+ - - - - + - - - - +
Pista: para imprimir más de un valor en una línea, puedes imprimir una secuencia de valores
separada por comas:
print('+', '-')
Por defecto, print avanza a la siguiente línea, pero puedes anular ese comportamiento y poner
un espacio al final, como esto:
2. Escribe una función que dibuje una cuadrícula similar con cuatro filas y cuatro columnas.
Solución: http: // thinkpython2. com/ code/ grid. py . Crédito: este ejercicio está basado en
un ejercicio de Oualline, Practical C Programming, Third Edition, O’Reilly Media, 1997.
Capítulo 4
Este capítulo presenta un estudio de caso que demuestra un proceso para diseñar funciones
que interactúen entre sí.
Se presenta el módulo turtle, el cual te permite crear imágenes utilizando gráficas tortu-
ga. El módulo turtle está incluido en la mayoría de las instalaciones de Python, pero si
estás ejecutando Python utilizando PythonAnywhere, no podrás ejecutar los ejemplos de
tortuga (al menos no podías cuando escribí esto).
Crea un archivo con nombre mipoligono.py y escribe en él las siguientes líneas de código:
import turtle
bob = turtle.Turtle()
print(bob)
turtle.mainloop()
El módulo turtle (con ’t’ minúscula) proporciona una función llamada Turtle (con ’T’
mayúscula) que crea un objeto Turtle, el cual asignamos a una variable con nombre bob. Al
imprimir bob se muestra algo como:
<turtle.Turtle object at 0xb7bfbf4c>
30 Capítulo 4. Estudio de caso: diseño de interfaz
Esto significa que bob se refiere a un objeto con tipo Turtle como se define en el módulo
turtle.
mainloop le dice a la ventana que espere a que el usuario haga algo, aunque en este caso
no hay mucho que pueda hacer el usuario excepto cerrar la ventana.
Una vez que creas una tortuga, puedes llamar a un método para moverla dentro de la
ventana. Un método es similar a una función, pero utiliza una sintaxis un poco diferente.
Por ejemplo, para mover la tortuga hacia adelante:
bob.fd(100)
El método, fd (forward), está asociado con el objeto tortuga que llamamos bob. Llamar a un
método es como hacer una solicitud: le estás pidiendo a bob que se mueva hacia adelante.
El argumento de fd es una distancia en pixeles, por lo que el tamaño real depende de tu
pantalla.
Otros métodos que puedes llamar en un objeto Turtle son bk (backward) para retroceder, lt
(left turn) para girar a la izquierda y rt (right turn) para girar a la derecha. El argumento
para lt y rt es un ángulo en grados.
Además, cada Turtle sostiene una pluma, que está arriba o abajo; si la pluma está abajo, la
tortuga deja un rastro cuando se mueve. Los métodos pu y pd representan “pen up” y “pen
down”.
Para dibujar un ángulo recto, agrega estas líneas al programa (después de crear a bob y
antes de llamar a mainloop):
bob.fd(100)
bob.lt(90)
bob.fd(100)
Cuando ejecutes este programa, deberías ver a bob moverse al este y luego al norte, dejando
dos segmentos de línea atrás.
Ahora modifica el programa para dibujar un cuadrado. ¡No continues hasta que lo hayas
hecho funcionar!
bob.fd(100)
bob.lt(90)
bob.fd(100)
bob.lt(90)
bob.fd(100)
Podemos hacer lo mismo de manera más concisa con una sentencia for. Agrega este ejem-
plo a mipoligono.py y ejecútalo de nuevo:
4.3. Ejercicios 31
for i in range(4):
print('½Hola!')
Deberías ver algo como esto:
½Hola!
½Hola!
½Hola!
½Hola!
Este es el uso más simple de una sentencia for; después veremos más. Pero eso debería ser
suficiente para dejarte reescribir tu programa que dibuja cruadrados. No continues hasta
que lo hagas.
Aquí hay una sentencia for que dibuja un cuadrado:
for i in range(4):
bob.fd(100)
bob.lt(90)
La sintaxis de una sentencia for es similar a una definición de función. Tiene un encabeza-
do que termina con el signo dos puntos y un cuerpo con sangrías. El cuerpo puede contener
cualquier número de sentencias.
Una sentencia for también es llamada bucle porque el flujo de ejecución pasa por el cuerpo
y luego vuelve hacia arriba. Es este caso, pasa por el cuerpo cuatro veces.
Esta versión es en realidad un poco diferente del código que dibuja cuadrados propuesto
anteriormente porque hace otro giro después de dibujar el último lado del cuadrado. El
giro extra toma más tiempo, pero simplifica el código si hacemos lo mismo en cada paso
por el bucle. Esta versión también tiene el efecto de regresar a la tortuga a su posición
inicial, apuntando a la dirección inicial.
4.3. Ejercicios
Lo siguiente es una serie de ejercicios que utilizan el módulo turtle. Pretenden ser diver-
tidos, pero también tienen un punto. Mientras trabajes en ellos, piensa cuál es el punto.
Las siguientes secciones tienen soluciones a los ejercicios, así que no las mires hasta que
hayas terminado (o al menos intentado).
1. Escribe una función llamada cuadrado que tome un parámetro con nombre t, que es
una tortuga. Debería utilizar la tortuga para dibujar un cuadrado.
Escribe una llamada a función que pase a bob como argumento de cuadrado, y luego
ejecuta el programa de nuevo.
2. Agrega otro parámetro, con nombre longitud, a cuadrado. Modifica el cuerpo para
que la longitud de los lados sea longitud, y luego modifica la llamada a función para
poner un segundo argumento. Ejecuta el programa de nuevo. Prueba tu programa
con un rango de valores para longitud.
3. Haz una copia de cuadrado y cambia el nombre a poligono. Agrega otro parámetro
con nombre n y modifica el cuerpo para que dibuje un polígono regular con n lados.
Pista: los ángulos exteriores de un polígono regular con n lados son de 360/n grados.
32 Capítulo 4. Estudio de caso: diseño de interfaz
4. Escribe una función llamada circulo que tome una tortuga, t, y radio, r, como pa-
rámetros y dibuje un círculo aproximado llamando a poligono con una longitud y
número de lados apropiado. Prueba tu función con un rango de valores de r.
Pista: averigua cuál es el perímetro del círculo y asegúrate de que se cumpla que
longitud * n = perimetro.
5. Haz una versión más general de circulo llamada arco que tome un parámetro adi-
cional, angulo, que determine qué fracción de un círculo dibujar. angulo está en gra-
dos, así que cuando angulo=360, arco debería dibujar un círculo completo.
4.4. Encapsulamiento
El primer ejercicio te pide poner tu código que dibuja cuadrados dentro de una definición
de función y luego llamar a la función, pasando a la tortuga como parámetro. Esta es la
solución:
def cuadrado(t):
for i in range(4):
t.fd(100)
t.lt(90)
cuadrado(bob)
Las sentencias de más adentro, fd y lt, tienen doble sangría para mostrar que están
dentro del bucle for, el cual está dentro de la definición de función. La siguiente línea,
cuadrado(bob), está alineada con el margen izquierdo, lo cual indica el término tanto del
bucle for como de la definición de función.
Dentro de la función, t se refiere a la misma tortuga bob, por lo que t.lt(90) tiene el
mismo efecto que bob.lt(90). En ese caso, ¿por qué no llamar al parámetro bob? La idea
es que t puede ser cualquier tortuga, no solo bob, así que podrías crear una segunda tortuga
y pasarla como argumento a cuadrado:
alice = turtle.Turtle()
cuadrado(alice)
El acto de envolver un pedazo de código en una función se llama encapsulamiento. Uno
de los beneficios del encapsulamiento es que adjunta un nombre al código, lo cual sirve
como una especie de documentación. Otra ventaja es que si reutilizas el código, ¡es más
conciso llamar a una función dos veces que copiar y pegar el cuerpo!
4.5. Generalización
El siguiente paso es agregar un parámetro longitud a cuadrado. Aquí hay una solución:
def cuadrado(t, longitud):
for i in range(4):
t.fd(longitud)
t.lt(90)
cuadrado(bob, 100)
4.6. Diseño de interfaz 33
El acto de agregar un parámetro a una función se llama generalización porque hace que la
función sea más general: en la versión anterior, el cuadrado tiene siempre el mismo tamaño;
en esta versión, puede ser de cualquier tamaño.
El siguiente paso es también una generalización. En lugar de dibujar cuadrados, poligono
dibuja polígonos regulares con cualquier número de lados. Aquí hay una solución:
def poligono(t, n, longitud):
angulo = 360 / n
for i in range(n):
t.fd(longitud)
t.lt(angulo)
poligono(bob, 7, 70)
Este ejemplo dibuja un polígono de 7 lados de longitud 70.
Si estás usando Python 2, el valor de angulo podría ser incorrecto debido a una división
entera. Una solución simple es calcular angulo = 360.0 / n. Dado que el numerador es
un número de coma flotante, el resultado es de coma flotante.
Cuando una función tiene más que unos pocos argumentos numéricos, es fácil olvidar qué
son, o en qué orden deberían estar. En ese caso, a menudo es una buena idea incluir los
nombres de los parámetros en la lista de argumentos:
poligono(bob, n=7, longitud=70)
Estos se llaman argumentos de palabra clave porque incluyen a los nombres de parámetro
tratándolos como “palabras clave” (no confundir con las palabras clave de Python como
while y def).
Esta sintaxis hace que el programa sea más legible. Es también un recordatorio sobre cómo
funcionan los argumentos y los parámetros: cuando llamas a una función, los argumentos
son asignados a los parámetros.
Una limitación de esta solución es que n es una constante, lo cual significa que para círculos
muy grandes, los segmentos de línea son muy largos, y para círculos pequeños, ocupamos
mucho tiempo dibujando segmentos muy pequeños. Una solución sería generalizar la fun-
ción para que tome a n como parámetro. Esto le daría al usuario (quien llame a circulo)
más control, pero la interfaz sería menos limpia.
La interfaz de una función es un resumen de cómo esta se utiliza: ¿cuáles son los pará-
metros? ¿Qué hace la función? ¿Y cuál es el valor de retorno? Una interfaz es “limpia” si
permite a la sentencia llamadora hacer lo que quiere sin lidiar con detalles innecesarios.
4.7. Refactorización
Cuando escribí circulo, fui capaz de reutilizar poligono porque un polígono de muchos
lados es una buena aproximación de un círculo. Pero arco no es tan cooperativo: no pode-
mos utilizar poligono o circulo para dibujar un arco.
Una alternativa es comenzar con una copia de poligono y transformarla en arco. El resul-
tado podría verse así:
def arco(t, r, angulo):
longitud_arco = 2 * math.pi * r * angulo / 360
n = int(longitud_arco / 3) + 1
longitud_paso = longitud_arco / n
angulo_paso = angulo / n
for i in range(n):
t.fd(longitud_paso)
t.lt(angulo_paso)
La segunda mitad de esta función se parece a poligono, pero no podemos reutilizar
poligono sin cambiar la interfaz. Podríamos generalizar poligono para que tome un án-
gulo como tercer argumento, ¡pero entonces poligono ya no sería un nombre apropiado!
En cambio, llamemos polilinea a la función más general:
4.8. Un plan de desarrollo 35
Este proceso tiene algunos inconvenientes —más tarde veremos alternativas— pero puede
ser útil si no sabes de antemano cómo dividir el programa en funciones. Este enfoque te
permite diseñar a medida que avanzas.
36 Capítulo 4. Estudio de caso: diseño de interfaz
4.9. docstring
Un docstring es una cadena al comienzo de una función que explica la interfaz (“doc” es
la abreviatura de “documentation”). Aquí hay un ejemplo:
def polilinea(t, n, longitud, angulo):
"""Dibuja n segmentos de línea con la longitud dada
y el ángulo (en grados) entre ellos. t es una tortuga.
"""
for i in range(n):
t.fd(longitud)
t.lt(angulo)
Por convención, todos los docstrings son cadenas entre triple comillas, también conocidas
como cadenas multilínea porque las triple comillas permiten expandir la cadena a más de
una línea.
Es breve, pero contiene la información esencial que alguien necesitaría para utilizar esta
función. Explica de manera concisa lo que hace la función (sin entrar en detalles sobre
cómo lo hace). Explica qué efecto tiene cada parámetro en el comportamiento de la función
y de qué tipo debería ser cada parámetro (si no es obvio).
Escribir este tipo de documentación es una parte importante del diseño de la interfaz. Una
interfaz bien diseñada debería ser simple de explicar; si tienes dificultades al explicar una
de tus funciones, quizás la interfaz podría mejorar.
4.10. Depuración
Una interfaz es como un contrato entre una función y la sentencia llamadora. La llamadora
acepta proporcionar ciertos argumentos y la función acepta hacer cierto trabajo.
Por ejemplo, polilinea requiere cuatro argumentos: t tiene que ser Turtle; n tiene que ser
un entero; longitud debería ser un número positivo; y angulo tiene que ser un número,
que se entiende que está en grados.
Estos requisitos se llaman precondiciones porque se supone que son verdaderos antes de
que la función comience a ejecutarse. De forma opuesta, las condiciones al final de la fun-
ción son postcondiciones. Las postcondiciones incluyen el efecto previsto de la función
(como al dibujar segmentos de línea) y cualquier efecto secundario (como mover la tortuga
o hacer otros cambios).
4.11. Glosario
método: Una función que se asocia a un objeto y se llama utilizando notación de punto.
4.12. Ejercicios 37
argumento de palabra clave: Un argumento que incluye el nombre del parámetro tratán-
dolo como “palabra clave”.
interfaz: Una descripción de cómo utilizar una función, incluyendo el nombre y descrip-
ciones de los argumentos y el valor de retorno.
refactorización: El proceso de modificar un programa que funciona para mejorar las inter-
faces de funciones y otras cualidades del código.
docstring: Una cadena que aparece en la parte superior de una definición de función para
documentar la interfaz de la función.
4.12. Ejercicios
Ejercicio 4.1. Descarga el código de este capítulo en http: // thinkpython2. com/ code/
polygon. py .
1. Dibuja un diagrama de pila que muestre el estado del programa al ejecutar circulo(bob,
radio). Puedes hacer la aritmética a mano o agregar sentencias print al código.
2. La versión de arco en la Sección 4.7 no es muy precisa debido a que la aproximación lineal del
círculo está siempre afuera del verdadero círculo. Como resultado, la tortuga termina a unos
pocos pixeles de distancia del destino correcto. Mi solución muestra una manera de reducir el
efecto de este error. Lee el código y ve si tiene sentido para ti. Si dibujas un diagrama, podrías
ver cómo trabaja.
38 Capítulo 4. Estudio de caso: diseño de interfaz
Ejercicio 4.2. Escribe un conjunto de funciones apropiadamente generales que puedan dibujar
flores como en la Figura 4.1.
Deberías escribir una función para cada letra, con nombres dibujar_a, dibujar_b, etc., y poner
tus funciones en un archivo con nombre letras.py. Puedes descargar una “máquina de escribir
tortuga” desde http: // thinkpython2. com/ code/ typewriter. py para ayudarte a probar tu
código.
Puedes obtener una solución en http: // thinkpython2. com/ code/ letters. py ; también re-
quiere http: // thinkpython2. com/ code/ polygon. py .
Ejercicio 4.5. Lee sobre espirales en http: // en. wikipedia. org/ wiki/ Spiral ; luego escribe
un programa que dibuje una espiral arquimediana (o uno de los otros tipos). Solución: http: //
thinkpython2. com/ code/ spiral. py .
Capítulo 5
Condicionales y recursividad
El tema principal de este capítulo es la sentencia if, la cual ejecuta código diferente depen-
diendo del estado del programa. Pero primero quiero presentar dos operadores nuevos:
división entera y módulo.
El operador de módulo es más útil de lo que parece. Por ejemplo, puedes verificar si un
número es divisible por otro: si x %y es cero, entonces x es divisible por y.
Además, puedes extraer el dígito de más a la derecha o más dígitos de un número. Por
ejemplo, x %10 entrega el dígito de más a la derecha de x (en base 10). De manera similar,
x %100 entrega los dos últimos dígitos.
Si estás usando Python 2, la división funciona diferente. El operador división, /, realiza una
división entera si ambos operandos son enteros, y la división de coma flotante si cualquiera
de los dos operandos es un float.
Finalmente, el operador not niega una expresión booleana, así que not (x > y) es verda-
dera si x > y es falsa, es decir, si x es menor o igual que y.
5.4. Ejecución condicional 41
Estrictamente hablando, los operandos de los operadores lógicos deberían ser expresiones
booleanas, pero Python no es muy estricto. Cualquier número distinto de cero es interpre-
tado como True:
>>> 42 and True
True
Esta flexibilidad puede ser útil, pero hay algunas sutilezas que podrían ser confusas. Qui-
zás quieras evitar esto (a menos que sepas lo que estás haciendo).
Las sentencias if tienen la misma estructura que las definiciones de función: un encabe-
zado seguido de un cuerpo con sangrías. Las sentencias como esta se llaman sentencias
compuestas.
No hay límite en el número de sentencias que pueden aparecer en el cuerpo, pero tiene
que haber al menos una. A veces, es útil tener un cuerpo sin sentencias (generalmente para
reservar lugar a código que no has escrito todavía). En ese caso, puedes usar la sentencia
pass, la cual no hace nada.
if x < 0:
pass # PENDIENTE: ½falta manejar los valores negativos!
A pesar de que la sangría de las sentencias hacen evidente la estructura, los condicionales
anidados se vuelven difíciles de leer rápidamente. Es una buena idea evitarlos cuando
puedas.
Los operadores lógicos a menudo proporcionan una manera de simplificar las sentencias
condicionales anidadas. Por ejemplo, podemos reescribir el siguiente código utilizando un
único condicional:
5.8. Recursividad 43
if 0 < x:
if x < 10:
print('x es un número positivo de un dígito.')
La sentencia print solo se ejecuta si pasamos por los dos condicionales, así que podemos
obtener el mismo efecto con el operador and:
if 0 < x and x < 10:
print('x es un número positivo de un dígito.')
Para este tipo de condición, Python proporciona una opción más concisa:
if 0 < x < 10:
print('x es un número positivo de un dígito.')
5.8. Recursividad
Es legal para una función llamar a otra función; es legal también para una función llamarse
a sí misma. Puede que no sea obvia la razón por la cual eso es una buena idea, pero resulta
ser una de las cosas más mágicas que puede hacer un programa. Por ejemplo, mira la
siguiente función:
def cuenta_reg(n):
if n <= 0:
print('½Despegue!')
else:
print(n)
cuenta_reg(n-1)
Si n es 0 o negativo, muestra la palabra, “¡Despegue!” De lo contrario, muestra a n y luego
llama a la función con nombre cuenta_reg —a sí misma— pasando a n-1 como argumento.
__main__
cuenta_reg n 3
cuenta_reg n 2
cuenta_reg n 1
cuenta_reg n 0
3
2
1
½Despegue!
Una función que se llama a sí misma es recursiva; el proceso de ejecutarla se llama recur-
sividad.
Como ejemplo adicional, podemos escribir una función que imprima una cadena n veces.
def imprimir_n(s, n):
if n <= 0:
return
print(s)
imprimir_n(s, n-1)
Si n <= 0, la sentencia return hace que se salga de la función. El flujo de ejecución vuelve
inmediatamente a la sentencia llamadora y las líneas restantes de la función no se ejecutan.
Para ejemplos simples como este, probablemente es más fácil utilizar un bucle for. Sin
embargo, más adelante veremos ejemplos que son difíciles de escribir con un bucle for y
fáciles de escribir con recursividad, así que es bueno comenzar pronto.
Cada vez que una función es llamada, Python crea un marco que contiene las variables
locales y los parámetros de la función. Para una función recursiva, podría haber más de un
marco en la pila al mismo tiempo.
Como siempre, la parte de arriba de la pila es el marco para __main__. Está vacío porque
no creamos variables en __main__ ni le pasamos argumentos.
Los cuatro marcos de cuenta_reg tienen valores diferentes para el parámetro n. La parte
de abajo de la pila, donde n=0, se llama caso base. No hace una llamada recursiva, así que
no hay más marcos.
Como ejercicio, dibuja un diagrama de pila para imprimir_n llamada con s = 'Hola' y
n=2. Luego, escribe una función llamada hacer_n que tome un objeto de función y un
número, n, como argumentos, y que llame a dicha función n veces.
Antes de obtener la entrada del usuario, es una buena idea imprimir un mensaje que le
diga al usuario qué escribir. input puede tomar un mensaje como argumento:
>>> nombre = input('¾Cuál...es tu nombre?\n')
¾Cuál...es tu nombre?
½Arturo, Rey de los Britones!
>>> nombre
'½Arturo, Rey de los Britones!'
La secuencia \n al final del mensaje representa una nueva línea, la cual es un carácter
especial que provoca un salto de línea. Esa es la razón por la cual la entrada del usuario
aparece debajo del mensaje.
Si esperas que el usuario escriba un entero, puedes intentar convertir el valor de retorno a
int:
>>> mensaje = '¾Cuál...es la velocidad media de una golondrina sin carga?\n'
>>> velocidad = input(mensaje)
¾Cuál...es la velocidad media de una golondrina sin carga?
42
>>> int(velocidad)
42
Pero si el usuario escribe algo distinto a una cadena de dígitos, obtienes un error:
>>> velocidad = input(mensaje)
¾Cuál...es la velocidad media de una golondrina sin carga?
¾De qué especie, de la africana o de la europea?
>>> int(velocidad)
ValueError: invalid literal for int() with base 10
Más adelante veremos cómo tratar este tipo de error.
5.12. Depuración
Cuando ocurre un error de sintaxis o de tiempo de ejecución, el mensaje de error contiene
mucha información, pero esto puede ser abrumador. Las partes más útiles suelen ser:
Dónde ocurrió.
Los errores de sintaxis son generalmente fáciles de encontrar, pero hay algunas trampas.
Los errores de espacio en blanco pueden ser complicados porque los espacios y las sangrías
son invisibles y estamos acostumbrados a ignorarlos.
>>> x = 5
>>> y = 6
File "<stdin>", line 1
y = 6
^
IndentationError: unexpected indent
5.13. Glosario 47
En este ejemplo, el problema es que la segunda línea está desajustada por un espacio. Pero
el mensaje de error señala a y, lo cual es engañoso. En general, los mensajes de error indican
dónde fue descubierto el problema, pero el error real podría estar antes en el código, a veces
en una línea anterior. Lo mismo ocurre con los errores de tiempo de ejecución.
Supongamos que estás intentando calcular una relación señal/ruido en decibeles. La fór-
mula es RSRdb = 10 log10 ( Pseñal /Pruido ). En Python, podrías escribir algo así:
import math
potencia_senal = 9
potencia_ruido = 10
relacion = potencia_senal // potencia_ruido
decibeles = 10 * math.log10(relacion)
print(decibeles)
Cuando ejecutas este programa, obtienes una excepción:
Traceback (most recent call last):
File "snr.py", line 5, in ?
decibeles = 10 * math.log10(relacion)
ValueError: math domain error
El mensaje de error indica la línea 5, pero no hay nada malo con esa línea. Para encontrar
el error real, podría ser útil imprimir el valor de relacion, que resulta ser 0. El problema
está en la línea 4, que utiliza división entera en lugar de división de coma flotante.
Deberías tomarte el tiempo de leer cuidadosamente los mensajes de error, pero no supon-
gas que todo lo que dice es correcto.
5.13. Glosario
división entera: Un operador, denotado por //, que divide dos números y redondea a un
entero hacia abajo (en sentido hacia el infinito negativo).
operador de módulo: Un operador, denotado con un signo de porcentaje ( %), que trabaja
con enteros y devuelve el resto de dividir un número por otro.
expresión booleana: Una expresión cuyo valor es True o False.
operador relacional: Uno de los operadores que comprara sus operandos: ==, !=, >, <, >=
y <=.
operador lógico: Uno de los operadores que combina expresiones booleanas: and, or y
not.
sentencia condicional: Una sentencia que controla el flujo de ejecución dependiendo de
una condición.
condición: La expresión booleana en una sentencia condicional que determina cuál rama
se ejecuta.
sentencia compuesta: Una sentencia que consiste en un encabezado y un cuerpo. El enca-
bezado termina con un signo de dos puntos (:). El cuerpo tiene sangrías relativas al
encabezado.
rama: Una de las secuencias de sentencias alternativas en una sentencia condicional.
48 Capítulo 5. Condicionales y recursividad
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.
sentencia return: Una sentencia que provoca que una función termine inmediatamente y
vuelva a la sentencia llamadora.
recursividad: El proceso de llamar a la función que ya se está ejecutando.
caso base: Una rama condicional de una función recursiva que no hace una llamada recur-
siva.
recursividad infinita: Una recursividad que no tiene un caso base, o nunca lo alcanza.
Eventualmente, una recursividad infinita provoca un error de tiempo de ejecución.
5.14. Ejercicios
Ejercicio 5.1. El módulo time proporciona una función, con el mismo nombre time, que devuelve
el tiempo transcurrido desde la Hora Media de Greenwich (GMT) en “la época” (epoch), que es un
momento arbitrario usado como punto de referencia. En sistemas UNIX, la época es el 1 de enero de
1970.
>>> import time
>>> time.time()
1437746094.5735958
Escribe un script que lea el tiempo actual y lo convierta a una hora del día en horas, minutos y
segundos, además del número de días desde la época.
Ejercicio 5.2. El Último Teorema de Fermat dice que no hay enteros positivos a, b y c tales que
an + bn = cn
1. Escribe una función con nombre comprobar_fermat que tome cuatro parámetros —a, b, c
y n— y compruebe si se cumple el teorema de Fermat. Si n es mayor que 2 y
an + bn = cn
Si cualquiera de las tres longitudes es mayor que la suma de las otras dos, entonces
no puedes formar un triángulo. De lo contrario, sí puedes. (Si la suma de dos longitudes
es igual a la tercera, forman lo que llaman un triángulo “degenerado”.)
5.14. Ejercicios 49
1. Escribe una función con nombre es_triangulo que tome tres enteros como argumentos e
imprima “Sí” o “No”, dependiendo de si puedes o no formar un triángulo con palos cuyas
longitudes sean los enteros dados.
2. Escribe una función que permita al usuario ingresar tres longitudes de palos, los convierta
a enteros y utilice la función es_triangulo para comprobar si los palos con las longitudes
dadas pueden formar un triángulo.
Ejercicio 5.4. ¿Cuál es la salida del siguiente programa? Dibuja un diagrama de pila que muestre
el estado del programa cuando imprime el resultado.
def recursivo(n, s):
if n == 0:
print(s)
else:
recursivo(n-1, n+s)
recursivo(3, 0)
2. Escribe un docstring que explique todo lo que alguien necesitaría saber para utilizar esta
función (y nada más).
La excepción es si x es menor que 3: en ese caso, puedes simplemente dibujar una línea recta con
longitud x.
1. Escribe una función llamada koch que tome una tortuga y una longitud como parámetros y
que use a la tortuga para dibujar una curva de Koch con la longitud dada.
2. Escribe una función llamada copo_de_nieve que dibuje tres curvas de Koch que hagan el
contorno de un copo de nieve.
Solución: http: // thinkpython2. com/ code/ koch. py .
3. La curva de Koch se puede generalizar en muchas formas. Mira http: // en. wikipedia.
org/ wiki/ Koch_ snowflake para ejemplos e implementa tu favorito.
Capítulo 6
Funciones productivas
Muchas de las funciones de Python que hemos utilizado, tales como las funciones mate-
máticas, producen valores de retorno. Sin embargo, las funciones que hemos escrito son
todas nulas: tienen un efecto, como imprimir un valor o mover una tortuga, pero no tienen
un valor de retorno. En este capítulo aprenderás a escribir funciones productivas.
else:
return x
Dado que estas sentencias return están en un condicional alternativo, solo se ejecuta una.
Tan pronto como se ejecute una sentencia return, la función termina sin ejecutar ninguna
de las sentencias posteriores. El código que aparece después de una sentencia return, o
cualquier otro lugar que el flujo de ejecución nunca puede alcanzar, se llama código muer-
to.
En una función productiva, es una buena idea asegurarse de que cada camino posible a
través del programa llegue a una sentencia return. Por ejemplo:
def valor_absoluto(x):
if x < 0:
return -x
if x > 0:
return x
Esta función es incorrecta porque si x es 0, ninguna condición es verdadera, y la función
termina sin llegar a una sentencia return. Si el flujo de ejecución llega al final de una
función, el valor de retorno es None, lo cual no es el valor absoluto de 0.
>>> print(valor_absoluto(0))
None
Por cierto, Python proporciona una función incorporada llamada abs que calcula valores
absolutos.
Como ejercicio, escribe una función comparar que tome dos valores, x e y, y devuelva 1 si
x > y, devuelva 0 si x == y o devuelva -1 si x < y.
El primer paso es considerar cómo debería ser una función de distancia en Python. En
otras palabras, ¿cuáles son las entradas (parámetros) y cuál es la salida (valor de retorno)?
En este caso, las entradas son dos puntos, que puedes representar utilizando cuatro núme-
ros. El valor de retorno es la distancia representada por un valor de coma flotante.
Inmediatamente puedes escribir un esbozo de la función:
def distancia(x1, y1, x2, y2):
return 0.0
6.2. Desarrollo incremental 53
Obviamente, esta versión no calcula distancias: siempre devuelve cero. Pero es sintáctica-
mente correcta, y funciona, lo cual significa que puedes probarla antes de que la hagas más
complicada.
Para probar la nueva función, llámala con argumentos de prueba:
>>> distancia(1, 2, 4, 6)
0.0
Escojo estos valores para que la distancia horizontal sea 3 y la distancia vertical sea 4; de
esa forma, el resultado es 5, la hipotenusa de un triángulo rectángulo 3-4-5. Al probar una
función, es útil saber la respuesta correcta.
En este punto hemos confirmado que la función es sintácticamente correcta y podemos co-
menzar agregando código al cuerpo. Un siguiente paso razonable es encontrar las diferen-
cias x2 − x1 e y2 − y1 . La siguiente versión almacena esos valores en variables temporales
y las imprime.
def distancia(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
print('dx es', dx)
print('dy es', dy)
return 0.0
Si la función está bien, debería mostrar dx es 3 y dy es 4. Si es así, sabemos que la función
obtiene los argumentos correctos y realiza el primer cálculo de manera correcta. Si no, solo
hay que revisar unas pocas líneas.
Luego calculamos la suma de los cuadrados de dx y dy:
def distance(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dcuadrado = dx**2 + dy**2
print('dcuadrado es: ', dcuadrado)
return 0.0
De nuevo, tendrías que ejecutar el programa en este punto y verificar la salida (que debería
ser 25). Finalmente, puedes usar math.sqrt para calcular y devolver el resultado:
def distancia(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dcuadrado = dx**2 + dy**2
resultado = math.sqrt(dcuadrado)
return resultado
Si eso funciona de manera correcta, estás listo. De lo contrario, tal vez quieras imprimir el
valor de resultado antes de la sentencia return.
La versión final de la función no muestra nada cuando se ejecuta: solo devuelve un valor.
Las sentencias print que escribimos son útiles para depurar, pero una vez que la función
esté bien, deberías borrarlas. Un código como ese se llama andamiaje (en inglés, scaffolding)
porque es útil para construir el programa pero no es parte del producto final.
Cuando empieces, deberías agregar solo una o dos líneas de código a la vez. A medida que
ganes experiencia, podrías encontrarte escribiendo y depurando partes más grandes. De
cualquier manera, el desarrollo incremental puede ahorrarte mucho tiempo de depuración.
54 Capítulo 6. Funciones productivas
2. Utiliza variables que guarden valores intermedios de tal manera que puedas mos-
trarlos y verificarlos.
3. Una vez que el programa funciona, tal vez quieras borrar algo del andamiaje o conso-
lidar varias sentencias en una expresión compuesta, pero solo si no hace al programa
difícil de leer.
Como ejercicio, utiliza desarrollo incremental para escribir una función llamada
hipotenusa que devuelva el largo de la hipotenusa de un triángulo rectánculo, dadas las
longitudes de los otros dos lados como argumentos. Guarda cada etapa del proceso de
desarrollo a medida que avances.
6.3. Composición
Como ya deberías esperar, puedes llamar a una función desde dentro de otra. Como ejem-
plo, escribiremos una función que tome dos puntos, el centro de un círculo y un punto de
su perímetro, y calcule el área del círculo.
Supongamos que el punto central se almacena en las variables xc e yc, y el punto del perí-
metro está en xp e yp. El primer paso es encontrar el radio del círculo, que es la distiancia
entre los dos puntos. Acabamos de escribir una función, distancia, que hace eso:
radio = distancia(xc, yc, xp, yp)
El siguiente paso es encontrar el área de un círculo con ese radio; también escribimos eso:
resultado = area(radio)
Encapsulando estos pasos en una función, obtenemos:
def area_circulo(xc, yc, xp, yp):
radio = distancia(xc, yc, xp, yp)
resultado = area(radio)
return resultado
Las variables temporales radio y resultado son útiles para el desarrollo y la depuración,
pero una vez que el programa funciona, podemos hacerlo más conciso componiendo las
llamadas a funciones:
def area_circulo(xc, yc, xp, yp):
return area(distancia(xc, yc, xp, yp))
Si vieras esa definición en el diccionario, quizás te moleste. Por otra parte, si buscaras la
definición de la función factorial, denotada con el símbolo !, podrías obtener algo así:
0! = 1
n! = n(n − 1)!
Así que 3! es 3 por 2!, lo cual es 2 por 1!, lo cual es 1 por 0!. Poniéndolo todo junto, 3! es
igual a 3 por 2 por 1 por 1, lo cual es 6.
Si puedes escribir una definición recursiva de algo, puedes escribir un programa en Python
para evaluarla. El primer paso es decidir cuáles deberían ser los parámetros. En este caso,
debería estar claro que factorial toma un entero:
def factorial(n):
Si el argumento es 0, todo lo que tenemos que hacer es devolver 1:
def factorial(n):
if n == 0:
return 1
De lo contrario, y esta es la parte interesante, tenemos que hacer una llamada recursiva
para encontrar el factorial de n − 1 y luego multiplicarlo por n:
def factorial(n):
if n == 0:
return 1
else:
recur = factorial(n-1)
resultado = n * recur
return resultado
El flujo de ejecución para este programa es similar al flujo de cuenta_reg de la Sección 5.8.
Si llamamos a factorial con el valor 3:
__main__
6
factorial n 3 recur 2 resultado 6
2
factorial n 2 recur 1 resultado 2
1
factorial n 1 recur 1 resultado 1
1
factorial n 0
La Figura 6.1 muestra cómo se ve el diagrama de pila para esta sucesión de llamadas a
función.
Los valores de retorno se muestran volviendo hacia arriba en la pila. En cada marco, el
valor de retorno es el valor de resultado, que es el producto de n y recur.
En el último marco, las variables locales recur y resultado no existen, porque la rama que
las crea no se ejecuta.
6.6. Salto de fe
Seguir el flujo de ejecución es una manera de leer programas, pero puede volverse abru-
mador rápidamente. Una alternativa es lo que yo llamo “salto de fe”. Cuando llegas a una
llamada a función, en lugar de seguir el flujo de ejecución, supones que la función es correcta
y que devuelve el resultado correcto.
Lo mismo es verdad cuando llamas a una de tus propias funciones. Por ejemplo, en la
Sección 6.4, escribimos una función llamada es_divisible que determina si un número
es divisible por otro. Una vez que nos hemos convencido de que esta función es correcta
—examinando el código y probándolo— podemos utilizar la función sin mirar el cuerpo
otra vez.
Lo mismo es verdad para las funciones recursivas. Cuando llegues a la llamada recursiva,
en lugar de seguir el flujo de ejecución, deberías suponer que la llamada recursiva funciona
(devuelve el resultado correcto) y luego preguntarte: “suponiendo que puedo encontrar el
factorial de n − 1, ¿puedo calcular el factorial de n?” Está claro que puedes, multiplicando
por n.
Desde luego, es un poco extraño suponer que la función hace lo correcto cuando no has
terminado de escribirla, ¡pero es por eso que se llama salto de fe!
58 Capítulo 6. Funciones productivas
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n − 1) + fibonacci(n − 2)
Tenemos dos opciones. Podemos intentar generalizar la función factorial para que fun-
cione con números de coma flotante o podemos hacer que factorial verifique el tipo de
sus argumentos. La primera opción se llama la función gamma y está un poco más allá del
alcance de este libro. Entonces iremos por la segunda.
Podemos utilizar la función incorporada isinstance para verificar el tipo del argumento.
Mientras estemos en ello, podemos también asegurarnos de que el argumento sea positivo:
def factorial(n):
if not isinstance(n, int):
print('El factorial solo está definido para enteros.')
return None
elif n < 0:
6.9. Depuración 59
Este programa demuestra un patrón a veces llamado guardián. Los primeros dos condi-
cionales actúan como guardianes, protegiendo el código que sigue de valores que podrían
provocar un error. Los guardianes hacen posible probar la exactitud del código.
En la Sección 11.4 veremos una alternativa más flexible que imprime un mensaje de error:
plantear una excepción.
6.9. Depuración
Separar un programa grande en funciones pequeñas crea puntos de control naturales para
la depuración. Si una función no está funcionando, hay tres posibilidades a considerar:
Hay algo mal en los argumentos que obtiene la función: se viola una precondición.
Para descartar la primera posibilidad, puedes agregar una sentencia print al comienzo
de la función y mostrar los valores de los parámetros (y quizás sus tipos). O bien puedes
escribir código que verifique las precondiciones de manera explícita.
Si los parámetros se ven bien, agrega una sentencia print antes de cada sentencia return
y muestra el valor de retorno. Si es posible, verifica el resultado a mano. Considera llamar
a la función con valores que faciliten la verificación del resultado (como en la Sección 6.2).
Si la función parece funcionar, mira la llamada a función para asegurarte de que el valor
de retorno se esté utilizando correctamente (¡o si al menos se está utilizando!).
Agregar sentencias print al comienzo y al final de una función puede ayudar a hacer más
visible el flujo de ejecución. Por ejemplo, aquí hay una versión de factorial con sentencias
print:
60 Capítulo 6. Funciones productivas
def factorial(n):
espacio = ' ' * (4 * n)
print(espacio, 'factorial', n)
if n == 0:
print(espacio, 'devolviendo 1')
return 1
else:
recursivo = factorial(n-1)
resultado = n * recursivo
print(espacio, 'devolviendo', resultado)
return resultado
espacio es una cadena de caracteres de espacio que controla la sangría de la salida. Este es
el resultado de factorial(4) :
factorial 4
factorial 3
factorial 2
factorial 1
factorial 0
devolviendo 1
devolviendo 1
devolviendo 2
devolviendo 6
devolviendo 24
Si estás confundido acerca del flujo de ejecución, este tipo de salidas puede ser útil. Desa-
rrollar andamiaje eficaz toma algo de tiempo, pero un poco de andamiaje puede ahorrar
mucha depuración.
6.10. Glosario
variable temporal: Una variable utilizada para almacenar un valor intermedio en una
computación compleja.
código muerto: Parte de un programa que nunca se ejecuta, a menudo debido a que apa-
rece después de una sentencia return.
guardián: Un patrón de programación que utiliza una sentencia condicional para verificar
y encargarse de circunstancias que podrían provocar un error.
6.11. Ejercicios
Ejercicio 6.1. Dibuja un diagrama de pila para el siguiente programa. ¿Qué imprime el programa?
6.11. Ejercicios 61
def b(z):
prod = a(z, z)
print(z, prod)
return prod
x = 1
y = x + 1
print(c(x, y+3, x+y))
Ejercicio 6.2. La función de Ackermann, A(m, n), se define:
n + 1
si m = 0
A(m, n) = A(m − 1, 1) si m > 0 y n = 0
A(m − 1, A(m, n − 1)) si m > 0 y n > 0.
Ver http: // en. wikipedia. org/ wiki/ Ackermann_ function . Escribe una función con
nombre ack que evalúe la función de Ackermann. Utiliza tu función para evaluar ack(3,
4), que debería ser 125. ¿Qué ocurre para valores más grandes de m y n? Solución: http:
// thinkpython2. com/ code/ ackermann. py .
Ejercicio 6.3. Un palíndromo es una palabra que se deletrea igual hacia atrás y hacia adelante,
como “noon” y “redivider”. De manera recursiva, una palabra es un palíndromo si la primera y la
última letra son la misma y el medio es un palíndromo.
Las siguientes son funciones que toman una cadena como argumento y devuelven las letras primera,
última y del medio:
def primera(palabra):
return palabra[0]
def ultima(palabra):
return palabra[-1]
def medio(palabra):
return palabra[1:-1]
Veremos cómo funcionan en el Capítulo 8.
1. Escribe estas funciones en un archivo con nombre palindromo.py y pruébalas. ¿Qué ocurre
si llamas a medio con una cadena de dos letras? ¿De una letra? ¿Qué pasa con la cadena
vacía, la cual se escribe '' y no contiene letras?
2. Escribe una función llamada es_palindromo que tome una cadena como argumento y de-
vuelva True si es un palíndromo y False si no. Recuerda que puedes utilizar la función
incorporada len para verificar la longitud de una cadena.
62 Capítulo 6. Funciones productivas
Una manera de encontrar el MCD de dos números está basada en la observación de que si r es
el resto de dividir a por b, entonces mcd( a, b) = mcd(b, r ). Como caso base, podemos utilizar
mcd( a, 0) = a.
Escribe una función llamada mcd que tome parámetros a y b y devuelva su máximo común divisor.
Crédito: Este ejercicio está basado en un ejemplo de Structure and Interpretation of Computer
Programs de Abelson y Sussman.
Capítulo 7
Iteración
Este capítulo trata sobre la iteración, que es la capacidad de ejecutar un bloque de sen-
tencias de forma repetida. Vimos un tipo de iteración, utilizando recursividad, en la Sec-
ción 5.8. Vimos otro tipo, utilizando un bucle for, en la Sección 4.2. Es este capítulo veremos
otro tipo, utilizando una sentencia while. Pero primero quiero contar un poco más sobre la
asignación de variables.
7.1. Reasignación
Como habrás descubierto, es legal hacer más de una asignación a la misma variable. Una
nueva asignación hace que una variable existente se refiera a un nuevo valor (y deje de
referirse al valor antiguo).
>>> x = 5
>>> x
5
>>> x = 7
>>> x
7
La primera vez que mostramos x, su valor es 5; la segunda vez, su valor es 7.
En este punto quiero abordar un origen común de confusión. Debido a que Python utiliza
el signo igual (=) para la asignación, es tentador interpretar una sentencia del tipo a = b
como una proposición matemática de igualdad, es decir, la afirmación de que a y b son
iguales. Sin embargo, esta interpretación es incorrecta.
5
x
7
>>> a = 5
>>> b = a # a y b son iguales ahora
>>> a = 3 # a y b ya no son iguales
>>> b
5
La tercera línea cambia el valor de a pero no cambia el valor de b, por lo que ya no son
iguales.
La reasignación de variables es a menudo útil, pero deberías utilizarla con precaución. Si
los valores de las variables cambian frecuentemente, puede hacer que el código sea difícil
de leer y depurar.
Otra es la sentencia while. Aquí hay una versión de cuenta_reg que utiliza una sentencia
while:
def cuenta_reg(n):
while n > 0:
print(n)
n = n - 1
print('½Despegue!')
Casi puedes leer la sentencia while como si fuera inglés. Significa, “Mientras n sea mayor
que 0, muestra el valor de n y luego decrementa n. Cuando llegues a 0, muestra la palabra
½Despegue!”
De manera más formal, aquí está el flujo de ejecución para una sentencia while:
Este tipo de flujo se llama bucle porque el tercer paso hace que vuelva hacia arriba.
El cuerpo del bucle debería cambiar el valor de una o más variables de modo que la condi-
ción se vuelva falsa eventualmente y el bucle termine. De lo contrario, el bucle se repetirá
por siempre, lo cual se llama bucle infinito. Una fuente interminable de diversión para in-
formáticos es la observación de que las instrucciones en un champú, “Enjabonar, enjuagar,
repetir”, son un bucle infinito.
En cada paso por el bucle, el programa muestra el valor de n y luego verifica si es par o
impar. Si es par, n se divide por 2. Si es impar, el valor de n se reemplaza por n*3 + 1. Por
ejemplo, si el argumento pasado a sucesion es 3, los valores resultantes de n son 3, 10, 5,
16, 8, 4, 2, 1.
Dado que n a veces aumenta y a veces disminuye, no hay demostración obvia de que n
alcanzará el 1 alguna vez, o de que el programa termina. Para algunos valores particulares
de n, podemos probar que termina. Por ejemplo, si el valor inicial es una potencia de dos,
66 Capítulo 7. Iteración
n será par cada vez que se pase por el bucle hasta que alcance el 1. El ejemplo anterior
termina con tal sucesión, comenzando con 16.
La pregunta difícil es si podemos probar que este programa termina para todos los valores
positivos de n. Hasta ahora, ¡nadie ha sido capaz de probarlo o refutarlo! (Ver http://en.
wikipedia.org/wiki/Collatz_conjecture.)
Como ejercicio, reescribe la función print_n de la Sección 5.8 utilizando iteración en lugar
de recursividad.
7.4. break
A veces no sabes que es momento de terminar un bucle hasta que llegas a la mitad del
cuerpo. En ese caso puedes utilizar la sentencia break para saltar hacia afuera del bucle.
Por ejemplo, supongamos que quieres tomar la entrada del usuario hasta que se escriba
listo. Podrías escribir:
while True:
linea = input('> ')
if linea == 'listo':
break
print(linea)
print('½Listo!')
La condición del bucle es True, lo cual siempre es verdadero, así que el bucle se ejecuta
hasta que llega a la sentencia break.
En cada paso, solicita la entrada del usuario con un paréntesis angular. Si el usuario escribe
listo, la sentencia break hace que se salga del bucle. De lo contrario, el programa repite
lo que escriba el usuario y regresa a la parte superior del bucle. Aquí hay una ejecución de
muestra:
> no listo
no listo
> listo
½Listo!
Esta forma de escribir bucles while es común porque puedes verificar la condición en cual-
quier lugar del bucle (no solo en la parte superior) y puedes expresar la condición de deten-
ción de manera afirmativa (“detente cuando esto ocurra”) en lugar de negativa (“continúa
hasta que eso no ocurra”).
Por ejemplo, una manera de calcular raíces cuadradas es el método de Newton. Suponga-
mos que quieres saber la raíz cuadrada de a. Si comienzas con casi cualquier estimación, x,
puedes calcular una mejor estimación con la siguiente fórmula:
7.5. Raíces cuadradas 67
x + a/x
y=
2
Por ejemplo, si a es 4 y x es 3:
>>> a = 4
>>> x = 3
>>> y = (x + a/x) / 2
>>> y
2.16666666667
√
El resultado está más cerca de la respuesta correcta ( 4 = 2). Si repetimos el proceso con
una nueva estimación, se acerca aún más:
>>> x = y
>>> y = (x + a/x) / 2
>>> y
2.00641025641
Después de algunas actualizaciones más, la estimación es casi exacta:
>>> x = y
>>> y = (x + a/x) / 2
>>> y
2.00001024003
>>> x = y
>>> y = (x + a/x) / 2
>>> y
2.00000000003
En general, no sabemos de antemano cuántos pasos toma llegar a la respuesta correcta,
pero sabemos cuándo la obtenemos porque la estimación deja de cambiar:
>>> x = y
>>> y = (x + a/x) / 2
>>> y
2.0
>>> x = y
>>> y = (x + a/x) / 2
>>> y
2.0
Cuando y == x, podemos parar. Aquí hay un bucle que comienza con una estimación ini-
cial, x, y la mejora hasta que deja de cambiar:
while True:
print(x)
y = (x + a/x) / 2
if y == x:
break
x = y
Para la mayoría de los valores de a esto funciona bien, pero en general es peligroso probar
la igualdad de números float. Los valores de coma flotante son solo aproximadamente
correctos:
√ la mayoría de los números racionales, como 1/3, y los números irracionales,
como 2, no se pueden representar de manera exacta con un float.
En lugar de verificar si x e y son exactamente iguales, es más seguro utilizar la función abs
para calcular el valor absoluto, o magnitud, de la diferencia entre estos:
68 Capítulo 7. Iteración
7.6. Algoritmos
El método de Newton es un ejemplo de algoritmo: es un proceso mecánico para resolver
una categoría de problemas (en este caso, calcular raíces cuadradas).
Para entender qué es un algoritmo, quizás ayude comenzar con algo que no es un algorit-
mo. Cuando aprendiste a multiplicar números de un solo dígito, probablemente memori-
zaste la tabla de multiplicar. En realidad, memorizaste 100 soluciones específicas. Esa clase
de conocimiento no es algorítmica.
Pero si eras “perezoso”, podrías haber aprendido algunos trucos. Por ejemplo, para encon-
trar el producto de n y 9, puedes escribir n − 1 como el primer dígito y 10 − n como el
segundo dígito. Este truco es una solución general para multiplicar cualquier número de
un solo dígito por 9. ¡Eso es un algoritmo!
Del mismo modo, las técnicas que aprendiste para la suma con reserva, resta con préstamo
y división larga son todas algoritmos. Una de las características de los algoritmos es que no
requieren ninguna inteligencia para realizarlos. Son procesos mecánicos donde cada paso
sigue al último de acuerdo a un conjunto simple de reglas.
Ejecutar algoritmos es aburrido, pero diseñarlos es interesante, intelectualmente desafiante
y una parte central de las ciencias de la computación.
Algunas de las cosas que las personas hacen de manera natural, sin dificultad o pensa-
miento consciente, son las más difíciles de expresar de manera algorítmica. Entender un
lenguaje natural es un buen ejemplo. Todos lo hacemos, pero hasta ahora nadie ha sido
capaz de explicar cómo lo hacemos, al menos no en la forma de un algoritmo.
7.7. Depuración
A medida que comiences a escribir programas más grandes, podrías encontrarte ocupando
más tiempo en la depuración. Más código significa más posibilidades de cometer un error
y más lugares para esconder errores de programación.
Una manera de acortar tu tiempo de depuración es la “depuración por bisección”. Por
ejemplo, si hay 100 líneas en tu programa y las revisas una a la vez, tomaría 100 pasos.
En cambio, intenta separar el problema por la mitad. Mira la mitad del programa, o cerca
de esta, para un valor intermedio que puedas verificar. Agrega una sentencia print (o algo
más que tenga un efecto verificable) y ejecuta el programa.
Si la verificación en el punto medio es incorrecta, debe haber un problema en la primera
mitad del programa. Si es correcta, el problema está en la segunda mitad.
Cada vez que hagas una verificación como esta, reduces a la mitad el número de líneas que
tienes que buscar. Después de seis pasos (lo cual es menos que 100), estarías revisando una
o dos líneas de código, al menos en teoría.
7.8. Glosario 69
En la práctica, no siempre está claro cuál es “la mitad del programa” y no siempre es posible
verificarla. No tiene sentido contar líneas y encontrar el punto medio exacto. En cambio,
piensa en lugares del programa donde podría haber errores y lugares donde es fácil poner
una verificación. Luego, escoge un sitio donde creas que las posibilidades de que el error
esté antes o después de la verificación son casi las mismas.
7.8. Glosario
reasignación: Asignar un nuevo valor a una variable que ya existe.
actualización: Una asignación donde el nuevo valor de una variable depende del antiguo.
inicialización: Una asignación que le da un valor inicial a una variable que será actualiza-
da.
7.9. Ejercicios
Ejercicio 7.1. Copia el bucle de la Sección 7.5 y encapsúlalo en una función llamada mi_sqrt
que tome a a como parámetro, escoja un valor razonable de x y devuelva una estimación de la raíz
cuadrada de a.
Para probarla, escribe una función con nombre probar_raiz_cuadrada que imprima una tabla
como esta:
a mi_sqrt(a) math.sqrt(a) diferencia
- ---------- ------------ ----------
1.0 1.0 1.0 0.0
2.0 1.41421356237 1.41421356237 2.22044604925e-16
3.0 1.73205080757 1.73205080757 0.0
4.0 2.0 2.0 0.0
5.0 2.2360679775 2.2360679775 0.0
6.0 2.44948974278 2.44948974278 0.0
7.0 2.64575131106 2.64575131106 0.0
8.0 2.82842712475 2.82842712475 4.4408920985e-16
9.0 3.0 3.0 0.0
La primera columna es un número, a; la segunda columna es la raíz cuadrada de a calculada con
mi_sqrt; la tercera columna es la raíz cuadrada calculada por math.sqrt; la cuarta columna es el
valor absoluto de la diferencia entre las dos estimaciones.
70 Capítulo 7. Iteración
Ejercicio 7.2. La función incorporada eval toma una cadena y la evalúa utilizando el intérprete
de Python. Por ejemplo:
>>> eval('1 + 2 * 3')
7
>>> import math
>>> eval('math.sqrt(5)')
2.2360679774997898
>>> eval('type(math.pi)')
<class 'float'>
Escribe una función llamada bucle_eval que, de manera iterativa, solicite la entrada del usuario,
tome la entrada resultante y la evalúe utilizando eval, e imprima el resultado.
Debería continuar hasta que el usuario ingrese 'listo' y luego devuelva el valor de la última
expresión que evaluó.
Ejercicio 7.3. El matemático Srinivasa Ramanujan encontró una serie infinita que se puede utilizar
para generar una aproximación numérica de 1/π:
√
1 2 2 ∞ (4k)!(1103 + 26390k )
9801 k∑
=
π =0 (k!)4 3964k
Escribe una función llamada estimacion_pi que utilice esta fórmula para calcular y devolver una
estimación de π. Debería utilizar un bucle while para calcular términos de la sumatoria hasta que
el último término sea más pequeño que 1e-15 (que es la notación de Python para 10−15 ). Puedes
verificar el resultado comparándolo con math.pi.
Cadenas
Las cadenas no son como los enteros, los números de coma flotante y los booleanos. Una
cadena es una secuencia, lo cual significa que es una colección ordenada de valores. En
este capítulo verás cómo acceder a los caracteres que forman una cadena y aprenderás
sobre algunos de los métodos que proporcionan las cadenas.
La expresión en los corchetes se llama índice. El índice indica cuál carácter en la secuencia
quieres (de ahí el nombre).
Como índice, puedes utilizar una expresión que contenga variables y operadores:
>>> i = 1
>>> fruta[i]
72 Capítulo 8. Cadenas
'a'
>>> fruta[i+1]
'n'
Pero el valor del índice tiene que ser un entero. De lo contrario, obtienes:
>>> letra = fruta[1.5]
TypeError: string indices must be integers
8.2. len
len es una función incorporada que devuelve el número de caracteres en una cadena:
>>> fruta = 'banana'
>>> len(fruta)
6
Para obtener la última letra de una cadena, quizás te tientes a intentar algo así:
>>> longitud = len(fruta)
>>> ultima = fruta[longitud]
IndexError: string index out of range
El motivo del IndexError es que no hay letra en 'banana' con el índice 6. Dado que co-
menzamos a contar desde cero, las seis letras se enumeran del 0 al 5. Para obtener el último
carácter, tienes que restar 1 a longitud:
>>> ultima = fruta[longitud-1]
>>> ultima
'a'
O bien puedes utilizar índices negativos, que cuentan hacia atrás desde el final de la cade-
na. La expresión fruta[-1] entrega la última letra, fruta[-2] entrega la penúltima, y así
sucesivamente.
fruta ’ banana’
indice 0 1 2 3 4 5 6
8.6. Buscar
¿Qué hace la siguiente función?
def encontrar(palabra, letra):
indice = 0
while indice < len(palabra):
if palabra[indice] == letra:
return indice
indice = indice + 1
return -1
8.7. Bucles y conteo 75
Este es el primer ejemplo que hemos visto de una sentencia return dentro de un bucle. Si
palabra[indice] == letra, la función se sale del bucle y devuelve inmediatamente.
Si el carácter no aparece en la cadena, el programa termina el bucle de manera normal y
devuelve -1.
Como ejercicio, modifica encontrar para que tenga un tercer parámetro: el índice en
palabra donde debería comenzar la búsqueda.
Como ejercicio, encapsula este código en una función con nombre contar y generalízalo
para que acepte la cadena y la letra como argumentos.
Luego reescribe la función de modo que, en lugar de recorrer la cadena, utilice la versión
de tres parámetros de encontrar de la sección anterior.
Esta forma de notación de punto especifica el nombre del método, upper, y el nombre de
la cadena a la cual se le aplica el método, palabra. Los paréntesis vacíos indican que este
método no toma argumentos.
Una llamada a un método se llama invocación; en este caso, diríamos que estamos invo-
cando a upper en palabra.
Resulta que hay un método de cadena con nombre find que es notablemente similar a la
función encontrar que escribimos:
>>> palabra = 'banana'
>>> indice = palabra.find('a')
>>> indice
1
En este ejemplo, invocamos a find en palabra y pasamos la letra que buscamos como
parámetro.
En realidad, el método find es más general que nuestra función; puede encontrar subca-
denas, no solo caracteres:
>>> palabra.find('na')
2
Por defecto, find comienza al principio de la cadena, pero puede tomar un segundo argu-
mento, el índice donde debería comenzar:
>>> palabra.find('na', 3)
4
Este es un ejemplo de argumento opcional; find puede tomar también un tercer argumen-
to, el índice donde debería detenerse:
>>> nombre = 'bob'
>>> nombre.find('b', 1, 2)
-1
Esta búsqueda falla porque b no aparece en el rango de índices desde 1 hasta 2, sin incluir
el 2. Buscar hasta el segundo índice, sin incluirlo, hace a find consistente con el operador
de trozo.
8.9. El operador in
La palabra in es un operador booleano que toma dos cadenas y devuelve True si la primera
aparece como una subcadena en la segunda:
>>> 'a' in 'banana'
True
>>> 'semilla' in 'banana'
False
Por ejemplo, la siguiente función imprime todas las letras de palabra1 que también apare-
cen en palabra2:
def en_ambas(palabra1, palabra2):
for letra in palabra1:
if letra in palabra2:
print(letra)
8.10. Comparación de cadenas 77
Con nombres de variables bien escogidos, Python a veces se lee como el inglés. Podrías leer
este bucle, “para (cada) letra en (la primera) palabra, si (la) letra (aparece) en (la segunda)
palabra, imprimir (la) letra.”
8.11. Depuración
Cuando utilizas índices para recorrer los valores en una secuencia, es difícil obtener el
comienzo y final de un recorrido de manera correcta. Aquí hay una función que se supone
que compara dos palabras y devuelve True si una de las palabras es el inverso de la otra,
pero contiene dos errores:
def es_inverso(palabra1, palabra2):
if len(palabra1) != len(palabra2):
return False
i = 0
j = len(palabra2)
while j > 0:
if palabra1[i] != palabra2[j]:
78 Capítulo 8. Cadenas
return False
i = i+1
j = j-1
return True
La primera sentencia if verifica si las palabras tienen la misma longitud. Si no, podemos
devolver False inmediatamente. De lo contrario, para el resto de la función, podemos su-
poner que las palabras tienen la misma longitud. Este es un ejemplo del patrón guardián
de la Sección 6.8.
i y j son índices: i recorre a palabra1 hacia adelante mientras j recorre a palabra2 hacia
atrás. Si encontramos dos letras que no coinciden, podemos devolver False inmediata-
mente. Si terminamos todo el bucle y todas las letras coinciden, devolvemos True.
Si probamos esta función con las palabras “pots” y “stop”, esperamos el valor de retorno
True, pero obtenemos un IndexError:
>>> es_inverso('pots', 'stop')
...
File "inverso.py", line 15, in es_inverso
if palabra1[i] != palabra2[j]:
IndexError: string index out of range
Para depurar este tipo de error, mi primer movimiento es imprimir los valores de los índi-
ces inmediatamente antes de la línea donde aparece el error.
while j > 0:
print(i, j) # imprimir aquí
if palabra1[i] != palabra2[j]:
return False
i = i+1
j = j-1
Ahora cuando ejecuto el programa de nuevo, obtengo más información:
>>> es_inverso('pots', 'stop')
0 4
...
IndexError: string index out of range
En el primer paso por el bucle, el valor de j es 4, lo cual está fuera de rango para la cadena
'pots'. El índice del último carácter es 3, por lo que el valor inicial para j debería ser
len(palabra2)-1.
Si arreglo este error y ejecuto el programa de nuevo, obtengo:
>>> es_inverso('pots', 'stop')
0 3
1 2
2 1
True
Esta vez obtenemos la respuesta correcta, pero se ve como si el bucle solo se ejecutara tres
veces, lo cual es sospechoso. Para obtener una mejor idea de lo que está ocurriendo, es útil
dibujar un diagrama de estado. Durante la primera iteración, el marco para es_inverso se
muestra en la Figura 8.2.
8.12. Glosario 79
i 0 j 3
Me tomé la licencia de organizar las variables en el marco y agregar líneas punteadas para
mostrar que los valores de i y j indican caracteres en palabra1 y palabra2.
Comenzando con este diagrama, ejecuta el programa en papel, cambiando los valores de i
y j durante cada iteración. Encuentra y arregla el segundo error en esta función.
8.12. Glosario
objeto: Algo a lo cual una variable puede referirse. Por ahora, puedes utilizar “objeto” y
“valor” indistintamente.
secuencia: Una colección ordenada de valores donde cada valor se identifica por un índice
entero.
ítem: Uno de los valores en una secuencia.
índice: Un valor entero utilizado para seleccionar un ítem en una secuencia, tal como un
carácter en una cadena. En Python, los índices parten desde 0.
trozo (slice): Una parte de una cadena especificada por un rango de índices.
cadena vacía: Una cadena sin caracteres y con longitud 0, representada por dos comillas.
inmutable: La propiedad de una secuencia cuyos ítems no pueden cambiarse.
recorrer: Iterar a través de los ítems en una secuencia, realizando una operación similar en
cada uno de estos.
búsqueda: Un patrón de un recorrido que se detiene cuando encuentra lo que busca.
contador: Una variable utilizada para contar algo, generalmente inicializada en cero y lue-
go incrementada.
invocación: Una sentencia que llama a un método.
argumento opcional: Un argumento de función o de método que no es obligatorio.
8.13. Ejercicios
Ejercicio 8.1. Lee la documentación de los métodos de cadena en http: // docs. python. org/
3/ library/ stdtypes. html# string-methods . Tal vez quieras experimentar con algunos para
asegurarte de que entiendes cómo funcionan. strip y replace son particularmente útiles.
La documentación utiliza una sintaxis que podría confundir. Por ejemplo, en
find(sub[, start[, end]]), los corchetes indican argumentos opcionales. Entonces sub
es obligatorio, pero start es opcional, y si incluyes start, entonces end es opcional.
80 Capítulo 8. Cadenas
Ejercicio 8.2. Hay un método de cadena llamado count que es similar a la función de la Sección 8.7.
Lee la documentación de este método y escribe una invocación que cuente el número de letras a en
'banana'.
Ejercicio 8.3. Un trozo de cadena puede tomar un tercer índice que especifique el “tamaño de paso”,
es decir, el número de espacios entre caracteres sucesivos. Un tamaño de paso de 2 significa cada dos
caracteres, 3 significa cada tres, etc.
>>> fruta = 'banana'
>>> fruta[0:5:2]
'bnn'
Un tamaño de paso de -1 pasa a través de la palabra hacia atrás, por lo que el trozo [::-1] genera
una cadena invertida.
Utiliza esta notación para escribir una versión de una línea de es_palindromo del Ejercicio 6.3.
Ejercicio 8.4. Las siguientes funciones tienen la intención de verificar si una cadena contiene
al menos una letra minúscula, pero algunas son incorrectas. Para cada función, describe qué hace
realmente la función (suponiendo que el parámetro es una cadena).
def contiene_minuscula1(s):
for c in s:
if c.islower():
return True
else:
return False
def contiene_minuscula2(s):
for c in s:
if 'c'.islower():
return 'True'
else:
return 'False'
def contiene_minuscula3(s):
for c in s:
flag = c.islower()
return flag
def contiene_minuscula4(s):
flag = False
for c in s:
flag = flag or c.islower()
return flag
def contiene_minuscula5(s):
for c in s:
if not c.islower():
return False
return True
Ejercicio 8.5. Un cifrado César es una forma débil de encriptación que implica la “rotación” de
cada letra en un número fijo de lugares. Rotar una letra significa desplazarla a través del alfabeto,
volviendo al comienzo si es necesario, por lo que ’A’ rotada en 3 es ’D’ y ’Z’ rotada en 1 es ’A’.
8.13. Ejercicios 81
Para rotar una palabra, rota cada letra en la misma cantidad. Por ejemplo, “cheer” rotada en 7 es
“jolly” y “melon” rotada en -10 es “cubed”. En la película 2001: Odisea del espacio, el computador
de la nave se llama HAL, que es IBM rotada en -1.
Escribe una función llamada rotar_palabra que tome una cadena y un entero como parámetros
y devuelva una cadena nueva que contenga las letras de la cadena original rotadas en la cantidad
entregada.
Tal vez quieras utilizar la función incorporada ord, que convierte un carácter en un código numéri-
co, y chr, que convierte códigos numéricos en caracteres. Las letras del alfabeto están codificadas en
orden alfabético, así por ejemplo:
>>> ord('c') - ord('a')
2
Debido a que 'c' es la dos-ésima letra del alfabeto. Pero ten cuidado: los códigos numéricos para las
letras mayúsculas son diferentes.
Los chistes potencialmente ofensivos en internet a veces están codificados en ROT13, que es un
cifrado César con rotación 13. Si no te ofendes fácilmente, encuentra y decodifica algunos. Solución:
http: // thinkpython2. com/ code/ rotate. py .
82 Capítulo 8. Cadenas
Capítulo 9
Este capítulo presenta el segundo estudio de caso, el cual involucra resolver puzles de
palabras buscando palabras que tengan ciertas propiedades. Por ejemplo, encontraremos
los palíndromos más largos en inglés y buscaremos palabras cuyas letras aparezcan en
orden alfabético. Además, presentaré otro plan de desarrollo de programa: reducción a un
problema previamente resuelto.
Este archivo está en texto plano, así que puedes abrirlo con un editor de texto, pero tam-
bién puedes leerlo desde Python. La función incorporada open toma el nombre del archivo
como parámetro y devuelve un objeto de archivo que puedes utilizar para leer dicho ar-
chivo.
>>> fin = open('words.txt')
fin es un nombre común para un objeto de archivo utilizado para la entrada (file input). El
objeto de archivo proporciona varios métodos para la lectura, incluyendo readline, que
lee caracteres desde un archivo hasta que llega a una nueva línea y devuelve el resultado
como una cadena:
>>> fin.readline()
'aa\n'
La primera palabra de esta particular lista es “aa”, que es un tipo de lava. La secuencia \n
representa el carácter nueva línea que separa esta palabra de la siguiente.
84 Capítulo 9. Estudio de caso: juego de palabras
El objeto de archivo hace un seguimiento del lugar del archivo en donde este se encuentra
en un instante determinado, así que si llamas a readline de nuevo, obtienes la palabra
siguiente:
>>> fin.readline()
'aah\n'
La palabra siguiente es “aah”, que es una palabra perfectamente legítima, así que deja de
mirarme así. O bien, si es el carácter nueva línea lo que te molesta, podemos deshacernos
de este con el método de cadena strip:
>>> linea = fin.readline()
>>> palabra = linea.strip()
>>> palabra
'aahed'
Puedes también utilizar un objeto de archivo como parte de un bucle for. Este programa
lee words.txt e imprime cada palabra, una por línea:
fin = open('words.txt')
for linea in fin:
palabra = linea.strip()
print(palabra)
9.2. Ejercicios
Hay soluciones a estos ejercicios en la siguiente sección. Deberías al menos intentar cada
uno antes de leer las soluciones.
Ejercicio 9.1. Escribe un programa que lea words.txt e imprima solo las palabras con más de 20
caracteres (sin contar espacios en blanco).
Ejercicio 9.2. En 1939, Ernest Vincent Wright publicó una novela de 50.000 palabras llamada
Gadsby, la cual no contiene la letra “e”. Dado que la “e” es la letra más común en el idioma inglés,
no es una tarea fácil.
Sin duda, solo imaginar una oración sin utilizar dicho símbolo tan común implica una actividad
difícil. Si tardas mucho al principio, hazlo con cuidado y trabaja horas para adquirir la habilidad
poco a poco.
Escribe una función llamada no_tiene_e que devuelva True si la palabra dada no incluye la letra
“e”.
Escribe un programa que lea words.txt e imprima solo las palabras que no tienen “e”. Calcula el
porcentaje de palabras en la lista que no tienen “e”.
Ejercicio 9.3. Escribe una función con nombre excluye que tome una palabra y una cadena de
letras prohibidas y devuelva True si la palabra no utiliza ninguna de las letras prohibidas.
Escribe un programa que solicite al usuario ingresar una cadena de letras prohibidas y luego imprima
el número de palabras que no contienen ninguna de estas. ¿Puedes encontrar una combinación de 5
letras prohibidas que excluya al menor número de palabras?
Ejercicio 9.4. Escribe una función con nombre usa_solo que tome una palabra y una cadena de
letras y devuelva True si la palabra contiene solo letras de la lista. ¿Puedes crear una oración en
inglés utilizando solo las letras acefhlo? ¿Una distinta a “Hoe alfalfa”?
9.3. Búsqueda 85
Ejercicio 9.5. Escribe una función con nombre usa_todas que tome una palabra y una cadena de
letras requeridas y devuelva True si la palabra utiliza todas las letras requeridas al menos una vez.
¿Cuántas palabras que utilizan todas las vocales aeiou existen? ¿Qué pasa con aeiouy?
Ejercicio 9.6. Escribe una función llamada es_abecedario que devuelva True si las letras en
una palabra aparecen en orden alfabético (las letras dobles están permitidas). ¿Cuántas palabras
abecedarias existen?
9.3. Búsqueda
Todos los ejercicios de la sección anterior tienen algo en común: pueden ser resueltos con
el patrón de búsqueda que vimos en la Sección 8.6. El ejemplo más simple es:
def no_tiene_e(palabra):
for letra in palabra:
if letra == 'e':
return False
return True
El bucle for recorre los caracteres en palabra. Si encontramos la letra “e”, podemos de-
volver False inmediatamente; de lo contrario, tenemos que ir a la siguiente letra. Si ter-
minamos el bucle de manera normal, significa que no encontramos una “e”, por lo cual
devolvemos True.
Podrías haber escrito esta función de manera más concisa utilizando el operador in, pero
comencé con esta versión porque demuestra la lógica del patrón de búsqueda.
excluye es una versión más general de no_tiene_e pero tiene la misma estructura:
def excluye(palabra, prohibidas):
for letra in palabra:
if letra in prohibidas:
return False
return True
Podemos devolver False apenas encontremos una letra prohibida; si llegamos al final del
bucle, devolvemos True.
En lugar de recorrer las letras en palabra, el bucle recorre las letras requeridas. Si alguna
de las letras requeridas no aparece en la palabra, podemos devolver False.
Si realmente estuvieras pensando como un informático, habrías reconocido que usa_todas
era una instancia de un problema previamente resuelto, y habrías escrito:
def usa_todas(palabra, requeridas):
return usa_solo(requeridas, palabra)
Este es un ejemplo de un plan de desarrollo de programa llamado reducción a un pro-
blema previamente resuelto, lo cual significa que reconoces el problema en el que estás
trabajando como una instancia de un problema resuelto y aplicas una solución existente.
'flossy'. La longitud de la palabra es 6, por lo cual la última vez que el bucle se ejecuta
es cuando i es 4, que es el índice del penúltimo carácter. En la última iteración, compara el
penúltimo carácter con el último, que es lo que queremos.
Aquí hay una versión de es_palindromo (ver Ejercicio 6.3) que utiliza dos índices: uno
comienza al principio y aumenta, el otro comienza al final y disminuye.
def es_palindromo(palabra):
i = 0
j = len(palabra)-1
while i<j:
if palabra[i] != palabra[j]:
return False
i = i+1
j = j-1
return True
O bien podríamos reducir a un problema previamente resuelto y escribir
def es_palindromo(palabra):
return es_inverso(palabra, palabra)
utilizando es_inverso de la Sección 8.11.
9.5. Depuración
Probar programas es difícil. Las funciones de este capítulo son relativamente fáciles de
probar porque puedes verificar los resultados a mano. Aun así, escoger un conjunto de
palabras que pruebe todos los errores posibles está en algún lugar entre difícil e imposible.
Tomando a no_tiene_e como ejemplo, hay dos casos obvios para verificar: las palabras que
tienen una ‘e’ deberían devolver False y las palabras que no la tienen deberían devolver
True. No deberías tener problemas para proponer una palabra de cada caso.
Dentro de cada caso, hay algunos subcasos menos obvios. Entre las palabras que tienen
una “e”, deberías probar palabras con una “e” al principio, al final y en algún lugar del
medio. Deberías probar palabras largas, palabras cortas y palabras muy cortas, como la
cadena vacía. La cadena vacía es un ejemplo de un caso especial, que es uno de los casos
no obvios donde los errores a menudo acechan.
Además de los casos de prueba que generes, puedes también probar tu programa con una
lista de palabras como words.txt. Escudriñando la salida, podrías ser capaz de captar los
errores, pero ten cuidado: podrías captar un tipo de error (palabras que no deberían estar
incluidas, pero lo están) y otro no (palabras que deberían estar incluidas, pero no lo están).
En general, las pruebas pueden ayudarte a encontrar errores, pero no es fácil generar un
buen conjunto de casos de prueba, e incluso si lo haces, no puedes asegurarte de que tu
programa está correcto. De acuerdo al legendario informático:
9.6. Glosario
objeto de archivo: Un valor que representa un archivo abierto.
caso especial: Un caso de prueba que es atípico o no obvio (y menos probable de abordar
correctamente).
9.7. Ejercicios
Ejercicio 9.7. Esta pregunta está basada en un Puzzler que fue transmitido en el programa de radio
Car Talk (http: // www. cartalk. com/ content/ puzzlers ):
Dame una palabra con tres letras dobles consecutivas. Te daré un par de palabras que
casi califican, pero no. Por ejemplo, la palabra committee, c-o-m-m-i-t-t-e-e. Estaría ex-
celente, si no fuera por la ‘i’ que se cuela allí. O Mississippi: M-i-s-s-i-s-s-i-p-p-i. Si
pudieras sacar esas ‘i’ funcionaría. Pero hay una palabra que tiene tres pares de letras
consecutivos y, por lo que sé, puede ser la única palabra. Por supuesto que probable-
mente hay 500 más pero solo puedo pensar en una. ¿Cuál es la palabra?
Escribe un programa en Python que pruebe todos los números de seis dígitos e imprima aque-
llos números que satisfagan estos requisitos. Solución: http: // thinkpython2. com/ code/
cartalk2. py .
Ejercicio 9.9. Aquí hay otro Puzzler de Car Talk que puedes resolver con una búsqueda (http:
// www. cartalk. com/ content/ puzzlers ):
“Recientemente tuve una visita con mi mamá y me di cuenta de que los dos dígitos que
componen mi edad cuando se invierten resulta en su edad. Por ejemplo, si ella tiene 73,
yo tengo 37. Nos preguntamos cuán a menudo ha ocurrido esto a través de los años pero
nos desviamos a otros temas y nunca dimos con una respuesta.
9.7. Ejercicios 89
“Cuando llegué a casa descubrí que los dígitos de nuestras edades han sido reversibles
seis veces hasta ahora. También descubrí que, si tenemos suerte, ocurriría de nuevo en
unos años, y si realmente tenemos suerte ocurriría una vez más después de eso. En
otras palabras, habría ocurrido 8 veces en total. Entonces la pregunta es, ¿qué edad
tengo ahora?”
Escribe un programa en Python que busque soluciones a este Puzzler. Pista: podrías encontrar útil
el método de cadena zfill.
Listas
Este capítulo presenta uno de los tipos incorporados más útiles de Python: las listas. Ade-
más, aprenderás más sobre objetos y lo que puede ocurrir cuando tienes más de un nombre
para el mismo objeto.
Hay varias maneras de crear una lista nueva; la más simple es encerrar los elementos en
corchetes ([ y ]):
[10, 20, 30, 40]
['crunchy frog', 'ram bladder', 'lark vomit']
El primer ejemplo es una lista de cuatro enteros. El segundo es una lista de tres cadenas.
Los elementos de una lista no tienen que ser del mismo tipo. La siguiente lista contiene una
cadena, un número de coma flotante, un entero y (¡atención!) otra lista:
['spam', 2.0, 5, [10, 20]]
Una lista dentro de otra lista está anidada.
Una lista que no contiene elementos se llama lista vacía; puedes crear una con corchetes
vacíos, [].
list
quesos 0 ’Cheddar’
1 ’Edam’
2 ’Gouda’
list
numeros 0 42
1 123
5
list
vacio
Las listas se representan por cajas con la palabra “list” por fuera y los elementos de la lista
por dentro. quesos se refiere a una lista con tres elementos con índices 0, 1 y 2. numeros
contiene dos elementos; el diagrama muestra que el valor del segundo elemento ha sido
reasignado de 123 a 5. vacio se refiere a una lista sin elementos.
Los índices de las listas funcionan de la misma manera que los índices de las cadenas:
Si un índice tiene un valor negativo, se cuenta hacia atrás desde el final de la lista.
isupper es un método de cadena que devuelve True si la cadena solo contiene letras ma-
yúsculas.
Una operación como solo_mayusculas se llama filtro porque selecciona algunos de los
elementos y filtra los otros.
La mayoría de las operaciones de lista se pueden expresar como una combinación de mapa,
filtro y reducción.
>>> s = 'spam'
>>> t = list(s)
>>> t
['s', 'p', 'a', 'm']
Dado que list es el nombre de una función incorporada, deberías evitar utilizarlo como
nombre de variable. Yo además evito l porque se parece mucho a 1. Entonces por eso uso
t.
La función list separa la cadena en letras individuales. Si quieres separar una cadena en
palabras, puedes utilizar el método split:
>>> s = 'pining for the fjords'
>>> t = s.split()
>>> t
['pining', 'for', 'the', 'fjords']
Un argumento opcional llamado delimitador especifica qué caracteres usar como separa-
dor de palabras. El siguiente ejemplo usa un guión como delimitador:
>>> s = 'spam-spam-spam'
>>> delimitador = '-'
>>> t = s.split(delimitador)
>>> t
['spam', 'spam', 'spam']
join es el inverso de split. Toma una lista de cadenas y concatena los elementos. join es
un método de cadena, por lo que tienes que invocarlo en el delimitador y pasarle la lista
como parámetro:
>>> t = ['pining', 'for', 'the', 'fjords']
>>> delimitador = ' '
>>> s = delimitador.join(t)
>>> s
'pining for the fjords'
En este caso el delimitador es un carácter de espacio, por lo que join pone un espacio entre
las palabras. Para concatenar cadenas sin espacios, puedes usar la cadena vacía, '', como
delimitador.
En el primer caso, a y b se refieren a dos objetos diferentes que tienen el mismo valor. En el
segundo caso, se refieren al mismo objeto.
Para verificar si dos variables se refieren al mismo objeto, puedes usar el operador is.
98 Capítulo 10. Listas
a ’banana’ a
’banana’
b ’banana’ b
a [ 1, 2, 3 ]
b [ 1, 2, 3 ]
>>> a = 'banana'
>>> b = 'banana'
>>> a is b
True
En este ejemplo, Python solo crea un objeto de cadena y tanto a como b se refieren a este.
Pero cuando creas dos listas, obtienes dos objetos:
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a is b
False
Entonces el diagrama de estado se ve como la Figura 10.3.
En este caso diríamos que las dos listas son equivalentes, porque tienen los mismos ele-
mentos, pero no idénticos, porque no son el mismo objeto. Si dos objetos son idénticos, son
también equivalentes, pero si son equivalentes, no necesariamente son idénticos.
Hasta ahora, hemos estado utilizando “objeto” y “valor” indistintamente, pero es más pre-
ciso decir que un objeto tiene un valor. Si evalúas [1, 2, 3], obtienes un objeto de lista
cuyo valor es una secuencia de enteros. Si otra lista tiene los mismos elementos, decimos
que tiene el mismo valor, pero no es el mismo objeto.
10.11. Alias
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
True
El diagrama de estado se ve como la Figura 10.4.
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, por lo que decimos que el
objeto tiene un alias.
Si el objeto con alias es mutable, los cambios realizados con un alias afectan al otro:
10.12. Argumentos de lista 99
a
[ 1, 2, 3 ]
b
list
__main__ letras
0 ’a’
1 ’b’
sin_cabeza t
2 ’c’
>>> b[0] = 42
>>> a
[42, 2, 3]
Aunque este comportamiento puede ser útil, es propenso a errores. En general, es más
seguro evitar los alias cuando trabajes con objetos mutables.
Para los objetos inmutables como las cadenas, los alias no son tan problemáticos. En este
ejemplo:
a = 'banana'
b = 'banana'
casi nunca hace una diferencia si a y b se refieren a la misma cadena o no.
Dado que la lista es compartida por dos marcos, la dibujé entre estos.
Es importante distinguir entre operaciones que modifican listas y operaciones que crean
nuevas listas. Por ejemplo, el método append modifica una lista, pero el operador + crea
una nueva lista.
>>> t1 = [1, 2]
>>> t2 = t1.append(3)
>>> t1
[1, 2, 3]
>>> t2
None
>>> t3 = t1 + [4]
>>> t1
[1, 2, 3]
>>> t3
[1, 2, 3, 4]
Esta diferencia es importante cuando escribes funciones que se supone que modifican lis-
tas. Por ejemplo, esta función no elimina la cabeza de una lista:
def sin_cabeza_mal(t):
t = t[1:] # ½INCORRECTO!
El operador de trozo crea una nueva lista y la asignación hace que t se refiera a esta, pero
eso no afecta a la llamadora.
>>> t4 = [1, 2, 3]
>>> sin_cabeza_mal(t4)
>>> t4
[1, 2, 3]
Una alternativa es escribir una función que cree y devuelva una nueva lista. Por ejemplo,
cola devuelve todos los elementos de una lista excepto el primero:
def cola(t):
return t[1:]
Esta función deja a la lista original sin modificar. Se utiliza de la siguiente manera:
>>> letra = ['a', 'b', 'c']
>>> resto = cola(letras)
>>> resto
['b', 'c']
10.13. Depuración
El uso descuidado de las listas (y otros objetos mutables) puede llevar a largas horas de
depuración. Aquí hay algunas trampas comunes y maneras de evitarlas:
10.13. Depuración 101
10.14. Glosario
lista: Una secuencia de valores.
elemento: Uno de los valores en una lista (u otra secuencia), también llamados ítems.
asignación aumentada: Una sentencia que actualiza el valor de una variable utilizando un
operador como +=.
reducción: Un patrón de procesamiento que recorre una secuencia y acumula los elemen-
tos en un solo resultado.
mapa: Un patrón de procesamiento que recorre una secuencia y realiza una operación en
cada elemento.
filtro: Un patrón de procesamiento que recorre una lista y selecciona los elementos que
satisfacen algún criterio.
objeto: Algo a lo cual una variable puede referirse. Un objeto tiene un tipo y un valor.
alias: Una circunstancia donde dos o más variables se refieren al mismo objeto.
delimitador: Un carácter o cadena utilizado para indicar dónde debería separarse una ca-
dena.
10.15. Ejercicios
Puedes descargar las soluciones a estos ejercicios en http://thinkpython2.com/code/
list_exercises.py.
Ejercicio 10.1. Escribe una función llamada suma_anidada que tome una lista de listas de enteros
y sume los elementos de todas las listas anidadas. Por ejemplo:
>>> t = [[1, 2], [3], [4, 5, 6]]
>>> suma_anidada(t)
21
Ejercicio 10.2. Escribe una función llamada cumsum que tome una lista de números y devuelva la
suma acumulativa, es decir, una lista nueva donde el i-ésimo elemento es la suma de los primeros
i + 1 elementos de la lista original. Por ejemplo:
>>> t = [1, 2, 3]
>>> cumsum(t)
[1, 3, 6]
Ejercicio 10.3. Escribe una función llamada medio que tome una lista y devuelva una nueva lista
que contenga todos los elementos excepto el primero y el último. Por ejemplo:
10.15. Ejercicios 103
>>> t = [1, 2, 3, 4]
>>> medio(t)
[2, 3]
Ejercicio 10.4. Escribe una función llamada acortar que tome una lista, la modifique eliminando
el primer y último elemento, y devuelva None. Por ejemplo:
>>> t = [1, 2, 3, 4]
>>> acortar(t)
>>> t
[2, 3]
Ejercicio 10.5. Escribe una función llamada esta_ordenada que tome una lista como parámetro
y devuelva True si la lista está ordenada de manera ascendente y False si no. Por ejemplo:
>>> esta_ordenada([1, 2, 2])
True
>>> esta_ordenada(['b', 'a'])
False
Ejercicio 10.6. Dos palabras son anagramas si puedes reordenar las letras de una para escribir
la otra. Escribe una función llamada es_anagrama que tome dos cadenas y devuelva True si son
anagramas.
Ejercicio 10.7. Escribe una función llamada tiene_duplicados que tome una lista y devuelva
True si hay algún elemento que aparece más de una vez. No debería modificar la lista original.
Ejercicio 10.8. Este ejercicio está relacionado con la denominada “Paradoja del cumpleaños”, de la
cual puedes leer en http: // en. wikipedia. org/ wiki/ Birthday_ paradox .
Si hay 23 estudiantes en tu clase, ¿cuáles son las posibilidades de que dos de ustedes estén de cum-
pleaños el mismo día? Puedes estimar esta probabilidad generando muestras al azar de 23 cumplea-
ños y verificar coincidencias. Pista: puedes generar cumpleaños aleatorios con la función randint
del módulo random.
De cualquier manera, cortas por la mitad el espacio de búsqueda que queda. Si la lista de palabras
tiene 113,809 palabras, tomará alrededor de 17 pasos para encontrar la palabra o concluir que no
está.
Escribe una función llamada in_bisect que tome una lista ordenada y un valor objetivo, y devuelva
True si la palabra está en la lista y False si no está.
¡O bien podrías leer la documentación del módulo bisect y utilizarlo! Solución: http: //
thinkpython2. com/ code/ inlist. py .
104 Capítulo 10. Listas
Ejercicio 10.11. Dos palabras son un “par inverso” si cada una es el inverso de la otra. Escribe
un programa que encuentre todos los pares inversos en la lista de palabras. Solución: http: //
thinkpython2. com/ code/ reverse_ pair. py .
Ejercicio 10.12. Dos palabras se “entrelazan” si tomando letras alternas de cada una se forma
una nueva palabra. Por ejemplo, “shoe” y “cold” se entrelazan para formar “schooled”. Solución:
http: // thinkpython2. com/ code/ interlock. py . Crédito: Este ejercicio está inspirado en
un ejemplo de http: // puzzlers. org .
1. Escribe un programa que encuentre todos los pares de palabras que se entrelazan. Pista: ¡no
revises todos los pares!
2. ¿Puedes encontrar alguna palabra que sea un triple entrelazado? Es decir, una palabra que si
se lee cada tres letras, comenzando con la primera, la segunda o la tercera letra, se forma una
nueva palabra.
Capítulo 11
Diccionarios
Este capítulo presenta otro tipo incorporado llamado diccionario. Los diccionarios son una
de las mejores características de Python: son los bloques de construcción de muchos algo-
ritmos eficientes y elegantes.
Un diccionario contiene una colección de índices, que se llaman claves, y una colección de
valores. Cada clave está asociada a un valor único. La asociación de una clave y un valor
se llama par clave-valor, o a veces ítem.
La función dict crea un nuevo diccionario sin ítems. Como dict es el nombre de una
función incorporada, deberías evitar utilizarla como nombre de variable.
>>> ing_esp = dict()
>>> ing_esp
{}
Las llaves, {}, representan un diccionario vacío. Para agregar ítems al diccionario, puedes
utilizar corchetes:
>>> ing_esp['one'] = 'uno'
Esta línea crea un ítem que mapea de la clave 'one' al valor 'uno'. Si imprimimos el
diccionario de nuevo, vemos un par clave-valor con un signo de dos puntos entre la clave
y el valor:
>>> ing_esp
{'one': 'uno'}
106 Capítulo 11. Diccionarios
Este formato de salida es también un formato de entrada. Por ejemplo, puedes crear un
nuevo diccionario con tres ítems:
>>> ing_esp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
Pero si imprimes ing_esp, quizás te sorprendas:
>>> ing_esp
{'one': 'uno', 'three': 'tres', 'two': 'dos'}
El orden de los pares clave-valor podría no ser el mismo. Si escribiste el mismo ejemplo en
tu computador, podrías obtener un resultado diferente. En general, el orden de los ítems
en un diccionario es impredecible.
Sin embargo, eso no es un problema porque los elementos de un diccionario nunca se
indexan con índices enteros. En cambio, utilizas las claves para buscar los valores corres-
pondientes:
>>> ing_esp['two']
'dos'
La clave 'two' siempre mapea al valor 'dos', entonces el orden de los ítems no importa.
Si la clave no está en el diccionario, obtienes una excepción:
>>> ing_esp['four']
KeyError: 'four'
La función len funciona con diccionarios: devuelve el número de pares clave-valor.
>>> len(ing_esp)
3
El operador in también funciona con diccionarios: te dice si algo aparece como una clave
en el diccionario (aparecer como un valor no basta).
>>> 'one' in ing_esp
True
>>> 'uno' in ing_esp
False
Para ver si algo aparece como un valor en un diccionario, puedes utilizar el método values,
el cual devuelve una colección de valores, y entonces utilizar el operador in:
>>> valores = ing_esp.values()
>>> 'uno' in valores
True
El operador in utiliza diferentes algoritmos para las listas y los diccionarios. Para las listas,
busca los elementos de la lista en orden, como en la Sección 8.6. A medida que la lista se
vuelve más larga, el tiempo de búsqueda se hace más largo en proporción directa.
Los diccionarios de Python utilizan una estructura de datos llamada tabla hash que tiene
una propiedad notable: el operador in toma casi la misma cantidad de tiempo sin importar
cuántos ítems hay en el diccionario. Explico cómo eso es posible en la Sección B.4, pero la
explicación podría no tener sentido hasta que hayas leído algunos capítulos más.
1. Podrías crear 26 variables, una para cada letra del alfabeto. Luego podrías recorrer la
cadena y, para cada carácter, incrementar el contador correspondiente, probablemen-
te utilizando un condicional encadenado.
2. Podrías crear una lista de 26 elementos. Luego podrías convertir cada carácter a un
número (usando la función incorporada ord), utilizar el número como un índice den-
tro de la lista e incrementar el contador apropiado.
3. Podrías crear un diccionario con caracteres como claves y contadores como los va-
lores correspondientes. La primera vez que veas un carácter, añadirías un ítem al
diccionario. Después de eso incrementarías el valor de un ítem existente.
Cada una de estas opciones realiza la misma computación, pero cada una de ellas imple-
menta esa computación de una manera diferente.
La primera línea de la función crea un diccionario vacío. El bucle for recorre la cadena. En
cada paso por el bucle, si el carácter c no está en el diccionario, creamos un nuevo ítem
con clave c y valor inicial 1 (dado que hemos visto esta letra una vez). Si c ya está en el
diccionario, incrementamos d[c].
Funciona así:
>>> h = histograma('brontosaurus')
>>> h
{'a': 1, 'b': 1, 'o': 2, 'n': 1, 's': 2, 'r': 2, 'u': 2, 't': 1}
El histograma indica que las letras 'a' y 'b' aparecen una vez; 'o' aparece dos veces, y
así sucesivamente.
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:
>>> h = histograma('a')
>>> h
{'a': 1}
>>> h.get('a', 0)
108 Capítulo 11. Diccionarios
1
>>> h.get('c', 0)
0
Como ejercicio, usa get para escribir histograma de manera más concisa. Deberías ser
capaz de eliminar la sentencia if.
Esta función es otro ejemplo de patrón de búsqueda pero que utiliza una característica que
no hemos visto antes, raise. La sentencia raise causa una excepción; en este caso causa un
LookupError, que es una excepción incorporada que se utiliza para indicar que falló una
operación de consulta.
Si se llega al final del bucle, significa que v no aparece en el diccionario como valor, por lo
que plantea una excepción.
Aquí hay un ejemplo de una consulta inversa eficaz:
>>> h = histograma('parrot')
>>> clave = consulta_inversa(h, 2)
>>> clave
'r'
Y una ineficaz:
>>> clave = consulta_inversa(h, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in consulta_inversa
LookupError
El efecto de cuando levantas una excepción mediante raise es el mismo que cuando
Python levanta una: imprime un rastreo y un mensaje de error.
Cuando levantes una excepción, puedes proporcionar un mensaje de error detallado como
argumento opcional. Por ejemplo:
>>> raise LookupError('el valor no aparece en el diccionario')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
LookupError: el valor no aparece en el diccionario
Una consulta inversa es mucho más lenta que una consulta directa; si tienes que hacer-
lo a menudo, o si el diccionario se vuelve grande, el rendimiento de tu progama se verá
afectado.
En cada paso por el bucle, clave obtiene una clave de d y valor obtiene el valor correspon-
diente. Si valor no está en inverso, lo cual significa que no lo hemos visto antes, entonces
creamos un nuevo ítem y lo inicializamos con un singleton (una lista que contiene un úni-
co elemento). De lo contrario, hemos visto este valor antes, por lo cual anexamos a la lista
la clave correspondiente.
Las listas pueden ser valores en un diccionario, tal como muestra este ejemplo simple, pero
no pueden ser claves. Esto es lo que ocurre si lo intentas:
>>> t = [1, 2, 3]
>>> d = dict()
>>> d[t] = 'ups'
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: list objects are unhashable
Anteriormente mencioné que un diccionario se implementa utilizando una tabla hash y
eso significa que las claves tienen que ser hashables.
Un hash es una función que toma un valor (de cualquier tipo) y devuelve un entero. Los
diccionarios utilizan estos enteros, llamados valores hash, para almacenar y consultar pares
clave-valor.
Este sistema funciona bien si las claves son inmutables. Pero si las claves son mutables,
como las listas, ocurren cosas malas. Por ejemplo, cuando creas un par clave-valor, Python
hashea la clave y la almacena en la ubicación correspondiente. Si modificas la clave y luego
la hasheas de nuevo, iría a una ubicación diferente. En ese caso podrías tener dos entradas
11.6. Memos 111
fibonacci
n 4
fibonacci fibonacci
n 3 n 2
fibonacci fibonacci
n 1 n 0
para la misma clave, o quizás no seas capaz de encontrar una clave. De cualquier manera,
el diccionario no funcionaría de manera correcta.
Por eso es que las claves tienen que ser hashables y, por la misma razón, los tipos mutables
como las listas no lo son. La manera más simple de evitar esta limitación es utilizar tuplas,
las cuales veremos en el capítulo siguiente.
Dado que los diccionarios son mutables, estos no pueden utilizarse como claves, pero pue-
den utilizarse como valores.
11.6. Memos
Si jugaste con la función fibonacci de la Sección 6.7, quizás has notado que mientras más
grande es el argumento que entregues, más tarda la función en ejecutarse. Además, el tiem-
po de ejecución aumenta rápidamente.
Para entender el por qué, considera la Figura 11.2, que muestra el gráfico de llamadas para
fibonacci con n=4.
Un gráfico de llamadas muestra un conjunto de marcos de funciones, con líneas que conec-
tan cada marco a los marcos de las funciones que están siendo llamadas. En la parte de arri-
ba del gráfico, 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 inefi-
ciente para el probema y se pone peor a medida que el argumento se hace más grande.
Una solución es hacer un seguimiento de los valores que ya han sido calculados almace-
nándolos en un diccionario. Un valor previamente calculado que se almacena para un uso
posterior se llama memo. Aquí hay una versión “memoizada” de fibonacci:
conocidos = {0:0, 1:1}
def fibonacci(n):
if n in conocidos:
return conocidos[n]
112 Capítulo 11. Diccionarios
Cada vez que se llama a fibonacci, revisa a conocidos. Si el resultado ya está ahí, puede
devolverlo inmediatamente. De lo contrario, tiene que calcular el nuevo valor, agregarlo al
diccionario y devolverlo.
Si ejecutas esta versión de fibonacci y la comparas con la original, encontrarás que esta es
mucho más rápida.
Es común utilizar variables globales para las banderas (en inglés, flags), es decir, variables
booleanas que indican si una condición es verdadera. Por ejemplo, algunos programas
utilizan una bandera llamada verbose para controlar el nivel de detalle en la salida:
verbose = True
def ejemplo1():
if verbose:
print('Ejecutando ejemplo1')
Si intentas reasignar una variable global, quizás te sorprendas. El siguiente ejemplo se su-
pone que hace seguimiento de si la función ha sido llamada:
fue_llamada = False
def ejemplo2():
fue_llamada = True # INCORRECTO
Pero si lo ejecutas verás que el valor de fue_llamada no cambia. El problema es que
ejemplo2 crea una nueva variable local con nombre fue_llamada. La variable local se va
cuando la función termina y no tiene efecto en la variable global.
Para reasignar una variable global dentro de una función, tienes que declarar la variable
global antes de utilizarla:
fue_llamada = False
def ejemplo2():
global fue_llamada
fue_llamada = True
11.8. Depuración 113
La sentencia global le dice al intérprete algo como “En esta función, cuando digo
fue_llamada, me refiero a la variable global; no crees una local.”
Aquí hay un ejemplo que intenta actualizar una variable global:
contar = 0
def ejemplo3():
contar = contar + 1 # INCORRECTO
Si lo ejecutas obtienes:
UnboundLocalError: local variable 'contar' referenced before assignment
Python supone que contar es local, y bajo ese supuesto lo estás leyendo antes de escribirlo.
La solución, nuevamente, es declarar contar como global.
def ejemplo3():
global contar
contar += 1
Si una variable global se refiere a un valor mutable, puedes modificar el valor sin declarar
la variable:
conocidos = {0:0, 1:1}
def ejemplo4():
conocidos[2] = 1
Entonces puedes agregar, eliminar y reemplazar elementos de una lista global o diccionario
global, pero si quieres reasignar la variable, tienes que declararla:
def ejemplo5():
global conocidos
conocidos = dict()
Las variables globales pueden ser útiles, pero si tienes muchas, y las modificas frecuente-
mente, pueden hacer que los programas sean difíciles de depurar.
11.8. Depuración
A medida que trabajes con conjuntos de datos más grandes, depurar imprimiendo y verifi-
cando la salida a mano puede volverse algo difícil de manejar. Aquí hay algunas sugeren-
cias para depurar conjuntos grandes de datos.
Reduce la escala: Si es posible, reduce el tamaño del conjunto de datos. Por ejemplo, si el
programa lee un archivo de texto, comienza solo con las primeras 10 líneas, o con
el ejemplo más pequeño que puedas encontrar. Puedes editar aquellos archivos o
(mejor) modificar el programa de manera que este lea solo las primeras n líneas.
Si hay un error, puedes reducir n al valor más pequeño que muestre el error y luego
incrementarlo gradualmente mientras encuentras y corriges los errores.
Revisa resúmenes y tipos: En lugar de imprimir y verificar el conjunto de datos completo,
considera imprimir resúmenes de los datos: por ejemplo, el número de ítems en el
diccionario o el total de una lista de números.
Una causa común de errores de tiempo de ejecución es un valor que no es del tipo
correcto. Para depurar esta clase de errores, a menudo es suficiente imprimir el tipo
del valor.
114 Capítulo 11. Diccionarios
Escribe verificaciones automáticas: A veces puedes escribir código para verificar errores
de manera automática. Por ejemplo, si estás calculando el promedio de una lista de
números, podrías verificar que el resultado no sea mayor que el elemento más grande
de la lista ni menor que el más pequeño. A esto se le llama “prueba de cordura” (en
inglés, sanity check) porque detecta resultados que son “locos”.
Otro tipo de prueba compara los resultados de dos computaciones diferentes para
ver si son consistentes. A esto se le llama “prueba de consistencia”.
Dale formato a la salida: Dar formato a la salida de la depuración puede hacer más fácil
detectar un error. Vimos un ejemplo en la Sección 6.9. Otra herramienta que quizás
encuentres útil es el módulo pprint, el cual proporciona una función pprint que
muestra tipos incorporados en un formato más legible por humanos (pprint significa
“pretty print”).
Nuevamente, el tiempo que pasas construyendo andamiaje puede reducir el tiempo que
pasas depurando.
11.9. Glosario
mapeo: Una relación en la cual cada elemento de un conjunto corresponde a un elemento
de otro conjunto.
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”.
función hash: Una función utilizada por una tabla hash para calcular la ubicación de una
clave.
hashable: Un tipo que tiene una función hash. Los tipos inmutables como los enteros,
números de coma flotante y cadenas son hashables; los tipos mutables como las listas
y diccionarios no lo son.
consulta: Una operación de diccionario que toma una clave y encuentra el valor corres-
pondiente.
consulta inversa: Una operación de diccionario que toma un valor y encuentra una o más
claves que mapean a este.
gráfico de llamadas: Un diagrama que muestra cada marco creado durante la ejecución de
un programa, con una flecha desde cada llamador hacia cada llamado.
memo: Un valor calculado que se almacena para evitar una futura computación innecesa-
ria.
variable global: Una variable definida fuera de la función. Las variables globales pueden
ser accesibles desde cualquier función.
bandera: Una variable booleana que se utiliza para indicar si una condición es verdadera.
declaración: Una sentencia como global que le dice al intérprete algo sobre una variable.
11.10. Ejercicios
Ejercicio 11.1. Escribe una función que lea las palabras en words.txt y las almacene como claves
en un diccionario. No importa cuáles sean los valores. Luego puedes utilizar el operador in como
una manera rápida de verificar si una cadena está en el diccionario.
Si hiciste el Ejercicio 10.10, puedes comparar la velocidad de esta implementación con el operador
in de lista y la búsqueda de bisección.
Ejercicio 11.2. Lee la documentación del método de diccionario setdefault y utilízalo para escri-
bir una versión más concisa de invertir_dict. Solución: http: // thinkpython2. com/ code/
invert_ dict. py .
Ejercicio 11.3. Memoiza la función de Ackermann del Ejercicio 6.2 y ve si la memoización permite
evaluar la función con argumentos más grandes. Pista: no. Solución: http: // thinkpython2.
com/ code/ ackermann_ memo. py .
Ejercicio 11.4. Si hiciste el Ejercicio 10.7, ya tienes una función con nombre tiene_duplicados
que toma una lista como parámetro y devuelve True si hay algún objeto que aparece más de una vez
en la lista.
Utiliza un diccionario para escribir una versión más rápida y simple de tiene_duplicados. Solu-
ción: http: // thinkpython2. com/ code/ has_ duplicates. py .
Ejercicio 11.5. Dos palabras son “pares rotativos” si puedes rotar una de ellas y obtener la otra
(ver rotar_palabra en el Ejercicio 8.5).
Escribe un programa que lea una lista de palabras y encuentre todos los pares rotativos. Solución:
http: // thinkpython2. com/ code/ rotate_ pairs. py .
Ejercicio 11.6. Aquí hay otro Puzzler de Car Talk (http: // www. cartalk. com/ content/
puzzlers ):
Este fue enviado por un compañero llamado Dan O’Leary. Se encontró hace poco con
una palabra monosílaba de cinco letras común, que tiene la siguiente propiedad única.
Cuando quitas la primera letra, las demás letras forman un homófono de la palabra
original, es decir, una palabra que suena exactamente igual. Reemplaza la primera letra,
es decir, ponla de nuevo y quita la segunda letra y el resultado es otro homófono de la
palabra original. Y la pregunta es, ¿cuál es la palabra?
Ahora voy a darte un ejemplo que no funciona. Veamos la palabra de cinco letras,
‘wrack.’ W-R-A-C-K, ya sabes, como ‘wrack with pain.’ Si quito la primera letra, me
116 Capítulo 11. Diccionarios
quedo con una palabra de cuatro letras, ’R-A-C-K.’ Como en ‘Holy cow, did you see
the rack on that buck! It must have been a nine-pointer!’ Es un homófono perfecto. Si
vuelves a poner la ‘w’ y quitas la ‘r’ en su lugar, te quedas con la palabra ‘wack’ que es
una palabra real, que simplemente no es un homófono de las otras dos palabras.
Pero hay, de todas maneras, al menos una palabra de la cual Dan y nosotros sabemos,
que entregará dos homófonos, si quitas cualquiera de las primeras dos letras para hacer
dos nuevas palabras de cuatro letras. La pregunta es, ¿cuál es la palabra?
Puedes utilizar el diccionario del Ejercicio 11.1 para verificar si una cadena está en la lista de pala-
bras.
Para verificar si dos palabras son homófonas, puedes utilizar el Diccionario de Pronuncia-
ción de la CMU. Lo puedes descargar en http: // www. speech. cs. cmu. edu/ cgi-bin/
cmudict o en http: // thinkpython2. com/ code/ c06d y también puedes descargar http:
// thinkpython2. com/ code/ pronounce. py , que proporciona una función con nombre
read_dictionary que lee el diccionario de pronunciación y devuelve un diccionario de Python
que mapea de cada palabra a una cadena que describe su pronunciación primaria.
Escribe un programa que haga una lista de todas las palabras que resuelven el Puzzler. Solución:
http: // thinkpython2. com/ code/ homophone. py .
Capítulo 12
Tuplas
Este capítulo presenta otro tipo incorporado más, la tupla, y luego muestra la manera en
que las listas, los diccionarios y las tuplas trabajan juntos. Además, presento una carac-
terística útil para listas de argumentos de longitud variable: los operadores de reunión y
dispersión.
>>> t = tuple('lupins')
>>> t
('l', 'u', 'p', 'i', 'n', 's')
Dado que tuple es el nombre de una función incorporada, deberías evitar usarlo como
nombre de una variable.
La mayoría de los operadores de lista funcionan también en tuplas. El operador de corche-
tes indexa un elemento:
>>> t = ('a', 'b', 'c', 'd', 'e')
>>> t[0]
'a'
Y el operador de trozo selecciona un rango de elementos.
>>> t[1:3]
('b', 'c')
Sin embargo, si intentas modificar uno de los elementos de la tupla, obtienes un error:
>>> t[0] = 'A'
TypeError: object doesn't support item assignment
Dado que las tuplas son inmutables, no puedes modificar sus elementos. Pero puedes re-
emplazar una tupla con otra:
>>> t = ('A',) + t[1:]
>>> t
('A', 'b', 'c', 'd', 'e')
Esta sentencia crea una nueva tupla y luego hace que t se refiera a esta.
Los operadores relacionales funcionan con las tuplas y otras secuencias; Python comienza
comparando el primer elemento de cada secuencia. Si son iguales, avanza a los siguientes
elementos, y así sucesivamente, hasta que encuentre elementos que sean diferentes. Los
elementos posteriores no se consideran (incluso si son realmente grandes).
>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True
>>> a, b = 1, 2, 3
ValueError: too many values to unpack
De manera más general, el lado derecho puede ser cualquier tipo de secuencia (cadena,
lista o tupla). Por ejemplo, para separar una dirección de email en nombre de usuario y
dominio, podrías intentar:
>>> direccion = '[email protected]'
>>> nombre_usuario, dominio = direccion.split('@')
El valor de retorno de split es una lista con dos elementos: el primer elemento se asigna a
nombre_usuario, el segundo a dominio.
>>> nombre_usuario
'monty'
>>> dominio
'python.org'
El parámetro de reunión puede tener cualquier nombre que quieras, pero args es conven-
cional. Aquí se muestra cómo opera la función:
>>> printall(1, 2.0, '3')
(1, 2.0, '3')
El complemento de la reunión es la dispersión. Si tienes una secuencia de valores y quie-
res pasarlo a una función como multiples argumentos, puedes utilizar el operador *. Por
ejemplo, divmod toma exactamente dos argumentos; no funciona con una tupla:
>>> t = (7, 3)
>>> divmod(t)
TypeError: divmod expected 2 arguments, got 1
Sin embargo, si dispersas la tupla, funciona:
>>> divmod(*t)
(2, 1)
Muchas de las funciones incorporadas utilizan tuplas de argumentos de longitud variable.
Por ejemplo, max y min pueden tomar cualquier cantidad de argumentos:
>>> max(1, 2, 3)
3
Pero sum no puede.
>>> sum(1, 2, 3)
TypeError: sum expected at most 2 arguments, got 3
Como ejercicio, escribe una función llamada sumar_todos que tome cualquier cantidad de
argumentos y devuelva su suma.
tuple
0 ’Cleese’
1 ’John’
El resultado es un objeto dict_items, que es un iterador que itera los pares clave-valor.
Puedes utilizarlo en un bucle for así:
>>> for clave, valor in d.items():
... print(clave, valor)
...
c 2
a 0
b 1
Tal como esperarías de un diccionario, los ítems no están en un orden particular.
En sentido contrario, puedes utilizar una lista de tuplas para inicializar un nuevo diccio-
nario:
>>> t = [('a', 0), ('c', 2), ('b', 1)]
>>> d = dict(t)
>>> d
{'a': 0, 'c': 2, 'b': 1}
Combinando dict con zip se produce una manera concisa de crear un diccionario:
>>> d = dict(zip('abc', range(3)))
>>> d
{'a': 0, 'c': 2, 'b': 1}
El método de diccionario update además toma una lista de tuplas y las agrega, como pares
clave-valor, a un diccionario existente.
Es común utilizar tuplas como claves en diccionarios (principalmente porque no puedes
utilizar listas). Por ejemplo, un directorio telefónico podría mapear de pares apellido-
nombre a números telefónicos. Suponiendo que hemos definido apellido, nombre y
numero, podríamos escribir:
directorio[apellido, nombre] = numero
La expresión en corchetes es una tupla. Podríamos utilizar asignación de tupla para reco-
rrer este diccionario.
for apellido, nombre in directorio:
print(nombre, apellido, directorio[apellido,nombre])
Este bucle recorre las claves en directorio, que son tuplas. Asigna los elementos de cada
tupla a apellido y nombre, luego imprime el nombre completo y el número telefónico
correspondiente.
Hay dos maneras de representar tuplas en un diagrama de estado. La versión más deta-
llada muestra los índices y elementos tal como aparecen en una lista. Por ejemplo, la tupla
('Cleese', 'John') se mostraría como en la Figura 12.1.
Pero en un diagrama más grande quizás quieras omitir los detalles. Por ejemplo, un dia-
grama del directorio telefónico podría mostrarse como en la Figura 12.2.
12.7. Secuencias de secuencias 123
dict
(’Cleese’, ’John’) ’08700 100 222’
(’Chapman’, ’Graham’) ’08700 100 222’
(’Idle’, ’Eric’) ’08700 100 222’
(’Gilliam’, ’Terry’) ’08700 100 222’
(’Jones’, ’Terry’) ’08700 100 222’
(’Palin’, ’Michael’) ’08700 100 222’
Dado que las tuplas son inmutables, no proporcionan métodos como sort y reverse,
que modifican listas existentes. Sin embargo, Python proporciona la función incorporada
sorted, que toma cualquier secuencia y devuelve una lista nueva con los mismos elemen-
tos en orden, y reversed, que toma una secuencia y devuelve un iterador que recorre la
lista en orden invertido.
12.8. Depuración
Las listas, diccionarios y tuplas son ejemplos de estructuras de datos; en este capítulo co-
menzamos a ver estructuras de datos combinadas, como listas de tuplas, o diccionarios
124 Capítulo 12. Tuplas
que contienen tuplas como claves y listas como valores. Las estructuras de datos combi-
nadas son útiles, pero son propensas a lo que yo llamo errores de forma, es decir, errores
causados cuando una estructura de datos tiene el tipo, tamaño o estructura incorrecta. Por
ejemplo, si estás esperando una lista que contiene un entero y yo te doy un simple y viejo
entero (no en una lista), no funcionará.
Para ayudar a depurar esta clase de errores, he escrito 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 una cadena que resume su forma. Puedes
descargarlo en http://thinkpython2.com/code/structshape.py
12.9. Glosario
tupla: Una secuencia inmutable de elementos.
asignación de tupla: Una asignación con una secuencia en el lado derecho y una tupla de
variables en el lado izquierdo. El lado derecho es evaluado y luego sus elementos son
asignados a las variables en el lado izquierdo.
dispersión: Una operación que hace que una secuencia se comporte como múltiples argu-
mentos.
objeto zip: El resultado de llamar a la función incorporada zip; un objeto que itera a través
de una secuencia de tuplas.
iterador: Un objeto que itera a través de una secuencia, pero que no proporciona operado-
res ni métodos de lista.
error de forma: Un error causado debido a que un valor tiene la forma incorrecta, es decir,
el tipo o tamaño incorrecto.
12.10. Ejercicios
Ejercicio 12.1. Escribe una función llamada mas_frecuente que tome una cadena e impri-
ma las letras en orden de frecuencia descendente. Encuentra ejemplos de texto en varios idio-
mas diferentes y ve cómo varía la frecuencia de letras entre los idiomas. Compara tus resulta-
dos con las tablas en http: // en. wikipedia. org/ wiki/ Letter_ frequencies . Solución:
http: // thinkpython2. com/ code/ most_ frequent. py .
Ejercicio 12.2. ¡Más anagramas!
1. Escribe un programa que lea una lista de palabras desde un archivo (ver Sección 9.1) e impri-
ma todos los conjuntos de palabras que son anagramas.
Aquí hay un ejemplo de cómo se vería la salida:
['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled']
['retainers', 'ternaries']
['generating', 'greatening']
['resmelts', 'smelters', 'termless']
Pista: quizás quieras construir un diccionario que mapee de una colección de letras a una lista
de palabras que se puedan escribir con esas letras. La pregunta es, ¿cómo puedes representar
la colección de letras de manera que se pueda utilizar como clave?
2. Modifica el programa anterior para que imprima la lista de anagramas más grande primero,
seguido de la segunda más grande, y así sucesivamente.
3. En Scrabble, un “bingo” es cuando juegas todas las siete fichas de tu atril, junto con una
letra en el tablero, para formar una palabra de ocho letras. ¿Qué colección de 8 letras forma la
mayor cantidad de bingos posible?
Solución: http: // thinkpython2. com/ code/ anagram_ sets. py .
Ejercicio 12.3. Dos palabras forman un “par de metátesis” si puedes transformar una en la otra in-
tercambiando dos letras; por ejemplo, “converse” y “conserve”. Escribe un programa que encuentre
todos los pares de metátesis en el diccionario. Pista: no pruebes todos los pares de palabras ni pruebes
todos los posibles intercambios. Solución: http: // thinkpython2. com/ code/ metathesis.
py . Crédito: Este ejercicio está inspirado por un ejemplo de http: // puzzlers. org .
Ejercicio 12.4. Aquí hay otro Puzzler de Car Talk (http: // www. cartalk. com/ content/
puzzlers ):
126 Capítulo 12. Tuplas
¿Cuál es la palabra en inglés más larga que, a medida que eliminas sus letras una a la
vez, sigue siendo una palabra en inglés válida?
A ver, las letras se pueden eliminar desde cualquier extremo, o del medio, pero no puedes
reorganizar ninguna de las letras. Cada vez que retiras una letra, terminas con otra pa-
labra en inglés. Si haces eso, eventualmente vas a terminar con una letra y esa también
va a ser una palabra en inglés, una que se encuentra en el diccionario. Quiero saber:
¿cuál es la palabra más larga y cuantas letras tiene?
Voy a darte un pequeño ejemplo modesto: Sprite. ¿De acuerdo? Inicias con sprite, quitas
una letra, una del interior de la palabra, te llevas la r, y nos quedamos con la palabra
spite, luego quitamos la e del final, nos quedamos con spit, quitamos la s, nos quedamos
con pit, it, y por último I.
Escribe un programa que encuentre todas las palabras que se pueden reducir de esta manera, y luego
encuentra la más larga.
Este ejercicio es uno de los más desafiantes, así que aquí hay algunas sugerencias:
1. Quizás quieras escribir una función que tome una palabra y obtenga una lista de todas las
palabras que se pueden formar eliminando una letra. Estas son las “hijas” de la palabra.
2. De manera recursiva, una palabra es reducible si cualquiera de sus hijas es reducible. Como
caso base, puedes considerar la cadena vacía reducible.
3. La lista de palabras que proporciono, words.txt, no contiene palabras de una sola letra.
Entonces quizás quieras agregar “I”, “a” y la cadena vacía.
4. Para mejorar el desempeño de tu programa, quizás quieras memoizar las palabras que se sabe
que son reducibles.
En este punto has aprendido sobre las estructuras de datos esenciales de Python, y has
visto algunos de los algoritmos que los utilizan. Si te gustaría saber más sobre algoritmos,
este podría ser un buen momento para leer el Apéndice B. Sin embargo, no tienes que leerlo
antes de continuar; puedes leerlo cuando te interese.
Este capítulo presenta un estudio de caso con ejercicios que te hacen pensar sobre la elec-
ción de estructuras de datos y practicar su uso.
Ejercicio 13.3. Modifica el programa del ejercicio anterior para que imprima las 20 palabras utili-
zadas con mayor frecuencia en el libro.
Ejercicio 13.4. Modifica el programa anterior para que lea una lista de palabras (ver Sección 9.1)
y luego imprima todas las palabras del libro que no están en la lista de palabras. ¿Cuántas de ellas
son errores tipográficos? ¿Cuántas de ellas son palabras comunes que deberían estar en la lista de
palabras y cuántas de ellas son realmente oscuras?
for i in range(10):
x = random.random()
print(x)
La función randint toma los parámetros low y high y devuelve un entero entre low y high
(incluyendo a ambos).
>>> random.randint(5, 10)
5
>>> random.randint(5, 10)
9
Para escoger un elemento de una secuencia de manera aleatoria, puedes usar choice:
>>> t = [1, 2, 3]
>>> random.choice(t)
2
>>> random.choice(t)
3
El módulo random también proporciona funciones para generar valores aleatorios a partir
de distribuciones continuas, incluyendo la gaussiana, exponencial, gamma y algunas más.
Ejercicio 13.5. Escribe una función con nombre escoger_de_hist que tome un histograma como
se definió en la Sección 11.2 y devuelva un valor aleatorio del histograma, esogido con probabilidad
proporcional a la frecuencia. Por ejemplo, para este histograma:
13.3. Histograma de palabras 129
def procesar_archivo(nombre_archivo):
hist = dict()
fp = open(nombre_archivo)
for linea in fp:
procesar_linea(linea, hist)
return hist
hist = procesar_archivo('emma.txt')
Este programa lee a emma.txt, que contiene el texto de Emma de Jane Austen.
procesar_archivo recorre las líneas del archivo, pasándolas una a la vez a
procesar_linea. El histograma hist se utiliza como acumulador.
procesar_linea utiliza el método de cadena replace para reemplazar los guiones con
espacios antes de utilizar split para separar la línea en una lista de cadenas. Recorre la lista
de palabras y utiliza strip y lower para eliminar la puntuación y convertir a minúsculas.
(Es una abreviatura decir que las cadenas se “convierten”; recuerda que las cadenas son
inmutables, por lo que los métodos como strip y lower devuelven cadenas nuevas.)
Finalmente, procesar_linea actualiza el histograma creando un nuevo ítem o incremen-
tando uno existente.
Para contar el número total de palabras en el archivo, podemos sumar las frecuencias del
histograma:
def total_palabras(hist):
return sum(hist.values())
130 Capítulo 13. Estudio de caso: selección de estructura de datos
t.sort(reverse=True)
return t
En cada tupla, la frecuencia aparece primero, por lo que la lista resultante está ordenada
por frecuencia. Aquí hay un bucle que imprime las diez palabras más comunes:
t = mas_comunes(hist)
print('Las palabras más comunes son:')
for frec, palabra in t[:10]:
print(palabra, frec, sep='\t')
Utilizo el argumento de palabra clave sep para decirle a print que utilice un carácter de ta-
bulación como “separador”, en lugar de un espacio, así la segunda columna está alineada.
Estos son los resultados de Emma:
Las palabras más comunes son:
to 5242
the 5205
and 4897
of 4295
i 3191
a 3130
it 2529
her 2483
was 2400
she 2364
Este código se puede simplificar utilizando el parámetro key de la función sort. Si sientes
curiosidad, puedes leer sobre este en https://wiki.python.org/moin/HowTo/Sorting.
13.5. Parámetros opcionales 131
Escribe un programa que utilice la diferencia de conjuntos para encontrar palabras en el libro que
no están en la lista. Solución: http: // thinkpython2. com/ code/ analyze_ book2. py .
return random.choice(t)
La expresión [palabra] * frec crea una lista con frec copias de la cadena palabra. El
método extend es similar a append, excepto que el argumento es una secuencia.
Este algoritmo funciona, pero no es muy eficiente: cada vez que escoges una palabra alea-
toria, reconstruye la lista, que es tan grande como el libro original. Una mejora obvia es
construir la lista una vez y luego hacer múltiples selecciones, pero la lista es grande aún.
1. Utilizar keys para obtener una lista de las palabras del libro.
2. Construir una lista que contenga la suma acumulativa de las frecuencias de las pala-
bras (ver Ejercicio 10.2). El último ítem en esta lista es el número total de palabras en
el libro: n.
En este texto, la frase “half the” está siempre seguida por la palabra “bee”, pero la frase
“the bee” podría estar seguida por “has” o “is”.
El resultado del análisis de Markov es un mapeo de cada prefijo (como “half the” y “the
bee”) a todos los posibles sufijos (como “has” e “is”).
Dado este mapeo, puedes generar un texto aleatorio comenzando con cualquier prefijo y
escogiendo de manera aleatoria en los posibles sufijos. Después, puedes combinar el final
del prefijo y el nuevo sufijo para formar el nuevo prefijo, y repetir.
Por ejemplo, si comienzas con el prefijo “Half a”, entonces la siguiente palabra tiene que
ser “bee”, porque el prefijo solo aparece una vez en el texto. El siguiente prefijo es “a bee”,
por lo que el siguiente sufijo podría ser “philosophically”, “be” o “due”.
En este ejemplo, la longitud del prefijo es siempre dos, pero puedes hacer análisis de Mar-
kov con cualquier longitud de prefijo.
Ejercicio 13.8. Análisis de Markov:
1. Escribe un programa que lea un texto desde un archivo y realice análisis de Markov. El re-
sultado podría ser un diccionario que mapee de prefijos a una colección de posibles sufijos. La
colección podría ser una lista, tupla o diccionario; depende de ti hacer una elección apropiada.
Puedes probar tu programa con un prefijo de largo dos, pero podrías escribir el programa de
una manera en que resulte fácil intentar otras longitudes.
2. Agrega una función al programa anterior que genere texto aleatorio basado en el análisis de
Markov. Aquí hay un ejemplo de Emma con prefijo de largo 2:
He was very clever, be it sweetness or be angry, ashamed or only amused, at such
a stroke. She had never thought of Hannah till you were never meant for me?I
cannot make speeches, Emma:"he soon cut it all himself.
134 Capítulo 13. Estudio de caso: selección de estructura de datos
Para este ejemplo, dejé la puntuación unida a las palabras. El resultado es casi sintácticamente
correcto, pero no del todo. Semánticamente, casi tiene sentido, pero no del todo.
¿Qué ocurre si aumentas la longitud del prefijo? ¿Tiene más sentido el texto aleatorio?
3. Una vez que tu programa funcione, quizás quieras probar una mezcla: si combinas texto de dos
o más libros, el texto aleatorio que generes mezclará el vocabulario y las frases de las fuentes
de maneras interesantes.
Crédito: Este estudio de caso está basado en un ejemplo de Kernighan and Pike, The Practice of
Programming, Addison-Wesley, 1999.
Deberías intentar este ejercicio antes de continuar; luego puedes descargar mi so-
lución en http://thinkpython2.com/code/markov.py. Además, necesitarás http://
thinkpython2.com/code/emma.txt.
El último es fácil: un diccionario es la elección obvia para mapear de claves a valores co-
rrespondientes.
Para los prefijos, las opciones más obvias son cadenas, lista de cadenas o tupla de cadenas.
Para los sufijos, una opción es una lista, otra es un histograma (diccionario).
¿Cómo deberías escoger? El primer paso es pensar en las operaciones que necesitarás im-
plementar para cada esturcura de datos. Para prefijos, necesitamos poder eliminar palabras
del principio y agregar al final. Por ejemplo, si el prefijo actual es “Half a”, y la siguiente
palabra es “bee”, necesitas poder formar el siguiente prefijo, “a bee”.
Tu primera elección podría ser una lista, dado que es fácil agregar y eliminar elementos,
pero también necesitamos poder utilizar los prefijos y claves en un diccionario, así que
eso descarta las listas. Con las tuplas, no puedes anexar o eliminar, pero puedes utilizar el
operador suma para formar una nueva tupla:
def cambiar(prefijo, palabra):
return prefijo[1:] + (palabra,)
cambiar toma una tupla de palabras, prefijo, y una cadena, palabra, y forma una nueva
tupla que tiene todas las palabras en prefijo excepto la primera, y palabra agregada al
final.
Para la colección de sufijos, las operaciones que necesitamos realizar incluyen agregar un
nuevo sufijo (o aumentar la frecuencia de uno existente) y escoger un sufijo aleatorio.
13.10. Depuración 135
Agregar un nuevo sufijo es igual de fácil con lista o histograma en cuanto a implemen-
tación. Escoger un elemento aleatorio de una lista es fácil; escoger del histograma es más
díficil de hacer de manera eficiente (ver Ejercicio 13.7).
Hasta ahora hemos estado hablando principalmente sobre la facilidad de la implementa-
ción, pero hay otros factores a considerar al escoger estructuras de datos. Uno es el tiempo
de ejecución. A veces hay una razón teórica para esperar que una estructura de datos sea
más rápida que otra; por ejemplo, mencioné que el operador in es más rápido para diccio-
narios que para listas, al menos cuando el número de elementos es grande.
Sin embargo, a menudo no sabes de antemano cuál implementación será más rápida. Una
opción es implementar ambas y ver cuál es mejor. Este enfoque se llama evaluación compa-
rativa (en inglés, benchmarking). Una alternativa práctica es escoger la estructura de datos
que sea la más fácil de implementar, y luego ver si es lo suficientemente rápida para la
aplicación en cuestión. Si es así, no hay necesidad de seguir. Si no, hay herramientas, como
el módulo profile, que puede identificar los lugares en un programa que toman la mayor
parte del tiempo.
El otro factor a considerar es el espacio de almacenamiento. Por ejemplo, utilizar un his-
tograma para la colección de sufijos podría ocupar menos espacio porque solo tienes que
almacenar cada palabra una vez, sin importar cuántas veces aparezca en el texto. En algu-
nos casos, ahorrar espacio puede también hacer que tu programa se ejecute más rápido,
y en el caso extremo, tu programa podría no ejecutarse en absoluto si te quedas sin me-
moria. Sin embargo, para muchas aplicaciones el espacio es una consideración secundaria
después del tiempo de ejecución.
Una útlima reflexión: en esta discusión, he insinuado que deberíamos utilizar una estruc-
tura de datos tanto para el análisis como para la generación. Pero dado que estas son fases
separadas, sería posible también utilizar una estructura para el análisis y luego convertirla
a otra estructura para la generación. Esto sería una ganancia neta si el tiempo ahorrado
durante la generación excediera al tiempo ocupado en la conversión.
13.10. Depuración
Cuando estés depurando un programa, y especialmente si estás trabajando en un error de
programación difícil, hay cinco cosas para probar:
Lectura: Examina tu código, léelo de nuevo a ti mismo y verifica que dice lo que querías
decir.
Ejecución: Experimenta haciendo cambios y ejecutando versiones diferentes. A menudo,
si muestras en pantalla lo correcto en el lugar correcto del programa, el problema se
vuelve obvio, pero a veces tienes que construir andamiaje.
Rumiación: ¡Tómate un tiempo para pensar! ¿Qué tipo de error es: de sintaxis, de tiempo
de ejecución o semántico? ¿Qué información puedes obtener a partir de los mensajes
de error o de la salida del programa? ¿Qué tipo de error podría causar el problema
que estás viendo? ¿Qué cambiaste últimamente, antes de que el problema apareciera?
Patito de goma: Si le explicas el problema a alguien más, a veces encuentras la respuesta
antes de terminar la pregunta. A menudo no necesitas a la otra persona; podrías sim-
plemente hablarle a un patito de goma. Y ese es el origen de la conocida estrategia
136 Capítulo 13. Estudio de caso: selección de estructura de datos
llamada método de depuración del patito de goma (en inglés, rubber duck debug-
ging). No estoy inventando; ver https://en.wikipedia.org/wiki/Rubber_duck_
debugging.
Retroceso: En algún punto, lo mejor que se puede hacer es retroceder, deshacer los cam-
bios recientes, hasta que regreses a un programa que funcione y que entiendas. Luego
puedes comenzar a reconstruir.
Los programadores principiantes a veces se atascan en una de estas actividades y olvidan
las otras. Cada actividad viene con su propio modo de fallo.
Por ejemplo, leer tu código podría ayudar si el problema es un error tipográfico, pero no
si el problema es un malentendido conceptual. Si no entiendes lo que tu programa hace,
puedes leerlo 100 veces y nunca ver el error, porque el error está en tu cabeza.
Ejecutar experimentos puede ayudar, especialmente si ejecutas pruebas pequeñas y sim-
ples. Pero si ejecutas experimentos sin pensar ni leer tu código, podrías caer en un patrón
que yo llamo “programación de camino aleatorio”, que es el proceso de hacer cambios alea-
torios hasta que el programa haga lo correcto. No hace falta decir que la programación de
camino aleatorio puede tomar mucho tiempo.
Tienes que tomarte el tiempo de pensar. La depuración es como una ciencia experimental:
deberías tener al menos una hipótesis acerca de cuál es el problema. Si hay dos o más
posibilidades, intenta pensar en una prueba que eliminaría una de ellas.
Sin embargo, incluso las mejores técnicas de depuración fallarán si hay muchos errores, o
si el código que intentas arreglar es muy grande y complicado. A veces la mejor opción es
retroceder, simplificando el programa hasta que obtengas algo que funcione y que entien-
das.
Los programadores principiantes son a menudo reacios a retroceder porque no pueden
soportar eliminar una línea de códiigo (incluso si es incorrecta). Si te hace sentir mejor,
copia tu programa en otro archivo antes de comenzar a recortarlo. Luego puedes volver a
copiar los pedazos uno a la vez.
Encontrar un error de programación difícil requiere lectura, ejecución, rumiación y a veces
retroceso. Si te atascas en una de estas actividades, intenta las otras.
13.11. Glosario
determinista: Dicho de un programa que hace lo mismo cada vez que se ejecuta, dadas las
mismas entradas.
pseudoaleatorio: Dicho de una secuencia de números que aparenta ser aleatorio, pero se
genera por un programa determinista.
valor por defecto: El valor dado a un parámetro opcional si no se le entrega un argumento.
anular: Reemplazar un valor por defecto con un argumento.
evaluación comparativa: El proceso de escoger entre estructuras de datos implementando
alternativas y probándolas en una muestra de posibles entradas.
método de depuración del patito de goma: Depurar explicando tu problema a un objeto
inanimado tal como un patito de goma. Articular el problema puede ayudarte a re-
solverlo, incluso si el patito de goma no sabe Python.
13.12. Ejercicios 137
13.12. Ejercicios
Ejercicio 13.9. El “rango” de una palabra es su posición en una lista de palabras ordenadas por
frecuencia: la palabra más común tiene rango 1, la segunda más común tiene rango 2, etc.
La ley de Zipf describe una relación entre los rangos y frecuencias de palabras en lenguajes naturales
(http: // en. wikipedia. org/ wiki/ Zipf's_ law ). Específicamente, predice que la frecuencia,
f , de cada palabra con rango r es:
f = cr −s
donde s y c son parámetros que dependen del lenguaje y el texto. Si tomas el logaritmo de ambos
lados de esta ecuación, obtienes:
log f = log c − s log r
Entonces, si graficas log f versus log r, deberías obtener una línea recta con pendiente −s e inter-
cepto log c.
Escribe un programa que lea un texto de un archivo, cuente las frecuencias de palabras e imprima
una línea para cada palabra, en orden de frecuencia descendiente, con log f y log r. Utiliza el prog-
mara de gráficos de tu elección para graficar los resultados y verificar si forman una línea recta.
¿Puedes estimar el valor de s?
Solución: http: // thinkpython2. com/ code/ zipf. py . Para ejecutar mi solución, necesitas el
módulo de gráficos matplotlib. Si instalaste Anaconda, ya tienes matplotlib; de lo contrario,
quizás tengas que instalarlo.
138 Capítulo 13. Estudio de caso: selección de estructura de datos
Capítulo 14
Archivos
Este capítulo presenta la idea de programas “persistentes” que mantienen los datos en
almacenamiento permanente y muestra cómo utilizar diferentes tipos de almacenamiento
permanente, tales como archivos y bases de datos.
14.1. Persistencia
La mayoría de los programas que hemos visto hasta ahora son transitorios en el sentido de
que se ejecutan por un tiempo corto y producen alguna salida, pero cuando terminan sus
datos desaparecen. Si ejecutas el programa de nuevo, comienza con una pizarra en blanco.
Otros programas son persistentes: se ejecutan por un tiempo largo (o todo el tiempo),
mantienen al menos algunos de sus datos en almacenamiento permanente (un disco duro,
por ejemplo) y, si se apagan y reinician, continúan donde estaban.
Ejemplos de programas persistentes son los sistemas operativos, que se se ejecutan casi
siempre que un computador está encendido, y los servidores web, que se ejecutan todo el
tiempo, esperando solicitudes para entrar en la red.
Una de las maneras más simples que tienen los programas para mantener sus datos es
leyendo y escribiendo archivos de texto. Ya hemos visto programas que leen archivos de
texto; en este capítulo veremos programas que los escriban.
Una alternativa es almacenar el estado del programa en una base de datos. En este capítulo
presentaré una base de datos simple y un módulo, pickle, que facilita el almacenamiento
de datos del programa.
open devuelve un objeto de archivo que proporciona métodos para trabajar con el archivo.
El método write pone datos en el archivo.
>>> linea1 = "He aquí el junco,\n"
>>> fout.write(linea1)
18
El valor de retorno es la cantidad de caracteres que se escribieron. El objeto de archivo hace
un seguimiento del lugar en donde está, por lo cual si llamas a write de nuevo, agrega los
nuevos datos al final del archivo.
>>> linea2 = "emblema de nuestra tierra.\n"
>>> fout.write(linea2)
27
Cuando hayas terminado de escribir, deberías cerrar el archivo.
>>> fout.close()
Si no cierras el archivo, se cierra cuando el programa termina.
El primer operando es la cadena de formato, que contiene una o más secuencias de forma-
to, las cuales especifican la manera en que se da formato al segundo operando. El resultado
es una cadena.
Por ejemplo, la secuencia de formato '%d' significa que el segundo operando debería for-
matearse como un entero decimal:
>>> camellos = 42
>>> '%d' % camellos
'42'
El resultado es la cadena '42', que no debe confundirse con el valor entero 42.
Una secuencia de formato puede aparecer en cualquier lugar de la cadena, así que puedes
incrustar un valor en una oración:
>>> 'He visto %d camellos.' % camellos
'He visto 42 camellos.'
14.4. Nombres de archivo y rutas 141
Si hay más de una secuencia de formato en la cadena, el segundo argumento tiene que
ser una tupla. Cada secuencia de formato es emparejada con un elemento de la tupla, en
orden.
El siguiente ejemplo utiliza '%d' para dar formato a un entero, '%g' para dar formato a un
número de coma flotante y '%s' para dar formato a una cadena:
>>> 'En %d años he visto %g %s.' % (3, 0.1, 'camellos')
'En 3 años he visto 0.1 camellos.'
El número de elementos en la tupla tiene que coincidir con el número de secuencias de
formato en la cadena. Además, los tipos de los elementos tienen que coincidir con las se-
cuencias de formato:
>>> '%d %d %d' % (1, 2)
TypeError: not enough arguments for format string
>>> '%d' % 'dólares'
TypeError: %d format: a number is required, not str
En el primer ejemplo, no hay suficientes elementos; en el segundo, el elemento tiene tipo
incorrecto.
El módulo os proporciona funciones para trabajar con archivos y directorios (“os” significa
“operating system”). os.getcwd devuelve el nombre del directorio actual:
>>> import os
>>> cwd = os.getcwd()
>>> cwd
'/home/dinsdale'
cwd significa “current working directory”. El resultado en este ejemplo es /home/dinsdale,
que es el directorio principal de un usuario con nombre dinsdale.
Un nombre de archivo simple, como memo.txt, también se considera una ruta, pe-
ro es una ruta relativa porque se relaciona con el directorio actual. Si el direc-
torio actual es /home/dinsdale, el nombre de archivo memo.txt haría referencia a
/home/dinsdale/memo.txt.
Una ruta que comienza con / no depende del directorio actual: se llama ruta absoluta. Para
encontrar la ruta absoluta de un archivo, puedes utilizar os.path.abspath:
142 Capítulo 14. Archivos
>>> os.path.abspath('memo.txt')
'/home/dinsdale/memo.txt'
os.path proporciona otras funciones para trabajar con nombres de archivo y rutas. Por
ejemplo, os.path.exists verifica si un archivo o directorio existe:
>>> os.path.exists('memo.txt')
True
Si existe, os.path.isdir verifica si es un directorio:
>>> os.path.isdir('memo.txt')
False
>>> os.path.isdir('/home/dinsdale')
True
Del mismo modo, os.path.isfile verifica si es un archivo.
os.listdir devuelve una lista de los archivos (y otros directorios) en el directorio dado:
>>> os.listdir(cwd)
['music', 'photos', 'memo.txt']
Para demostrar estas funciones, el siguiente ejemplo “recorre” un directorio, imprime los
nombres de todos los archivos y se llama a sí mismo de manera recursiva en todos los
directorios.
def walk(nombre_dir):
for nombre in os.listdir(nombre_dir):
ruta = os.path.join(nombre_dir, nombre)
if os.path.isfile(ruta):
print(ruta)
else:
walk(ruta)
os.path.join toma un directorio y un archivo y los une en una ruta completa.
El módulo os proporciona una función llamada walk que es similar a esta pero más versátil.
Como ejercicio, lee la documentación y utiliza esta función para imprimir los nombres de
los archivos en un directorio dado y sus subdirectorios. Puedes descargar mi solución en
http://thinkpython2.com/code/walk.py.
Es mejor continuar y avanzar —y lidiar con los problemas, si ocurren— que es exactamente
lo que hace la sentencia try. La sintaxis es similar a una sentencia if...else:
try:
fin = open('archivo_malo')
except:
print('Algo salió mal.')
Python comienza ejecutando la cláusula try. Si todo sale bien, se salta la cláusula except y
continúa. Si ocurre una excepción, salta hacia afuera de la cláusula try y ejecuta la cláusula
except.
Manejar una excepción con una sentencia try se llama capturar una excepción. En este
ejemplo, la cláusula except imprime un mensaje de error que no es de mucha ayuda. En
general, capturar una excepción te da una oportunidad de arreglar el problema, o intentar
de nuevo, o al menos terminar el programa de manera elegante.
El módulo dbm proporciona una interfaz para crear y actualizar archivos de base de datos.
Como ejemplo, crearé una base de datos que contiene títulos para archivos de imagen.
Si haces otra asignación a una clave existente, dbm reemplaza el valor antiguo:
144 Capítulo 14. Archivos
El módulo pickle puede ayudar. Este módulo traduce casi cualquier tipo de objeto en una
cadena adecuada para almacenar en una base de datos y también traduce cadenas para
que vuelvan a ser objetos.
Puedes utilizar pickle para almacenar objetos que no sean cadena en una base de datos.
De hecho, esta combinación es tan común que ha sido encapsulada en un módulo llamado
shelve.
14.8. Tuberías 145
14.8. Tuberías
La mayoría de los sistemas operativos proporcionan una interfaz de línea de comandos,
también conocida como shell. Las shells generalmente proporcionan comandos para nave-
gar en el sistema de archivos e iniciar aplicaciones. Por ejemplo, en Unix puedes cambiar
directorios con cd, mostrar los contenidos de un directorio con ls e iniciar un navegador
web escribiendo (por ejemplo) firefox.
Cualquier programa que puedes iniciar desde la shell puede también iniciarse desde
Python utilizando un objeto de tubería (en inglés, pipe object), que representa un programa
en ejecución.
Por ejemplo, el comando Unix ls -l normalmente muestra los contenidos del directorio
actual en formato largo. Puedes ejecutar ls con os.popen1 :
>>> cmd = 'ls -l'
>>> fp = os.popen(cmd)
El argumento es una cadena que contiene un comando de shell. El valor de retorno es un
objeto que se comporta como un archivo abierto. Puedes leer la salida del proceso ls una
línea a la vez con readline u obtener todo de una vez con read:
>>> res = fp.read()
Cuando termines, cierras la tubería como un archivo:
>>> stat = fp.close()
>>> print(stat)
None
El valor de retorno es el estado final del proceso ls; None significa que termina de manera
normal (sin errores).
Por ejemplo, la mayoría de los sistemas Unix proporcionan un comando llamado md5sum
que lee los contenidos de un archivo y calcula una “suma de verificación”. Puedes leer
sobre MD5 en http://en.wikipedia.org/wiki/Md5. Este comando proporciona una ma-
nera eficiente de verificar que dos archivos tengan los mismos contenidos. La probabilidad
de que diferentes contenidos entreguen la misma suma de verificación es muy pequeña (es
decir, improbable que ocurra antes de que el universo colapse).
Puedes utilizar una tubería para ejecutar md5sum desde Python y obtener el resultado:
>>> nombre_archivo = 'book.tex'
>>> cmd = 'md5sum ' + nombre_archivo
>>> fp = os.popen(cmd)
>>> res = fp.read()
>>> stat = fp.close()
>>> print(res)
1e0033f0ed0656636de0d75144ba32e0 book.tex
>>> print(stat)
None
1 popen ahora está obsoleto, lo cual significa que se supone que debemos dejar de utilizarla y comenzar a
utilizar el módulo subprocess. Pero para casos simples, encuentro a subprocess más complicado que necesario.
Entonces voy a seguir utilizando popen hasta que lo quiten.
146 Capítulo 14. Archivos
print(contar_lineas('wc.py'))
Si ejecutas este programa, se lee a sí mismo e imprime el número de líneas en el archivo, el
cual es 7. Puedes también importarlo así:
>>> import wc
7
Ahora tienes un objeto de módulo wc:
>>> wc
<module 'wc' from 'wc.py'>
El objeto de módulo proporciona contar_lineas:
>>> wc.contar_lineas('wc.py')
7
Entonces así es como escribes módulos en Python.
El único problema con este ejemplo es que, cuando importas el módulo, ejecuta el código
de prueba de la parte final. Normalmente, cuando importas un módulo, este define nuevas
funciones pero no las ejecuta.
Los programas que serán importados como módulos a menudo utilizan la siguiente forma:
if __name__ == '__main__':
print(contar_lineas('wc.py'))
__name__ es una variable incorporada que se establece cuando se inicia el programa. Si el
programa se está ejecutando como un script, __name__ tiene el valor '__main__'; en ese
caso, el código de prueba se ejecuta. De lo contrario, si el módulo se está importando, se
salta el código de prueba.
Como ejercicio, escribe este ejemplo en un archivo con nombre wc.py y ejecútalo como
un script. Luego ejecuta el intérprete de Python y haz import wc. ¿Cuál es el valor de
__name__ cuando el módulo se está importando?
Si quieres volver a cargar un módulo, puedes utilizar la función incorporada reload, pero
puede ser complicada, por lo cual lo más seguro es reiniciar el intérprete y luego importar
el módulo de nuevo.
14.10. Depuración 147
14.10. Depuración
Cuando lees y escribes archivos, podrías tener problemas con el espacio en blanco. Estos
errores pueden ser difíciles de depurar porque los espacios, sangrías y nuevas líneas son
normalmente invisibles:
>>> s = '1 2\t 3\n 4'
>>> print(s)
1 2 3
4
La función incorporada repr puede ayudar. Toma cualquier objeto como argumento y de-
vuelve una representación de cadena del objeto. Para las cadenas, representa los caracteres
de espacio en blanco con secuencias de barras invertidas:
>>> print(repr(s))
'1 2\t 3\n 4'
Esto puede ser útil para depurar.
Otro problema que podrías encontrar es que sistemas diferentes utilizan caracteres diferen-
tes para indicar el fin de una línea. Algunos sistemas utilizan una nueva línea, representada
por \n. Otros utilizan un carácter “return”, representado por \r. Algunos utilizan ambos. Si
mueves archivos entre sistemas diferentes, estas inconsistencias pueden causar problemas.
Para la mayoría de los sistemas, hay aplicaciones para convertir de un formato a otro.
Puedes encontrarlas (y leer más sobre este tema) en http://en.wikipedia.org/wiki/
Newline. O, por supuesto, puedes escribir una por tu cuenta.
14.11. Glosario
persistente: Dicho de un programa que se ejecuta indefinidamente y mantiene al menos
alguno de sus datos en almacenamiento permanente.
operador de formato: Un operador, %, que toma una cadena de formato y una tupla y ge-
nera una cadena que incluye los elementos de la tupla con el formato especificado
por la cadena de formato.
cadena de formato: Una cadena, utilizada con el operador de formato, que contiene se-
cuencias de formato.
secuencia de formato: Una secuencia de caracteres en una cadena de formato, como %d,
que especifica cómo debería ser el formato de un valor.
directorio: Una colección de archivos que tiene un nombre, también llamado carpeta.
ruta absoluta: Una ruta que comienza desde el directorio más alto en el sistema de archi-
vos.
148 Capítulo 14. Archivos
capturar : Evitar que una excepción termine un programa, utilizando las sentencias try y
except.
base de datos: Un archivo cuyo contenido está organizado como un diccionario con claves
y sus correspondientes valores.
shell: Un programa que permite a los usuarios escribir comandos y luego ejecutarlos ini-
ciando otros programas.
14.12. Ejercicios
Ejercicio 14.1. Escribe una función llamada sed que tome como argumentos una cadena de patrón,
una cadena de reemplazo y dos nombres de archivo; debería leer el primer archivo y escribir los
contenidos en el segundo archivo (creándolo si es necesario). Si la cadena de patrón aparece en algún
lugar en el archivo, debería reemplazarse con la cadena de reemplazo.
Si ocurre un error mientras se abren, leen, escriben o cierran los archivos, tu programa debería
capturar la excepción, imprimir un mensaje de error y salir. Solución: http: // thinkpython2.
com/ code/ sed. py .
Ejercicio 14.2. Si descargas mi solución al Ejercicio 12.2 de http: // thinkpython2. com/
code/ anagram_ sets. py , verás que crea un diccionario que mapea de una cadena ordenada de
letras a la lista de palabras que se pueden formar con esas letras. Por ejemplo, 'opst' mapea a la
lista ['opts', 'post', 'pots', 'spot', 'stop', 'tops'].
1. Escribe un programa que busque un directorio y todos sus subdirectorios, de manera recursi-
va, y devuelva una lista de rutas completas para todos los archivos con un sufijo dado (como
.mp3). Pista: os.path proporciona varias funciones útiles para manipular nombres de archi-
vo y de rutas.
2. Para reconocer duplicados, puedes utilizar md5sum para calcular una “suma de verificación”
de cada archivo. Si dos archivos tienen la misma suma de verificación, probablemente tienen
los mismos contenidos.
Clases y objetos
En este punto sabes cómo utilizar funciones para organizar código y tipos incorporados
para organizar datos. El siguiente paso es aprender “programación orientada a objetos”,
que utiliza tipos definidos por el programador para organizar tanto código como datos. La
programación orientada a objetos es un gran tema; tomará un par de capítulos para llegar
allí.
Los ejemplos de código de este capítulo están disponibles en http://thinkpython2.
com/code/Point1.py; las soluciones a los ejercicios están disponibles en http://
thinkpython2.com/code/Point1_soln.py.
Crear un tipo nuevo es más complicado que las otras opciones, pero tiene ventajas que
pronto serán aparentes.
Un tipo definido por el programador también se llama clase. Una definición de clase se ve
así:
class Punto:
"""Representa un punto en un espacio 2-D."""
150 Capítulo 15. Clases y objetos
Punto
vacio x 3.0
y 4.0
El encabezado indica que la clase nueva se llama Punto. El cuerpo es un docstring que
explica para qué es la clase. Puedes definir variables y métodos dentro de una definición
de clase, pero volveremos a eso más adelante.
Definir una clase con nombre Punto crea un objeto de clase.
>>> Punto
<class '__main__.Punto'>
Dado que Punto se define en el nivel más alto, su “nombre completo” es __main__.Punto.
El objeto de clase es como una fábrica para crear objetos. Para crear un Punto, llamas a
Punto como si fuera una función.
>>> vacio = Punto()
>>> vacio
<__main__.Punto object at 0xb7e9d3ac>
El valor de retorno es una referencia a un objeto Punto, que asignamos a vacio.
El acto de crear un objeto nuevo se llama instanciación, y el objeto es una instancia de la
clase.
Cuando imprimes una instancia, Python te dice a qué clase pertenece y dónde se almacena
en la memoria (el prefijo 0x significa que el siguiente número es un hexadecimal).
Cada objeto es una instancia de alguna clase, por tanto “objeto” e “instancia” son intercam-
biables. Sin embargo, en este capítulo utilizo “instancia” para indicar que estoy hablando
de un tipo definido por el programador.
15.2. Atributos
Puedes asignar valores a una instancia utilizando notación de punto:
>>> vacio.x = 3.0
>>> vacio.y = 4.0
Esta sintaxis es similar a la sintaxis para seleccionar una variable de un módulo, tal como
math.pi o string.whitespace. En este caso, sin embargo, estamos asignando valores a
elementos que tienen nombre y pertenecen a un objeto. Estos elementos se llaman atribu-
tos.
La Figura 15.1 es un diagrama de estado que muestra el resultado de estas asignaciones. Un
diagrama de estado que muestra un objeto con sus atributos se llama diagrama de objeto.
La variable vacio se refiere a un objeto Punto, que contiene dos atributos. Cada atributo se
refiere a un número de coma flotante.
Puedes leer el valor de un atributo utilizando la misma sintaxis:
15.3. Rectángulos 151
>>> vacio.y
4.0
>>> x = vacio.x
>>> x
3.0
La expresión vacio.x significa “Ve al objeto al cual vacio se refiere y obten el valor de x.”
En este ejemplo, asignamos ese valor a una variable con nombre x. No hay conflicto entre
la variable x y el atributo x.
Puedes utilizar la notación de punto como parte de una expresión. Por ejemplo:
>>> '(%g, %g)' % (vacio.x, vacio.y)
'(3.0, 4.0)'
>>> distancia = math.sqrt(vacio.x**2 + vacio.y**2)
>>> distancia
5.0
Puedes pasar una instancia como argumento en la manera usual. Por ejemplo:
def imprimir_punto(p):
print('(%g, %g)' % (p.x, p.y))
imprimir_punto toma un punto como argumento y lo muestra en notación matemática.
Para invocarla, puedes pasar a vacio como argumento:
>>> imprimir_punto(vacio)
(3.0, 4.0)
Dentro de la función, p es un alias para vacio, por tanto si la función modifica a p, vacio
cambia.
Como ejercicio, escribe una función llamada distancia_entre_puntos que tome dos Pun-
tos como argumentos y devuelva la distancia entre ellos.
15.3. Rectángulos
A veces es obvio cuáles deberían ser los atributos de un objeto, pero otras veces tienes que
tomar decisiones. Por ejemplo, imagina que estás diseñando una clase para representar
rectángulos. ¿Qué atributos utilizarías para especificar la ubicación y tamaño de un rec-
tángulo? Puedes ignorar el ángulo; para mantener las cosas simples, supongamos que el
rectángulo es vertical u horizontal.
En este punto es difícil decir si una alternativa es mejor que la otra, así que implementare-
mos la primera, solo como ejemplo.
Rectangulo
caja anchura 100.0 Punto
altura 200.0 x 0.0
esquina y 0.0
class Rectangulo:
"""Representa un rectángulo.
Para representar un rectángulo, tienes que instanciar un objeto Rectángulo y asignar valo-
res a los atributos:
caja = Rectangulo()
caja.anchura = 100.0
caja.altura = 200.0
caja.esquina = Punto()
caja.esquina.x = 0.0
caja.esquina.y = 0.0
La expresión caja.esquina.x significa “Ve al objeto al cual caja se refiere y seleccionea el
atributo con nombre esquina; luego ve a ese objeto y selecciona el atributo con nombre x.”
La Figura 15.2 muestra el estado de este objeto. Un objeto que es un atributo de otro objeto
está incrustado.
15.6. Copiar
Los alias pueden hacer que un programa sea difícil de leer porque los cambios en un lugar
podrían tener efectos inesperados en otro lugar. Es difícil hacer un seguimiento de todas
las variables que podrían referirse a un objeto dado.
Copiar un objeto es a menudo una alternativa a los alias. El módulo copy contiene una
función llamada copy que puede duplicar cualquier objeto:
>>> p1 = Punto()
>>> p1.x = 3.0
>>> p1.y = 4.0
>>> p1 == p2
False
El operador is indica que p1 y p2 no son el mismo objeto, que es lo que esperábamos.
Sin embargo, tal vez hayas esperado que == entregue True porque estos puntos contienen
los mismos datos. En ese caso, te decepcionará aprender que, para instancias, el compor-
tamiento por defecto del operador == es el mismo que el operador is: verifica identidad
de objeto, no equivalencia de objeto. Eso ocurre debido a que para tipos definidos por el
programador, Python no sabe qué debería considerarse equivalente. Al menos, no todavía.
Si usas copy.copy para duplicar un Rectángulo, encontrarás que copia el objeto Rectángulo
pero no el Punto incrustado.
>>> caja2 = copy.copy(caja)
>>> caja2 is caja
False
>>> caja2.esquina is caja.esquina
True
La Figura 15.3 muestra cómo se ve el diagrama de objeto. Esta operación se llama copia
superficial porque copia al objeto y cualquier referencia que contenga, pero no los objetos
incrustados.
Para la mayoría de las aplicaciones, esto no es lo que quieres. En este ejemplo, invo-
car a crecer_rectangulo en uno de los Rectángulos no afectaría al otro, ¡pero invocar
a mover_rectangulo en cualquiera afectaría a ambos! Este comportamiento es confuso y
propenso a errores.
Afortunadamente, el módulo copy proporciona un método con nombre deepcopy que co-
pia no solo el objeto sino también los objetos a los cuales este se refiere, y los objetos a los
cuales estos se refieren, y así sucesivamente. No te sorprenderá saber que esta operación se
llama copia profunda.
>>> caja3 = copy.deepcopy(caja)
>>> caja3 is caja
False
>>> caja3.esquina is caja.esquina
False
caja3 y caja son objetos completamente separados.
Como ejercicio, escribe una versión de mover_rectangulo que cree y devuelva un Rectán-
gulo nuevo en lugar de modificar el antiguo.
15.7. Depuración
Cuando comienzas a trabajar con objetos, es probable que encuentres algunas excepciones
nuevas. Si intentas acceder a un atributo que no existe, obtienes un AttributeError:
15.8. Glosario 155
>>> p = Punto()
>>> p.x = 3
>>> p.y = 4
>>> p.z
AttributeError: Point instance has no attribute 'z'
Si no sabes bien de qué tipo es un objeto, puedes consultar:
>>> type(p)
<class '__main__.Punto'>
Puedes también utilizar isinstance para verificar si un objeto es una instancia de una
clase:
>>> isinstance(p, Punto)
True
Si no sabes bien si un objeto tiene un atributo en particular, puedes utilizar la función
incorporada hasattr:
>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False
El primer argumento puede ser cualquier objeto; el segundo argumento es una cadena que
contiene el nombre del atributo.
Puedes también utilizar una sentencia try para ver si el objeto tiene los atributos que ne-
cesitas:
try:
x = p.x
except AttributeError:
x = 0
Este enfoque puede hacer más fácil escribir funciones que trabajen con tipos diferentes; se
verá más sobre este tema en la Sección 17.9.
15.8. Glosario
clase: Un tipo definido por el programador. Una definición de clase crea un nuevo objeto
de clase.
objeto de clase: Un objeto que contiene información acerca del tipo definido por el pro-
gramador. El objeto de clase se puede utilizar para crear instancias del tipo.
atributo: Uno de los valores con nombre que están asociados a un objeto.
copia profunda: Copiar los contenidos de un objeto así como cualquier objeto incrustado,
y cualquier objeto incrustado en estos, y así sucesivamente; implementado por la
función deepcopy del módulo copy.
diagrama de objeto: Un diagrama que muestra objetos, sus atributos y los valores de los
atributos.
15.9. Ejercicios
Ejercicio 15.1. Escribe una definición de una clase con nombre Circulo cuyos atributos sean
centro y radio, donde centro es un objeto Punto y radio es un número.
Instancia un objeto Círculo que represente un círculo con su centro en (150, 100) y radio 75.
Escribe una función con nombre punto_en_circulo que tome un Círculo y un Punto, y devuelva
True si el Punto está dentro o en el borde del círculo.
Escribe una función con nombre rect_en_circulo que tome un Círculo y un Rectángulo y de-
vuelva True si el Rectángulo está completamente dentro o en el borde del círculo.
Escribe una función con nombre rect_circ_traslapan que tome un Círculo y un Rectángulo y
devuelva True si alguna de las esquinas del Rectángulo cae dentro del círculo. O como versión más
desafiante, que devuelva True si cualquier parte del Rectángulo cae dentro del círculo.
Escribe una función llamada dibujar_circulo que tome un Turtle y un Círculo y dibuje el Círcu-
lo.
Clases y funciones
Ahora que sabemos cómo crear tipos nuevos, el siguiente paso es escribir funciones que
tomen objetos definidos por el programador como parámetros y los devuelvan como re-
sultados. En este capítulo presento también el “estilo de programación funcional” y dos
nuevos planes de desarrollo de programas.
Los ejemplos de código de este capítulo están disponibles en http://thinkpython2.com/
code/Time1.py. Las soluciones a los ejercicios están en http://thinkpython2.com/code/
Time1_soln.py.
16.1. Tiempo
Como otro ejemplo de tipo definido por el programador, definiremos una clase llamada
Tiempo que registre la hora del día. La definición de la clase se ve así:
class Tiempo:
"""Representa la hora del día.
Tiempo
tiempo hora 11
minuto 59
segundo 30
return suma
Aunque esta función es correcta, está comenzando a ponerse grande. Después veremos
una alternativa más corta.
16.3. Modificadores
A veces es útil para una función modificar los objetos que obtiene como parámetros. En ese
caso, los cambios son visibles para la llamadora. Las funciones que trabajan de esta manera
se llaman modificadores.
aumentar, que agrega un número dado de segundos al objeto Tiempo, se puede escribir de
manera natural como modificador. Aquí hay un borrador:
def aumentar(tiempo, segundos):
tiempo.segundo += segundos
¿Esta función es correcta? ¿Qué ocurre si segundos es mucho más grande que sesenta?
En ese caso, no es suficiente acarrear una vez; tenemos que seguir haciéndolo hasta que
tiempo.segundo sea menor que sesenta. Una solución es reemplazar las sentencias if con
sentencias while. Eso haría que la función sea correcta, pero no muy eficiente. Como ejer-
cicio, escribe una versión correcta de aumentar que no contenga ningún ciclo.
Cualquier cosa que se pueda hacer con modificadores también se puede hacer con funcio-
nes puras. De hecho, algunos lenguajes de programación solo permiten funciones puras.
160 Capítulo 16. Clases y funciones
Existe evidencia de que los programas que utilizan funciones puras son más rápidos de
desarrollar y menos propensos a errores que los programas que utilizan modificadores.
Sin embargo, los modificadores son convenientes en ocasiones, y los programas funciona-
les tienden a ser menos eficientes.
En general, recomiendo que escribas funciones puras siempre que sea razonable y recurras
a los modificadores solo si hay una ventaja convincente. Este enfoque podría ser llamado
estilo de programación funcional.
Como ejercicio, escribe una versión “pura” de aumentar que cree y devuelva un nuevo
objeto Tiempo en lugar de modificar el parámetro.
Este enfoque puede ser efectivo, especialmente si todavía no tienes un entendimiento pro-
fundo del problema. Sin embargo, las correcciones incrementales pueden generar código
innecesariamente complicado —dado que lidia con muchos casos especiales— y no confia-
ble —dado que es difícil saber si has encontrado todos los errores.
Una alternativa es el desarrollo diseñado, en el cual la visión de alto nivel del problema
puede hacer mucho más fácil la programación. En este caso, la visión es que un objeto Tiem-
po es en realidad un número de tres dígitos en base 60 (ver http://en.wikipedia.org/
wiki/Sexagesimal). El atributo segundo es la “columna de unidades”, el atributo minuto
es la “columna de sesentenas” y el atributo hora es la “columna de centenas de treinta y
seis”.
Esta observación sugiere otro enfoque al problema completo: podemos convertir objetos
Tiempo a enteros y tomar ventaja del hecho de que el computador sabe cómo hacer arit-
mética con enteros.
Quizás tengas que pensar un poco, y ejecutar algunas pruebas, para convencer-
te de que estas funciones son correctas. Una manera de probarlas es verificar que
tiempo_a_int(int_a_tiempo(x)) == x para muchos valores de x. Este es un ejemplo de
prueba de consistencia.
Una vez que te convenzas de que son correctas, puedes utilizarlas para reescribir
sumar_tiempo:
def sumar_tiempo(t1, t2):
segundos = tiempo_a_int(t1) + tiempo_a_int(t2)
return int_a_tiempo(segundos)
Esta versión es más corta que la original, y más fácil de verificar. Como ejercicio, reescribe
aumentar utilizando tiempo_a_int e int_a_tiempo.
En algunas formas, convertir de base 60 a base 10 y viceversa es más difícil que solo tra-
tar con tiempos. La conversión de base es más abstracta; nuestra intuición para lidiar con
valores de tiempo es mejor.
Sin embargo, si tenemos la visión para tratar tiempos como números en base 60 y nos dedi-
camos a escribir las funciones de conversión (tiempo_a_int e int_a_tiempo), obtenemos
un programa más corto, más fácil de leer y depurar, y más confiable.
Es más fácil también añadir características más adelante. Por ejemplo, imagina restar dos
Tiempos para encontrar la duración entre ellos. El enfoque ingenuo sería implementar resta
con préstamo. Utilizando funciones de conversión sería más fácil y con más probabilidades
de ser correcto.
Irónicamente, a veces hacer que un programa sea más difícil (o más general) lo hace más
fácil (porque hay menos casos especiales y menos oportunidades de error).
16.5. Depuración
Un objeto Tiempo está bien formado si los valores de minuto y segundo están entre 0 y
60 (incluyendo a 0 pero no a 60) y si hora es positivo. hora y minuto deberían ser valores
enteros, pero podríamos permitir que segundo tenga una parte de fracción.
Requisitos como estos se llaman invariantes porque deberían ser siempre verdaderos. Visto
de manera diferente, si no son verdaderos es porque algo salió mal.
Escribir código para verificar invariantes puede ayudar a detectar errores y encontrar sus
causas. Por ejemplo, quizás tengas una función como tiempo_valido que tome un objeto
Tiempo y devuelva False si viola un invariante:
def tiempo_valido(tiempo):
if tiempo.hora < 0 or tiempo.minuto < 0 or tiempo.segundo < 0:
return False
if tiempo.minuto >= 60 or tiempo.segundo >= 60:
return False
return True
Al principio de cada función podrías verificar los argumentos para asegurarte de que son
válidos:
162 Capítulo 16. Clases y funciones
16.6. Glosario
prototipo y parche: Un plan de desarrollo que involucra escribir un borrador de un pro-
grama, probarlo y corregir errores a medida que se encuentran.
desarrollo diseñado: Un plan de desarrollo que involucra una visión de alto nivel del pro-
blema y más planificación que el desarrollo incremental o el desarrollo de prototipo.
función pura: Una función que no modifica ninguno de los objetos que recibe como argu-
mentos. La mayoría de las funciones puras son productivas.
modificador: Una función que cambia uno o más de los objetos que recibe como argumen-
tos. La mayoría de los modificadores son funciones nulas, es decir, devuelven None.
invariante: Una condición que debería ser siempre verdadera durante la ejecución de un
programa.
sentencia assert: Una sentencia que verifica una condición y levanta una excepción si esta
falla.
16.7. Ejercicios
Los ejemplos de código de este capítulo están disponibles en http://thinkpython2.com/
code/Time1.py; las soluciones a los ejercicios están disponibles en http://thinkpython2.
com/code/Time1_soln.py.
Ejercicio 16.1. Escribe una función llamada mul_tiempo que tome un objeto Tiempo y un número
y devuelva un nuevo objeto Tiempo que contenga el producto del Tiempo original y el número.
Luego, utiliza mul_tiempo para escribir una función que tome un objeto Tiempo que represente el
tiempo de término en una carrera, y un número que represente la distancia, y devuelva un objeto
Tiempo que represente el ritmo de carrera promedio (tiempo por cada milla).
16.7. Ejercicios 163
Ejercicio 16.2. El módulo datetime proporciona objetos time que son similares a los objetos Tiem-
po de este capítulo, pero proporcionan un abundante conjunto de métodos y operadores. Lee la docu-
mentación en http: // docs. python. org/ 3/ library/ datetime. html .
1. Utiliza el módulo datetime para escribir un programa que obtenga la fecha actual e imprima
el día de la semana.
2. Escribe un programa que tome un día de nacimiento como entrada e imprima la edad del
usuario y el número de días, horas y segundos que faltan para el siguiente cumpleaños.
3. Para dos personas nacidas en días diferentes, hay un día en que una tiene el doble de edad que
la otra. Ese es su Día Doble. Escribe un programa que tome dos días de nacimiento y calcule
su Día Doble.
4. Para ponerlo un poco más desafiante, escribe una versión más general que calcule el día en
que una persona es n veces mayor que la otra.
Clases y métodos
Los objetos a menudo representan cosas del mundo real, y los métodos a menudo
corresponden a las maneras en las cuales interactúan las cosas del mundo real.
Hasta aquí, no hemos tomado ventaja de las características que Python proporciona para
admitir programación orientada a objetos. Estas características no son estrictamente nece-
sarias: la mayoría de estas proporciona una sintaxis alternativa para cosas que ya hemos
166 Capítulo 17. Clases y métodos
hecho. Sin embargo, en muchos casos, la alternativa es más concisa y comunica la estruc-
tura del programa de manera más precisa.
Por ejemplo, en Time1.py no hay conexión obvia entre la definición de clase y las defini-
ciones de funciones que siguen. Con algo de revisión, es evidente que cada función toma
al menos un objeto Tiempo como argumento.
Esta observación es la motivación para los métodos; un método es una función que está
asociada a una clase en particular. Hemos visto métodos para cadenas, listas, diccionarios
y tuplas. En este capítulo, definiremos métodos para tipos definidos por el programador.
Los métodos son semánticamente lo mismo que las funciones, pero hay dos diferencias
sintácticas:
Los métodos se definen dentro de una definición de clase para hacer explícita la rela-
ción entre la clase y el método.
En las siguientes secciones, tomaremos las funciones de los dos capítulos anteriores y los
transformaremos en métodos. Esta transformación es puramente mecánica: puedes hacerla
siguiendo una secuencia de pasos. Si te acostumbras a convertir de una forma a la otra,
podrás escoger la mejor forma para lo que sea que estés haciendo.
def imprimir_tiempo(tiempo):
print('%.2d:%.2d:%.2d' % (tiempo.hora, tiempo.minuto, tiempo.segundo))
Para llamar a esta función, tienes que pasar el objeto Tiempo como argumento:
>>> comienzo = Tiempo()
>>> comienzo.hora = 9
>>> comienzo.minuto = 45
>>> comienzo.segundo = 00
>>> imprimir_tiempo(comienzo)
09:45:00
Para convertir imprimir_tiempo a método, todo lo que tenemos que hacer es mover la
definición de función al interior de la definición de clase. Nota el cambio en la sangría.
class Tiempo:
def imprimir_tiempo(tiempo):
print('%.2d:%.2d:%.2d' % (tiempo.hora, tiempo.minuto, tiempo.segundo))
Ahora hay dos maneras de llamar a imprimir_tiempo. La primera manera (y menos co-
mún) es utilizar sintaxis de función:
17.3. Otro ejemplo 167
>>> Tiempo.imprimir_tiempo(comienzo)
09:45:00
En este uso de notación de punto, Tiempo es el nombre de la clase e imprimir_tiempo es el
nombre del método. comienzo se pasa como parámetro.
Dentro del método, el sujeto se asigna al primer parámetro, por tanto en este caso comienzo
se asigna a tiempo.
Por convención, el primer parámetro de un método se llama self, por lo cual sería más
común escribir imprimir_tiempo así:
class Tiempo:
def imprimir_tiempo(self):
print('%.2d:%.2d:%.2d' % (self.hora, self.minuto, self.segundo))
La razón para esta convención es una metáfora implícita:
En la programación orientada a objetos, los objetos son los agentes activos. Una in-
vocación a método como comienzo.imprimir_tiempo() dice “¡Oye comienzo! Por
favor imprímete.”
Este cambio de perspectiva quizás sea más cortés, pero su utilidad no es obvia. En los
ejemplos que hemos visto hasta ahora, podría no serlo. Sin embargo, a veces cambiar de
funciones a objetos de manera responsable hace posible escribir funciones (o métodos) más
versátiles, y hace más fácil mantener y reutilizar código.
Como ejercicio, reescribe tiempo_a_int (de la Sección 16.4) como método. Quizás te tien-
tes a reescribir int_a_tiempo como método también, pero eso realmente no tiene sentido
porque no habría objeto donde invocarlo.
Esta versión supone que tiempo_a_int está escrito como método. Además, notar que esta
es una función pura, no un modificador.
Así es como invocarías a aumentar:
>>> comienzo.imprimir_tiempo()
09:45:00
>>> termino = comienzo.aumentar(1337)
>>> termino.imprimir_tiempo()
10:07:17
El sujeto, comienzo, se asigna al primer parámetro, self. El argumento, 1337, se asigna al
segundo parámetro, segundos.
Este mecanismo puede ser confuso, especialmente si cometes un error. Por ejemplo, si in-
vocas a aumentar con dos argumentos, obtienes:
>>> termino = comienzo.aumentar(1337, 460)
TypeError: aumentar() takes 2 positional arguments but 3 were given
El mensaje de error es confuso al inicio, debido a que hay solo dos argumentos en parénte-
sis. Sin embargo, el sujeto también se considera como argumento, por lo cual todos juntos
son tres.
Por cierto, un argumento posicional es un argumento que no tiene un nombre de paráme-
tro, es decir, no es un argumento de palabra clave. En esta llamada a función:
dibujar(loro, jaula, muerto=True)
loro y jaula son posicionales, y muerto es un argumento de palabra clave.
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hora, self.minuto, self.segundo)
Cuando utilizas print en un objeto, Python invoca al método str:
>>> tiempo = Tiempo(9, 45)
>>> print(tiempo)
09:45:00
Cuando escribo una clase nueva, casi siempre comienzo escribiendo a __init__, que hace
más fácil instanciar objetos, y __str__, que es útil para depurar.
Como ejercicio, escribe un método str para la clase Punto. Crea un objeto Punto e imprí-
melo.
170 Capítulo 17. Clases y métodos
Cambiar el comportamiento de un operador para que funcione con tipos definidos por
el programador se llama sobrecarga de operador. Para cada operador en Python hay un
método especial, como __add__, que le corresponde. Para más detalles, ver http://docs.
python.org/3/reference/datamodel.html#specialnames.
Como ejercicio, escribe un método add para la clase Punto.
17.9. Polimorfismo
El despacho basado en tipo es útil si es necesario, pero (afortunadamente) no siempre es ne-
cesario. A menudo puedes evitarlo escribiendo funciones que trabajen de manera correcta
con argumentos de tipo diferente.
172 Capítulo 17. Clases y métodos
Muchas de las funciones que escribimos para cadenas también funcionan para otros tipos
de secuencia. Por ejemplo, en la Sección 11.2 utilizamos histograma para contar el número
de veces que aparece cada letra en una palabra.
def histograma(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c] + 1
return d
Esta función también se puede utilizar con listas, tuplas e incluso diccionarios, siempre que
los elementos de s sean hashables, por lo cual se pueden utilizar como claves en d.
>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histograma(t)
{'bacon': 1, 'egg': 1, 'spam': 4}
Las funciones que se pueden utilizar con varios tipos se llaman polimórficas. Los poli-
morfismos pueden facilitar la reutilización de código. Por ejemplo, la función incorporada
sum, que suma los elementos de una secuencia, funciona siempre que los elementos de la
secuencia admitan la suma.
Dado que los objetos Tiempo proporcionan un método add, funcionan con sum:
>>> t1 = Tiempo(7, 43)
>>> t2 = Tiempo(7, 41)
>>> t3 = Tiempo(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00
En general, si todas las operaciones dentro de una función se pueden utilizar con un tipo
dado, la función se puede utilizar con ese tipo.
Los mejores polimorfismos son los involuntarios, donde descubres que una función que ya
escribiste se puede aplicar a un tipo para el cual nunca planeaste.
17.10. Depuración
Es legal añadir atributos a los objetos en cualquier punto de la ejecución de un programa,
pero si tienes objetos con el mismo tipo que no tienen los mismos atributos, es fácil cometer
errores. Se considera una buena idea inicializar todo lo de un atributo de un objeto en el
método init.
Otra manera de acceder a los atributos es la función incorporada vars, que toma un objeto
y devuelve un diccionario que mapea de los nombres de atributos (como cadenas) a sus
valores:
17.11. Interfaz e implementación 173
>>> p = Punto(3, 4)
>>> vars(p)
{'y': 4, 'x': 3}
Para fines de depuración, quizás encuentres útil tener esta función a mano:
def imprimir_atributos(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
imprimir_atributos recorre el diccionario e imprime cada nombre de atributo y su valor
correspondiente.
Un principio de diseño que ayuda a alcanzar ese objetivo es mantener las interfaces separa-
das de las implementaciones. Para objetos, eso significa que los métodos que proporciona
una clase no deberían depender de la manera en que los atributos estén representados.
Por ejemplo, en este capítulo desarrollamos una clase que representa una hora del día.
Los métodos proporcionados por esta clase incluyen a tiempo_a_int, esta_despues y
sumar_tiempo.
Como alternativa, podíamos reemplazar estos atributos con un único entero que represen-
te al número de segundos desde la medianoche. Esta implementación haría que algunos
métodos, como esta_despues, sean más fáciles de escribir, pero haría que otros métodos
sean más difíciles.
Después de que implementes una clase nueva, quizás descubras una mejor implemen-
tación. Si otras partes del programa están utilizando tu clase, cambiar la interfaz podría
consumir tiempo y ser propenso a errores.
Sin embargo, si diseñaste la interfaz con cuidado, puedes cambiar la implementación sin
cambiar la interfaz, lo cual significa que otras partes del programa no tienen que cambiar.
17.12. Glosario
lenguaje orientado a objetos: Un lenguaje que proporciona características, tales como mé-
todos y tipos definidos por el programador, que facilitan la programación orientada
a objetos.
174 Capítulo 17. Clases y métodos
método: Una función que se define dentro de una definición de clase y se invoca en ins-
tancias de esa clase.
polimórfico: Dicho de una función que puede trabajar con más de un tipo.
17.13. Ejercicios
Ejercicio 17.1. Descarga el código de este capítulo en http: // thinkpython2. com/ code/
Time2. py . Cambia los atributos de Time para que sea un único entero que represente los segundos
desde la media noche. Luego, modifica los métodos (y la función int_to_time) para que funcionen
con la nueva implementación. No deberías tener que modificar el código de prueba en main. Cuan-
do termines, la salida debería ser la misma que antes. Solución: http: // thinkpython2. com/
code/ Time2_ soln. py .
Ejercicio 17.2. Este ejercicio es un cuento con moraleja acerca de uno de los errores en Python más
comunes y más difíciles de encontrar. Escribe una definición de una clase con nombre Kangaroo con
los siguientes métodos:
2. Un método con nombre put_in_pouch (poner en la bolsa) que tome un objeto de cualquier
tipo y lo añada a pouch_contents.
3. Un método __str__ que devuelva una representación de cadena del objeto Kangaroo y los
contenidos de la bolsa.
Prueba tu código creando dos objetos Kangaroo, asignándolos a variables con nombres kanga y
roo, y luego añadiendo roo al contenido de la bolsa de kanga.
Descarga http: // thinkpython2. com/ code/ BadKangaroo. py . Contiene una solución al
problema anterior con un gran y horrible error. Encuentra y corrije el error.
Herencia
Este código facilita la comparación entre cartas: dado que los palos con mayor valor ma-
pean a números mayores, podemos comparar palos comparando sus códigos.
El mapeo para los rangos es bastante obvio: cada uno de los rangos de número mapean al
entero correspondiente, y para cartas de figura:
Jota 7→ 11
Reina 7→ 12
Rey 7→ 13
Utilizo el símbolo 7→para que quede claro que estos mapeos no son parte del programa en
Python. Son parte del diseño del programa, pero no aparecen explícitamente en el código.
La definición de clase para Carta se ve así:
class Carta:
"""Representa una carta de juego estándar."""
def __str__(self):
return '%s de %s' % (Carta.nombres_de_rango[self.rango],
Carta.nombres_de_palo[self.palo])
Variables como nombres_de_palo y nombres_de_rango, que se definen dentro de una clase
pero fuera de cualquier método, se llaman atributos de clase porque están asociados al
objeto de clase Carta.
Este término los distingue de variables como palo y rango, que se llaman atributos de
instancia porque están asociados a una instancia particular.
Para ambos tipos de atributo, se accede utilizando notación de punto. Por ejemplo, en
__str__, self es un objeto Carta y self.rango es su rango. De manera similar, Carta es
un objeto de clase y Carta.nombres_de_rango es una lista de cadenas asociadas a la clase.
18.3. Comparar cartas 177
type list
Carta nombres_de_palo
list
nombres_de_rango
Carta
carta1 palo 1
rango 11
Cada carta tiene su propio palo y rango, pero hay solo una copia de nombres_de_palo y
nombres_de_rango.
Poniendo todos los elementos juntos, la expresión Carta.nombres_de_rango[self.rango]
significa “utiliza el atributo rango del objeto self como índice dentro de la lista
nombres_de_rango de la clase Carta, y selecciona la cadena correspondiente.”
El primer elemento de nombres_de_rango es None porque no hay carta con rango cero.
Incluyendo a None como guardián de lugar, obtenemos un mapeo con la genial propiedad
de que el índice 2 mapea a la cadena '2', y así sucesivamente. Para evitar este ajuste,
podríamos haber utilizado un diccionario en lugar de una lista.
Con los métodos que hemos visto hasta ahora, podemos crear e imprimir cartas:
>>> carta1 = Carta(2, 11)
>>> print(carta1)
Jota de Corazones
La Figura 18.1 es un diagrama del objeto de clase Carta y una instancia de Carta. Carta
es un objeto de clase; su tipo es type. carta1 es una instancia de Carta, por lo cual
su tipo es Carta. Para ahorrar espacio, no dibujé los contenidos de nombres_de_palo y
nombres_de_rango.
18.4. Barajas
Ahora que tenemos Cartas, el siguiente paso es definir Barajas. Dado que una baraja está
compuesta de cartas, es natural que cada Baraja contenga una lista de cartas como atributo.
Lo siguiente es una definición de clase para Baraja. El método init crea el atributo cartas
y genera el conjunto estándar de cincuenta y dos cartas:
class Baraja:
def __init__(self):
self.cartas = []
for palo in range(4):
for rango in range(1, 14):
carta = Carta(palo, rango)
self.cartas.append(carta)
La manera más fácil de llenar la baraja es con un bucle anidado. El bucle externo enumera
los palos de 0 a 3. El bucle interior enumera los rangos de 1 a 13. Cada iteración crea una
Carta nueva con el palo y el rango actuales, y la anexa a self.cartas.
def __str__(self):
res = []
18.6. Agregar, quitar, barajar y ordenar 179
def quitar_carta(self):
return self.cartas.pop()
Dado que pop quita la última carta en la lista, estamos repartiendo desde el fondo de la
baraja.
Para agregar una carta, podemos utilizar el método de lista append:
# dentro de class Baraja:
def barajar(self):
random.shuffle(self.cartas)
No olvides importar a random.
Como ejercicio, escribe un método de Baraja con nombre ordenar que utilice el método
de lista sort para ordenar las cartas en una Baraja. sort utiliza el método __lt__ que
definimos para determinar el orden.
18.7. Herencia
La herencia es la posibilidad de definir una clase nueva que sea una versión modificada de
una clase existente. Como ejemplo, digamos que queremos una clase que represente una
“mano”, es decir, las cartas sostenidas por un jugador. Una mano es similar a una baraja:
ambas están compuestas de una colección de cartas, y ambas requieren operaciones como
agregar y quitar cartas.
Una mano es también diferente de una baraja: hay operaciones para manos que no tienen
sentido para una baraja. Por ejemplo, en el póker podríamos comparar dos manos para ver
cuál gana. En el bridge, podríamos calcular el puntaje de una mano para hacer un canto.
Estas relaciones entre clases —similares, pero diferentes— dan lugar a la herencia. Para
definir una clase nueva que herede de una clase existente, pones el nombre de la clase
existente en paréntesis:
class Mano(Baraja):
"""Representa una mano de cartas de juego."""
Esta definición indica que Mano hereda de Baraja; eso significa que podemos utilizar mé-
todos como quitar_carta y agregar_carta en Manos de igual manera que en Barajas.
Cuando una clase nueva hereda de una existente, la existente se llama padre y la clase
nueva se llama hija.
En este ejemplo, Mano hereda a __init__ de Baraja, pero en realidad no hace lo que que-
remos: en lugar de llenar la mano con 52 cartas nuevas, el método init para Manos debería
inicializar cartas con una lista vacía.
Los otros métodos se heredan de Baraja, así que podemos utilizar a quitar_carta y
agregar_carta para repartir una carta:
>>> baraja = Baraja()
>>> carta = baraja.quitar_carta()
>>> mano.agregar_carta(carta)
>>> print(mano)
Rey de Picas
Un siguiente paso natural es encapsular este código en un método llamado mover_cartas:
# dentro de class Baraja:
Los objetos en una clase podrían contener referencias a objetos en otra clase. Por
ejemplo, cada Rectángulo contiene una referencia a un Punto, y cada Baraja contiene
referencias a muchas Cartas. Este tipo de relaciones se llama HAS-A (tiene un), como
en “un Rectángulo tiene un Punto.”
Una clase podría heredar de otra. Esta relación se llama IS-A (es un), como en “una
Mano es un tipo de Baraja.”
182 Capítulo 18. Herencia
Baraja * Carta
Mano
Una clase podría depender de otra en el sentido de que objetos de una clase tomen
objetos de la segunda clase como parámetros, o utilizar objetos de la segunda clase
como parte de una computación. Este tipo de relación se llama dependencia.
18.9. Depuración
La herencia puede dificultar la depuración porque cuando invocas a un método en un
objeto, podría ser difícil averiguar qué método será invocado.
Supongamos que estás escribiendo una función que trabaja con objetos Mano. Te gustaría
trabajar con todos los tipos de Mano, como ManoPóker, ManoBridge, etc. Si invocas a un
método como barajar, quizás obtengas el que se definió en Baraja, pero si alguna de las
subclases anula este método, obtendrás esa versión en su lugar. Este comportamiento es
generalmente algo bueno, pero puede ser confuso.
Cada vez que tengas insegurdad acerca del flujo de ejecución a través de tu programa,
la solución más simple es agregar sentencias print al principio de métodos relevantes. Si
Baraja.barajar imprime un mensaje que diga algo como Ejecutando Baraja.barajar,
entonces se sigue el flujo de ejecución a medida que el programa avanza.
Como alternativa, podrías utilizar esta función, que toma un objeto y un nombre de método
(como cadena) y devuelve la clase que proporciona la definición del método:
18.10. Encapsulamiento de datos 183
Aquí hay una sugerencia de diseño: cuando anulas un método, la interfaz del nuevo mé-
todo debería ser la misma que el antiguo. Debería tomar los mismos parámetros, devolver
el mismo tipo y cumplir las mismas precondiciones y postcondiciones. Si sigues esta re-
gla, encontrarás que cualquier función diseñada para trabajar con una instancia de una
clase padre, como Baraja, funcionará también con instancias de clases hijas, como Mano y
ManoPóker.
Si violas esta regla, que se llama “principio de sustitución de Liskov”, tu código colapsará
como (lo siento) un castillo de cartas.
Sin embargo, a veces es menos obvio qué objetos necesitas y cómo deberían interactuar. En
ese caso, necesitas un plan de desarrollo diferente. De la misma manera en que descubri-
mos interfaces de función encapsulando y generalizando, podemos descubrir interfaces de
clase a través del encapsulamiento de datos.
class Markov:
def __init__(self):
self.suffix_map = {}
self.prefix = ()
Luego, transforamos las funciones en métodos. Por ejemplo, aquí está process_word:
def process_word(self, word, order=2):
if len(self.prefix) < order:
self.prefix += (word,)
return
try:
self.suffix_map[self.prefix].append(word)
except KeyError:
# si no hay entrada para este prefijo, crear una
self.suffix_map[self.prefix] = [word]
1. Comienza escribiendo funciones que lean y escriban variables globales (cuando sea
necesario).
2. Una vez que hagas funcionar el programa, busca asociaciones entre variables globales
y las funciones que las utilizan.
18.11. Glosario
codificar: Representar un conjunto de valores utilizando otro conjunto de valores, cons-
truyendo un mapeo entre ambos.
atributo de clase: Un atributo asociado a un objeto de clase. Los atributos de clase se defi-
nen dentro de una definición de clase pero afuera de cualquier método.
enchapado: Un método o función que proporciona una interfaz diferente a otra función
sin hacer muchas computaciones.
18.12. Ejercicios 185
herencia: La posibilidad de definir una clase nueva que sea una versión modificada de
una clase definida anteriormente.
clase hija: Una clase nueva creada a través de herencia de una clase existente; también
llamada “subclase”.
relación IS-A: Una relación entre una clase hija y su clase padre.
relación HAS-A: Una relación entre dos clases donde instancias de una clase contienen
referencias a instancias de la otra.
dependencia: Una relación entre dos clases donde instancias de una clase utilizan instan-
cias de otra clase, pero no las almacenan como atributos.
diagrama de clase: Un diagrama que muestra las clases en un programa y las relaciones
entre estas.
multiplicidad: Una notación en un diagrama de clase que muestra, para una relación
HAS-A, cuántas referencias a instancias de otra clase hay.
18.12. Ejercicios
Ejercicio 18.1. Para el siguiente programa, dibuja un diagrama de clase UML que muestre estas
clases y las relaciones entre estas.
class PingPongPadre:
pass
class Ping(PingPongPadre):
def __init__(self, pong):
self.pong = pong
class Pong(PingPongPadre):
def __init__(self, pings=None):
if pings is None:
self.pings = []
else:
self.pings = pings
pong = Pong()
ping = Ping(pong)
pong.agregar_ping(ping)
186 Capítulo 18. Herencia
Ejercicio 18.2. Escribe un método de Baraja llamado repartir_manos que tome dos parámetros,
el número de manos y el número de cartas por cada mano. Debería crear el número apropiado de
objetos Mano, repartir el número apropiado de cartas por cada Mano y devolver una lista de Manos.
Ejercicio 18.3. Lo siguiente son las posibles manos en el póker, en orden creciente de valor y decre-
ciente de probabilidad:
4. Escribe un método con nombre clasificar que averigüe la clasificación de mayor valor para
una mano y ponga el atributo etiqueta según corresponda. Por ejemplo, una mano de 7
cartas podría contener un “color” y una “pareja”; debería etiquetarse “color”.
5. Cuando te convenzas de que tus métodos de clasificación funcionan, el siguiente paso es es-
timar las probabilidades de varias cartas. Escribe una función en PokerHand.py que baraje
una baraja de cartas, la divida en manos, clasifique las manos y cuente el número de veces que
aparecen varias clasificaciones.
6. Imprime una tabla con las clasificaciones y sus probabilidades. Ejecuta tu programa con nú-
meros más y más grandes de manos hasta que los valores de salida converjan a un grado de
exactitud razonable. Compara tus resultado con los valores en http: // en. wikipedia.
org/ wiki/ Hand_ rankings .
Trucos extra
Uno de mis objetivos para este libro ha sido enseñarte la menor cantidad de Python posible.
Cuando había dos maneras de hacer algo, escogí una y evité mencionar la otra. O a veces
puse la segunda dentro de un ejercicio.
Ahora quiero volver a ver algunas de estas cosas buenas que dejé atrás. Python proporcio-
na un número de características que no son realmente necesarias —puedes escribir buen
código sin estas— pero con estas a veces puedes escribir código más conciso, legible o
eficiente, y a veces las tres cosas a la vez.
Podemos escribir esta sentencia de manera más concisa utilizando una expresión condi-
cional:
y = math.log(x) if x > 0 else float('nan')
Puedes leer esta línea casi como si estuviera en inglés: “y es log-x si x es mayor que 0; de lo
contrario es NaN”.
else:
return n * factorial(n-1)
Podemos reescribirla así:
def factorial(n):
return 1 if n == 0 else n * factorial(n-1)
Otro uso de las expresiones condicionales es al manejar argumentos opcionales. Por ejem-
plo, aquí está el método init de GoodKangaroo (ver Ejercicio 17.2):
def __init__(self, name, contents=None):
self.name = name
if contents == None:
contents = []
self.pouch_contents = contents
Podemos reescribirla así:
def __init__(self, name, contents=None):
self.name = name
self.pouch_contents = [] if contents == None else contents
En general, puedes reemplazar una sentencia condicional con una expresión condicional
si ambas ramas contienen expresiones simples que se devuelven o asignan a la misma
variable.
Sin embargo, en mi defensa, las comprensiones de lista son difíciles de depurar porque no
puedes poner una sentencia print dentro del bucle. Sugiero que los utilices solo si el cálculo
es lo suficientemente simple para que sea probable que lo hagas bien al primer intento. Y
para principiantes eso significa nunca.
Los contadores proporcionan métodos y operadores para realizar operaciones como las
de los conjuntos, incluyendo la suma, resta, unión e intersección. Además, proporcionan
un método que a menudo es útil, most_common, que devuelve una lista de pares valor-
frecuencia, ordenados desde el más común al menos común:
>>> contar = Counter('parrot')
>>> for valor, frecuencia in contar.most_common(3):
... print(valor, frecuencia)
r 2
p 1
a 1
19.7. defaultdict
El módulo collections también proporciona a defaultdict, que es como un diccionario
excepto que si accedes a una clave que no existe, este puede generar un valor nuevo sobre
la marcha.
Cuando creas un defaultdict, proporcionas una función que se utiliza para crear valores
nuevos. Una función utilizada para crear objetos a veces es llamada una fábrica. Las fun-
ciones incorporadas que crean listas, conjuntos y otros tipos se pueden utilizar como fábri-
cas:
>>> from collections import defaultdict
>>> d = defaultdict(list)
Notar que el argumento es list, que es un objeto de clase, no list(), que es una lista
nueva. La función que proporcionas no es llamada a menos que accedas a una clave que
no existe.
>>> t = d['clave nueva']
>>> t
[]
19.7. defaultdict 193
La lista nueva, la cual llamamos t, también es añadida al diccionario. Por lo tanto, si modi-
ficamos t, el cambio aparece en d:
>>> t.append('valor nuevo')
>>> d
defaultdict(<class 'list'>, {'clave nueva': ['valor nuevo']})
Si estás creando un diccionario de listas, a menudo puedes escribir código más simple
utilizando defaultdict. En mi solución al Ejercicio 12.2, que puedes obtener en http:
//thinkpython2.com/code/anagram_sets.py, creo un diccioinario que mapea de una ca-
dena de letras ordenada a la lista de palabras que se pueden escribir con esas letras. Por
ejemplo, 'opst' mapea a la lista ['opts', 'post', 'pots', 'spot', 'stop', 'tops'].
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
Esto es mucho código para transmitir una cantidad pequeña de información. Python pro-
porciona una manera más concisa de decir lo mismo:
from collections import namedtuple
Punto = namedtuple('Punto', ['x', 'y'])
El primer argumento es el nombre de la clase que quieres crear. El segundo es una lista de
atributos que los objetos Punto debieran tener, en forma de cadenas. El valor de retorno de
namedtuple es un objeto de clase:
>>> Punto
<class '__main__.Punto'>
Punto automáticamente proporciona métodos como __init__ y __str__, así que no tienes
que escribirlos.
Para crear un objeto Punto, utilizas la clase Punto como una función:
>>> p = Punto(1, 2)
>>> p
Punto(x=1, y=2)
El método init asigna argumentos a atributos utilizando nombres que proporcionas. El
método str imprime una representación del objeto Punto y sus atributos.
Puedes acceder a los elementos de la tupla con nombre, escribiendo sus nombres:
>>> p.x, p.y
(1, 2)
Sin embargo, puedes también tratar a una tupla con nombre como una tupla:
>>> p[0], p[1]
(1, 2)
>>> x, y = p
>>> x, y
(1, 2)
Las tuplas con nombre proporcionan una manera rápida de definir clases simples. El in-
conveniente es que las clases simples no siempre permanecen simples. Tal vez decidas más
tarde que quieres agregar métodos a una tupla con nombre. En ese caso, podrías definir
una clase nueva que tome herencia de la tupla con nombre:
class SuperPunto(Punto):
# agregar más métodos aquí
O bien podrías cambiar a una definición de clase convencional.
19.9. Reunir argumentos de palabra clave 195
19.10. Glosario
expresión condicional: Una expresión que tiene uno de dos valores, dependiendo de una
condición.
comprensión de lista: Una expresión con un bucle for en corchetes que produce una lista
nueva.
196 Capítulo 19. Trucos extra
expresión generadora: Una expresión con un bucle for en paréntesis que produce un ob-
jeto generador.
multiconjunto: Una entidad matemática que representa un mapeo entre los elementos de
un conjunto y el número de veces que estos aparecen.
fábrica: Una función, generalmente pasada como parámetro, utilizada para crear objetos.
19.11. Ejercicios
Ejercicio 19.1. Lo siguiente es una función que calcula el coeficiente binomial de manera recursiva.
def coeficiente_binomial(n, k):
"""Calcula el coeficiente binomial "n sobre k".
n: número de ensayos
k: número de éxitos
devuelve: int
"""
if k == 0:
return 1
if n == 0:
return 0
Nota: esta función no es muy eficiente porque termina calculando los mismos valores una y otra vez.
Podrías hacerla más eficiente memoizando (ver Sección 11.6). Sin embargo, encontrarás que es más
difícil memoizar si la escribes utilizando expresiones condicionales.
Apéndice A
Depuración
Cuando estés depurando, deberías distinguir entre los diferentes tipos de errores con el fin
de rastrearlos de manera más rápida:
Los errores de sintaxis son descubiertos por el intérprete cuando está traduciendo el
código fuente a código byte. Indican que hay algo mal en la estructura del programa.
Ejemplo: omitir el signo de dos puntos al final de una sentencia def genera el mensaje
algo redundante SyntaxError: invalid syntax.
Los errores de tiempo de ejecución son producidos por el intérprete si algo va mal
mientras el programa se está ejecutando. La mayoría de los mensajes de error de
tiempo de ejecución incluyen información acerca de dónde ocurrió el error y qué
funciones se estaban ejecutando. Ejemplo: una recursividad infinita eventualmente
causa el error de tiempo de ejecución “maximum recursion depth exceeded”.
Los errores semánticos son problemas que tiene un programa que se ejecuta sin pro-
ducir mensajes de error pero sin hacer lo correcto. Ejemplo: una expresión puede que
no sea evaluada en el orden que esperas, entregando un resultado incorrecto.
El primer paso en la depuración es averiguar con qué tipo de error estás lidiando. Aunque
las siguientes secciones están organizadas por tipo de error, algunas técnicas son aplicables
en más de una situación.
Por otra parte, el mensaje sí te dice el lugar del programa donde ocurrió el problema. En
realidad, te dice dónde Python notó un problema, que no necesariamente es donde está el
error. A veces el error está antes de la ubicación del mensaje de error, a menudo en la línea
precedente.
198 Apéndice A. Depuración
Si estás construyendo el programa de manera incremental, deberías tener una buena idea
acerca de dónde está el error. Estará en la última línea que agregaste.
Si estás copiando código de un libro, comienza comparando tu código con el código del
libro de manera muy cuidadosa. Revisa cada carácter. Al mismo tiempo, recuerda que el
libro podría estar mal, por tanto si ves algo que parece un error de sintaxis, puede serlo.
Aquí hay algunas maneras de evitar los errores de sintaxis más comunes:
1. Asegúrate de que no estás utilizando una palabra clave de Python para un nombre
de variable.
2. Verifica que tienes un signo de dos puntos al final del encabezado de cada sentencia
compuesta, incluyendo las sentencias for, while, if y def.
3. Asegúrate de que todas las cadenas en el código tengan comillas coincidentes. Ase-
gúrate de que todas las comillas son “comillas rectas”, no “comillas tipográficas”.
4. Si tienes cadenas multilínea con comillas triples (simples o dobles), asegúrate de que
has terminado la cadena de manera apropiada. Una cadena sin terminar puede cau-
sar un error invalid token al final de tu programa, o puede tratar la siguiente parte
del programa como una cadena hasta que llega a la siguiente cadena. En el segundo
caso, ¡podría no producir ningún mensaje de error!
7. Revisa la sangría para asegurarte de que esté alineada como se supone que debe
estar. Python puede manejar el espacio y la tabulación, pero si los mezclas puede
causar problemas. La mejor manera de evitar este problema es utilizar un editor de
texto que sepa sobre Python y genere sangría consistente.
Si no sabes bien, intenta poniendo un error de sintaxis obvio y deliberado al principio del
programa. Ahora ejecútalo de nuevo. Si el intérprete no encuentra el nuevo error, no estás
ejecutando el código nuevo.
Si te atascas y no puedes averiguar qué está pasando, una manera de abordarlo es comen-
zar de nuevo con un nuevo programa como “Hola, mundo” y asegurarte de que puedes
obtener un programa conocido para ejecutar. Luego agrega gradualmente los pedazos del
programa original al nuevo programa.
Si hay un bucle en particular del cual sospechas que es el problema, agrega una sen-
tencia print inmediatamente antes del bucle que diga “entrando al bucle” y otro
inmediatamente después que diga “saliendo del bucle”.
Ejecuta el programa. Si obtienes el primer mensaje y no el segundo, tienes un bucle
infinito. Ve a la sección “Bucle infinito” de más adelante.
La mayor parte del tiempo, una recursividad infinita causará que el programa se
ejecute por un momento y luego produzca un error “RuntimeError: Maximum recur-
sion depth exceeded”. Si eso ocurre, ve a la sección “Recursividad infinita” de más
adelante.
Si no obtienes este error pero sospechas que hay un problema con una función recur-
siva o método recursivo, todavía puedes utilizar las técnicas de la sección “Recursi-
vidad infinita”.
200 Apéndice A. Depuración
Si ninguno de esos pasos funciona, comienza a probar otros bucles y otras funciones
y métodos recursivos.
Bucle infinito
Si crees que tienes un bucle infinito y crees que sabes qué bucle está causando el problema,
agrega una sentencia print al final del bucle que imprima los valores de las variables en la
condición y el valor de la condición.
Por ejemplo:
while x > 0 and y < 0 :
# hacer algo a x
# hacer algo a y
print('x: ', x)
print('y: ', y)
print("condición: ", (x > 0 and y < 0))
Ahora cuando ejecutes el programa, verás tres líneas de salida cada vez que se pase por
el bucle. En el último paso por el bucle, la condición debería ser False. Si el bucle conti-
núa, podrás ver los valores de x e y, y podrías averiguar por qué no se están actualizando
correctamente.
Recursividad infinita
La mayoría de las veces, la recursividad infinita causa que el programa se ejecute por un
momento y luego produzca un error Maximum recursion depth exceeded.
Si sospechas que una función está causando una recursividad infinita, asegúrate de que
haya un caso base. Debería haber alguna condición que cause que la función devuelva
algo sin hacer una invocación recursiva. Si no, necesitas volver a pensar el algoritmo e
identificar un caso base.
Si hay un caso base pero el programa no parece estar alcanzándolo, agrega una senten-
cia print al principio de la función que imprima los parámetros. Ahora cuando ejecutes
el programa, verás algunas líneas de salida cada vez que se invoca a la función, y verás
los valores de los parámetros. Si los parámetros no se están moviendo hacia el caso base,
obtendrás algunas ideas sobre por qué no ocurre.
Flujo de ejecución
Ahora cuando ejecutes el programa, imprimirá una señal de cada función que se invoque.
A.2. Errores de tiempo de ejecución 201
NameError: Estás intentando utilizar una variable que no existe en el entorno actual. Re-
visa si el nombre está bien escrito, o al menos de manera consistente. Y recuerda que
las variables locales son locales: no puedes referirte a estas desde afuera de la función
donde se definieron.
TypeError: Hay varias causas posibles:
Estás intentando utilizar un valor de manera inapropiada. Ejemplo: indexar una
cadena, lista o tupla con algo que no es un entero.
Hay una discordancia entre los ítems en una cadena de formato y los ítems pa-
sados para una conversión. Eso puede ocurrir si el número de ítems no coincide
o si se pidió una conversión no válida.
Estás pasando el número equivocado de argumentos a una función. Para los
métodos, mira la definición del método y verifica que el primer parámetro es
self. Luego, mira la invocación al método; asegúrate de que estás invocando al
método en un objeto con el tipo correcto y proporcionando los otros argumentos
de manera correcta.
KeyError: Estás intentando acceder a un elemento de un diccionario utilizando una clave
que el diccionario no contiene. Si las claves son cadenas, recuerda que las mayúsculas
importan.
AttributeError: Estás intentando acceder a un atributo o método que no existe. ¡Revisa la
ortografía! Puedes utilizar la función incorporada vars para hacer una lista de los
atributos que sí existen.
Si un AttributeError indica que un objeto tiene NoneType, eso significa que es None.
Entonces el problema no es el nombre de atributo, sino el objeto.
La razón por la cual el objeto es None podría ser que olvidaste devolver un valor
desde una función; si llegas al final de una función poniendo una sentencia return,
devuelve None. Otra causa común es utilizar el resultado de un método de lista, como
sort, que devuelve None.
IndexError: El índice que estás utilizando para acceder a una lista, cadena o tupla es ma-
yor que su longitud menos uno. Inmediatamente antes del lugar del error, agrega
una sentencia print para mostrar en pantalla el valor del índice y la longitud de la
secuencia. ¿Tiene la secuencia el tamaño correcto? ¿Tiene el índice el valor correcto?
El depurador de Python (pdb, Python debugger) es útil para rastrear excepciones porque
te permite examinar el estado del programa inmediatamente antes del error. Puedes leer
sobre pdb en https://docs.python.org/3/library/pdb.html.
202 Apéndice A. Depuración
¿Hay algo que se supone que el programa debe hacer pero que no parece estar ocu-
rriendo? Encuentra la sección del código que realiza esa función y asegúrate de que
se está ejecutando cuando crees que debería.
A.3. Errores semánticos 203
¿Ocurre algo que no debería? Encuentra código en tu programa que realiza esa fun-
ción y ve si se está ejecutando cuando no debería.
¿Hay una sección de código produciendo un efecto que no es lo que esperabas? Ase-
gúrate de que entiendes el código en cuestión, especialmente si involucra funciones o
métodos de otros módulos de Python. Lee la documentación para las funciones que
llamas. Pruébalas escribiendo casos de prueba simples y verificando los resultados.
Para programar, necesitas un modelo mental de cómo funcionan los programas. Si escribes
un programa que no hace lo que esperas, muchas veces el problema no está en el programa:
está en tu modelo mental.
La mejor manera de corregir tu modelo mental es separar el programa en sus componen-
tes (generalmente las funciones y métodos) y probar cada componente de manera inde-
pendiente. Una vez que encuentres la discrepancia entre tu modelo y la realidad, puedes
resolver el problema.
Por supuesto, deberías estar construyendo y probando componentes a medida que desa-
rrollas el programa. Si encuentras un problema, debería haber solo una pequeña cantidad
de código nuevo que no se sabe si es correcto.
Frustración e ira.
Creencias supersticiosas (“el computador me odia”) y pensamiento mágico (“el pro-
grama solo funciona cuando uso mi gorra hacia atrás”).
Programación de camino aleatorio (el intento de programar escribiendo cada progra-
ma posible y escoger el que hace lo correcto).
Cuando encuentres el error, tómate un segundo para pensar sobre qué podrías haber hecho
para encontrarlo de manera más rápida. La próxima vez que veas algo similar, serás capaz
de entontrar el error con más rapidez.
Recuerda, la meta no solo es hacer que el programa funcione. La meta es aprender cómo
hacer que el programa funcione.
206 Apéndice A. Depuración
Apéndice B
Análisis de algoritmos
rápida de ordenar un millón de enteros es utilizar cualquier función de ordenamiento que sea proporcionada por
el lenguaje que estoy utilizando. Su rendimiento es suficientemente bueno para la gran mayoría de las aplicacio-
nes, pero si resulta que mi aplicación fue muy lenta, utilizaría un analizador de rendimiento para ver dónde fue
utilizado el tiempo. Si pareciera que un algoritmo de ordenamiento más rápido tendría un efecto importante en
el rendimiento, entonces buscaría una buena implementación del ordenamiento radix.”
208 Apéndice B. Análisis de algoritmos
El rendimiento relativo podría depender de los detalles del conjunto de datos. Por
ejemplo, algunos algoritmos de ordenamiento se ejecutan de manera más rápida si
los datos ya están parcialmente ordenados; otros algoritmos se ejecutan de manera
más lenta en este caso. Una manera común de evitar este problema es analizar el
peor de los casos. A veces es útil analizar el rendimiento del caso promedio, pero
eso es generalmente más difícil, y el conjunto de casos sobre el cual se establece el
promedio podría no ser obvio.
El rendimiento relativo depende también del tamaño del problema. Un algoritmo de
ordenamiento que es rápido para listas pequeñas podría ser lento para listas largas.
La solución usual a este problema es expresar el tiempo de ejecución (o número de
operaciones) como una función del tamaño del problema, y agrupar funciones en ca-
tegorías dependiendo de qué tan rápido crecen a medida que el tamaño del problema
aumenta.
Lo bueno de este tipo de comparaciones es que asegura una clasificación simple de los
algoritmos. Por ejemplo, si sé que el tiempo de ejecución del Algorigmo A tiende a ser
proporcional al tamaño de la entrada, n, y el algoritmo B tiende a ser proporcional a n2 ,
entonces espero que A sea más rápido que B, al menos para valores grandes de n.
Este tipo de análisis viene con algunas advertencias, pero llegaremos a eso más adelante.
En general, esperamos que un algoritmo con un término principal más pequeño sea un
mejor algoritmo para problemas grandes, pero para problemas más pequeños puede haber
un punto de cruce donde otro algoritmo es mejor. La ubicación del punto de cruce depende
de los detalles de los algoritmos, las entradas y el hardware, por lo cual usualmente se
ignora para propósitos de análisis algorítmico. Pero eso no significa que puedes olvidarlo.
Si dos algoritmos tienen el mismo término de orden principal, es difícil decir cuál es mejor;
nuevamente, la respuesta depende de los detalles. Entonces para análisis algorítmico, las
funciones con el mismo término principal se consideran equivalentes, incluso si tienen
coeficientes diferentes.
Todas las funciones con término principal n2 pertenecen a O(n2 ): se llaman cuadráticas.
La siguiente tabla muestra algunos de los órdenes de crecimiento que aparecen más co-
múnmente en el análisis algorítmico, en orcen creciente de ineficiencia.
Orden de Nombre
crecimiento
O (1) constante
O(logb n) logarítmica (para cualquier b)
O(n) lineal
O(n logb n) linearítmica
O ( n2 ) cuadrática
O ( n3 ) cúbica
O(cn ) exponencial (para cualquier c)
Para los términos logarítmicos, la base del logarítmo no importa: cambiar las bases es equi-
valente a multiplicar por una constante, la cual no cambia el orden de crecimiento. De igual
manera, todas las funciones exponenciales pertenecen al mismo orden de crecimiento, in-
dependiente de la base del exponente. Las funciones exponenciales crecen de manera muy
rápida, por lo cual los algoritmos exponenciales solo son útiles para problemas pequeños.
Ejercicio B.1. Lee la página de Wikipedia sobre la notación O grande en http: // en.
wikipedia. org/ wiki/ Big_ O_ notation y responde las siguientes preguntas:
3. Si f está en O( g), para una función g no especificada, ¿qué podemos decir de a f + b, donde
a y b son constantes?
Los programadores que se preocupan del rendimiento a menudo encuentran este tipo de
análisis difícil de tragar. Ellos tienen un punto: a veces los coeficientes y los términos no
principales hacen una diferencia real. A veces los detalles del hardware, el lenguaje de pro-
gramación y las características de la entrada hacen una gran diferencia. Y para problemas
pequeños, el orden de crecimiento es irrelevante.
Un bucle for que recorre una secuencia o diccionario es generalmente lineal, siempre y
cuando todas las operaciones en el cuerpo del bucle sean de tiempo constante. Por ejemplo,
sumar los elementos de una lista es lineal:
total = 0
for x in t:
total += x
La función incorporada sum también es lineal porque hace lo mismo, pero tiende a ser más
rápida porque es una implementación más eficiente; en el lenguaje del análisis algorítmico,
tiene un coeficiente principal más pequeño.
Como regla general, si el cuerpo de un bucle está en O(n a ) entonces el bucle completo está
en O(n a+1 ). La excepción es cuando puedes mostrar que el bucle se interrumpe después
de un número constante de iteraciones. Si un bucle se ejecuta k veces independiente de n,
entonces el bucle está en O(n a ), incluso para k grande.
La mayoría de las operaciones de cadena y de tupla son lineales, excepto indexar y len,
que es de tiempo constante. Las funciones incorporadas min y max son lineales. El tiempo
de ejecución de una operación de trozo es proporcional a la longitud de la salida, pero
independiente del tamaño de la entrada.
Todos los métodos de cadena son lineales, pero si las longitudes de las cadenas están aco-
tadas por una constante —por ejemplo, operaciones en caracteres individuales— se con-
sideran de tiempo constante. El método de cadena join es lineal: el tiempo de ejecución
depende de la longitud total de las cadenas.
La mayoría de los métodos de lista son lineales, pero hay algunas excepciones:
La mayoría de las operaciones y métodos de diccionario son de tiempo constante, pero hay
algunas excepciones:
keys, values e items son de tiempo constante porque devuelven iteradores. Pero si
recorres un bucle con los iteradores, el bucle será lineal.
2. ¿Cuál es el orden de crecimiento del ordenamiento de burbuja, y por qué Barack Obama piensa
que es “la manera incorrecta de proceder”?
add(k, v): Agrega un nuevo ítem que mapea de una clave k a un valor v. Con un diccio-
nario de Python, d, esta operación se escribe d[k] = v.
get(k): Busca y devuelve el valor que corresponde a la clave k. Con un diccionario de
Python, d, esta operación se escribe d[k] o d.get(k).
Por ahora, supongo que cada clave aparece una sola vez. La implementación más simple
de esta interfaz utiliza una lista de tuplas, donde cada tupla es un par clave-valor.
B.4. Tablas hash 213
class LinearMap:
def __init__(self):
self.items = []
get utiliza un bucle for para buscar la lista: encuentra la clave objetivo y devuelve el valor
correspondiente; de lo contrario, ocurre un KeyError. Entonces, get es lineal.
Una alternativa es mantener la lista ordenada por clave. Entonces get podría utilizar una
búsqueda de bisección, que es O(log n). Pero insertar un nuevo ítem en el medio de una
lista es lineal, por lo cual podría no ser la mejor opción. Hay otras estructuras de datos que
pueden implementar add y get en tiempo logarítmico, pero eso aún no es tan bueno como
el tiempo constante, así que vamos a lo siguiente.
Una manera de mejorar LinearMap es separar la lista de pares clave-valor en listas más
pequeñas. Aquí hay una implementación llamada BetterMap, que es una lista de 100 Li-
nearMaps. Tal como veremos en un segundo, el orden de crecimiento para get aún es
lineal, pero BetterMap es un paso en el camino hacia las tablas hash:
class BetterMap:
find_map es utilizado por add y get para averiguar en cuál mapa poner el nuevo ítem, o
en cuál mapa buscar.
214 Apéndice B. Análisis de algoritmos
find_map utiliza la función incorporada hash, que toma casi cualquier objeto de Python y
devuelve un entero. Una limitación de esta implementación es que solo funciona con claves
hashables. Los tipos mutables como las listas y los diccionarios no son hashables.
Los objetos hashables que se consideran equivalentes devuelven el mismo valor hash, pe-
ro lo inverso no es necesariamente verdadero: dos objetos con valores diferentes pueden
devolver el mismo valor hash.
find_map utiliza el operador de módulo para ajustar los valores hash en el rango entre
0 y len(self.maps), por lo cual el resultado es un índice legal en la lista. Por supuesto,
esto significa que muchos valores hash diferentes se ajustarán al mismo índice. Pero si
la función hash distribuye las cosas de manera muy uniforme (que es para lo que están
diseñadas las funciones hash), entonces esperamos n/100 ítems por cada LinearMap.
Dado que el tiempo de ejecución de LinearMap.get es proporcional al número de ítems,
esperamos que BetterMap sea cerca de 100 veces más rápido que LinearMap. El orden de
crecimiento es aún lineal, pero el coeficiente principal es más pequeño. Eso es bueno, pero
aún no es tan bueno como una tabla hash.
Aquí (finalmente) está la idea clave que hace que las tablas hash sean rápidas: si puedes
mantener acotada la longitud máxima de los LinearMaps, LinearMap.get es de tiempo
constante. Todo lo que tienes que hacer es un seguimiento del número de ítems y cuando
el número de ítems por cada LinearMap exceda un límite, cambiar el tamaño de la tabla
hash añadiendo más LinearMaps.
Aquí hay una implementación de una tabla hash:
class HashMap:
def __init__(self):
self.maps = BetterMap(2)
self.num = 0
self.maps.add(k, v)
self.num += 1
def resize(self):
new_maps = BetterMap(self.num * 2)
for m in self.maps.maps:
for k, v in m.items:
new_maps.add(k, v)
self.maps = new_maps
__init__ crea un BetterMap e inicializa a num, que hace un seguimiento del número de
ítems.
B.4. Tablas hash 215
get solo despacha a BetterMap. El trabajo real ocurre en add, que verifica el número de
ítems y el tamaño del BetterMap: si son iguales, el número promedio de ítems por cada
LinearMap es 1, entonces llama a resize.
resize crea un nuevo BetterMap, dos veces más grande que el anterior, y luego “rehashea”
los ítems desde el mapa antiguo hacia el nuevo.
Rehashear es necesario porque al cambiar el número de LinearMaps cambia el denomi-
nador del operador de módulo en find_map. Eso significa que algunos objetos que solían
hacer hash en el mismo LinearMap se dividirán (que es lo que queríamos, ¿verdad?).
El rehasheo es lineal, entonces resize es lineal, que podría parecer mal, dado que prometí
que add sería de tiempo constante. Pero recuerda que no tenemos que cambiar de tamaño
cada vez, entonces add es generalmente de tiempo constante y solo ocasionalmente lineal.
La cantidad total de trabajo al ejecutar add n veces es proporcional a n, ¡entonces el tiempo
promedio de cada add es de tiempo constante!
Para ver cómo funciona esto, piensa en comenzar con un HashTable vacío y agregar una
secuencia de ítems. Comenzamos con 2 LinearMaps, entonces los 2 primeros añadidos son
rápidos (no se requiere cambiar tamaño). Digamos que estos toman una unidad de trabajo
cada uno. El siguiente añadido requiere un cambio de tamaño, por lo cual tenemos que
rehashear los primeros dos ítems (digamos que 2 unidades más de trabajo) y luego añadir
el tercer ítem (una unidad más). Añadir el siguiente ítem cuesta 1 unidad, entonces el total
hasta ahora es de 6 unidades de trabajo para 4 ítems.
El siguiente add cuesta 5 unidades, pero los siguientes tres son solo una unidad cada uno,
entonces el total es de 14 unidades para los primeros 8 añadidos.
El siguiente add cuesta 9 unidades, pero luego podemos añadir 7 más antes del siguiente
cambio de tamaño, entonces el total es de 30 unidades para los primeros 16 añadidos.
Después de 32 añadidos, el costo total es de 62 unidades, y espero que comiences a ver
el patrón. Después de n añadidos, donde n es potencia de dos, el costo total es de 2n −
2 unidades, entonces el trabajo promedio por cada añadido es un poco menos que dos
unidades. Cuando n es una potencia de dos, ese es el mejor caso; para los otros valores de
n el trabajo promedio es un poco más alto, pero eso no es importante. Lo importante es que
es O(1).
La Figura B.1 muestra cómo funciona esto de manera gráfica. Cada bloque representa una
unidad de trabajo. Las columnas muestran el trabajo total para cada añadido en orden de
izquierda a derecha: los primeros dos adds cuestan 1 unidad cada uno, el tercero cuesta 3
unidades, etc.
El trabajo extra de rehashear aparece como una secuencia de torres cada vez más altas cuyo
espacio entre ellas es cada vez mayor. Ahora si derribas las torres, repartiendo el costo de
cambiar de tamaño sobre todos los añadidos, puedes ver gráficamente que el costo total
después de n añadidos es 2n − 2.
Una característica importante de este algoritmo es que, cuando cambiamos el tamaño del
HashTable, este crece de manera geométrica, es decir, multiplicamos el tamaño por una
constante. Si aumentas el tamaño de manera aritmética —añadiendo un número fijo cada
vez— el tiempo promedio por cada add es lineal.
Puedes descargar mi implementación de HashMap en http://thinkpython2.com/code/
Map.py, pero recuerda que no hay razón para utilizarla: si quieres un mapa, solo utiliza un
diccionario de Python.
216 Apéndice B. Análisis de algoritmos
B.5. Glosario
análisis de algoritmos: Una manera de comparar algoritmos en términos de su tiempo de
ejecución y/o requerimientos de espacio.
peor de los casos: La entrada que hace que un algoritmo dado se ejecute de la manera más
lenta (o requiera el mayor espacio).
orden de crecimiento: Un conjunto de funciones que crecen todas de una manera consi-
derada equivalente para propósitos del análisis de algoritmos. Por ejemplo, todas las
funciones que crecen linealmente pertenecen al mismo orden de crecimiento.
búsqueda: El problema de localizar un elemento de una colección (como una lista o dic-
cionario) o determinar que este no está presente.
tabla hash: Una estructura de datos que representa una colección de pares clave-valor y
realiza una búsqueda en tiempo constante.
Índice alfabético
vacía
cadena, 97
lista, 91
valor, 4, 7, 97, 98, 114
tupla, 119
valor de retorno, 17, 26, 51, 152
tupla, 119
valor especial
False, 40
None, 24, 26, 52, 94, 96
True, 40
valor por defecto, 131, 136, 169
evitar mutable, 174
ValueError, 46, 118
values, método, 106
variable, 9, 14
actualizar, 64
variable global, 112, 115
actializar, 113
variable local, 22, 26
variable temporal, 51, 60, 203
varlable del bucle, 188
verificación de errores, 58
verificación de tipos, 58
vorpal, 56
while, bucle, 64
zip
función, 120
objeto, 125
usar con dict, 122